Skip to content

feat(tpviz): new transport-type colors + WebGL graph view (default) for full all-transports - #9

Closed
0pcom wants to merge 54 commits into
developfrom
feat/tpviz-transport-types
Closed

feat(tpviz): new transport-type colors + WebGL graph view (default) for full all-transports#9
0pcom wants to merge 54 commits into
developfrom
feat/tpviz-transport-types

Conversation

@0pcom

@0pcom 0pcom commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

The transport-graph UI didn't know the newer transport types and the vis-network Flat view hangs at the full all-transports scale. This PR fixes both and adds a GPU-rendered view.

Transport types

stcpr/sudph/dmsg were the only colored types; the fleet now also runs squicr (QUIC), swtr (WebTransport), swsr (WebSocket), and webrtc — all falling through to a gray default with no legend or filter. Added colors + legend swatches + filter checkboxes across both the tpviz flat/globe views and the Angular network-visualizer, so the two graphs read consistently. (Live TPD data: ~44% sudph, ~38% stcpr, ~14% webrtc, plus squicr/swtr.)

WebGL view (now the default)

The vis-network Flat view (Canvas2D) stabilizes its force layout on the main thread and re-tessellates every edge per redraw; at ~20k edges that froze the UI for seconds. Added a third WebGL view backed by @cosmograph/cosmos (GPU force simulation + rendering) that draws the entire routable topology — all-transports, the set the route finder actually builds routes from (GetTransportsByEdge), not just the QoS-reporting /metrics subset — without blocking the main thread. It's now the default; Flat/Globe remain switchable.

Interaction/feature parity with the Flat view:

  • Fit / zoom± buttons, hover tooltip + neighborhood highlight, click → node-info and the TPS/group/local-transport/multihop/dmsg-health/ping pick-modes (shared node-click.ts), focusNode / sidebar-list / search → zoom-to-node.
  • Straight edges; layout tuned to frame the viewport.

Not ported (documented follow-ups): always-on node labels (infeasible with a points renderer at ~1k nodes), clustering / country-grouping / group boundaries / satellite orbits — Flat-view-only affordances.

Notes

  • Adds @cosmograph/cosmos to pkg/tpviz/ui (bundle 1.16→~1.4 MB).
  • Angular color change takes effect after make build-ui + make embed-wasm-visor.
  • go build ./... OK; tpviz UI npm run build OK; validated live against real all-transports data (21k transports render, colored, interactive).

0pcom added 30 commits June 30, 2026 13:10
This official dmsg server was registered in discovery but missing from
services-config.json. Added to .prod.dmsg_servers (+ clearnet mirror + regen
data_static_js.go). Its port was moved 30081->30088 on the host so it no longer
shares port 30081 with 03f57e7c @ 172.235.168.146 (avoids confusion); verified
listening + externally reachable + re-registered at the new port.
…-lite control, skychat, usability audit (skycoin#3359)

* fix(wasm-hv): serve /api/network/transports so the visualizer draws edges

The Network Visualizer needs two sources: nodes from /api/network-view
(already served) and EDGES from /api/network/transports. The wasm core 404'd
the latter ('not found in wasm hypervisor core'), so the visualizer drew a
cloud of 1097 disconnected nodes — '1097 visors · 0 transports', no links,
nothing like the native graph.

Add SelfNetworkTransports(days): fetch the TPD's
/metrics?days=N&bandwidth=true&latency=true&edges=true over dmsg (the same
call the native HV proxies) and return the raw JSON array unchanged — the
visualizer parses edges[]/type/live itself. Wire it through the wasmhv.Self
interface + a /network/transports router case (days parsed from the raw
path, bounded 0..35 like the native handler).

* fix(hv-ui): multi-visor-resources fmtBytes typo + wasm /api/client-log accept

Batch of two issues found by sweeping every HV-UI page on the wasm visor:

- multi-visor-resources (#/nodes/resources): template called fmtBytes() but
  the component defines formatBytes() (the app-wide name is fmtBytes; this one
  component diverged). Fired 'TypeError: fmtBytes is not a function' ~16x per
  render. Fixed the two stray calls to formatBytes to match the method.

- wasm core 404'd POST /api/client-log — the hvui client-error-reporter ships
  console errors there, so every reported error 404'd (spamming console with
  its own failure). Accept + discard in the wasm router (in the browser the
  console already IS the visor log). Rebuilt UI + wasm blob.

* feat(wasm-hv): verbose request logging for skysocks-lite + resolving proxy

Phase 1 of skynet-browser proxy visibility. Neither the skysocks-client-lite
(fetchClearnet) nor the resolving proxy (fetchDmsg) logged anything, so you
couldn't see what the iframe browser was doing.

- skysocks-lite: always logs route+session establish/failure to an exit
  ([skysocks-lite] ... established (Nms)); per-request method/url/status/
  bytes/ms when verbose.
- resolving proxy: per-request host/path→resolved/status/bytes/ms when
  verbose ([resolve-proxy] ...); failures always logged.
- proxyVerbose global flag toggled via skywireVisor.proxyVerbose(bool) and a
  '🐞 verbose' button in each browser window's proxy (⚙) panel. Logs surface
  in the existing 'visor log' window (filter by prefix).

Global for now; per-window instances + per-window proxy-log pane + optimized
routing are the next phases.

* feat(wasm-hv): usability audit (hide unusable tabs) + skysocks-lite mux routing

Interface work for the flagship in-browser visor — show only what works in a
tab, and route the browsing proxy better.

Usability audit (gated on the node's arch==='wasm', so a REMOTE native visor
managed from the wasm hypervisor keeps everything):
- hide node-detail tabs that have no tab-runtime data source: resources (host
  CPU/mem/disk/net — a tab has none, rendered all-zeros), terminal (dmsgpty
  needs a native shell), wallet (current tab runs 'skywire skycoin daemon';
  the over-dmsg thin-client wallet is a separate future tab). bandwidth/
  uptime/rewards/web-proxy were already hidden.
- hide the VPN + Skysocks apps sub-tabs (TUN / skysocks-server can't run in a
  tab); Skychat (in-process) stays. These host-only features are the
  invitation to install the visor on the host.

skysocks-client-lite: request a 2-route mux (MuxRoutes=2) for the browsing
route — throughput + mid-browse resilience; policy hook clamps + degrades
gracefully to single-route.

* feat(wasm-hv): per-window skysocks-lite instances

Each iframe browser window now gets its OWN skysocks-client-lite session/route
to the exit instead of all windows sharing one per-exit session. So two windows
pointed at the same exit browse over independent routes (independently
loggable), while the resolving proxy (fetchDmsg) stays shared — matching the
'same resolving proxy, different skysocks-client-lite per window' design.

- skysocksSessions re-keyed by winId+exitPK; skysocksSession(winId, exit).
- fetchClearnet takes an optional 5th arg (winId); log lines carry it
  ([skysocks-lite w1] ...) so the shared log window distinguishes windows.
- skywireVisor.closeWindow(winId) tears down a window's sessions; browse.js
  generates a per-window winId and calls it from the window's onclose.
- Windows still default to the same configured exit, overridable per window
  (unchanged winUpstream).

* fix(hv-ui): wire skychat to the wasm in-process hooks (was stuck Disconnected)

The skychat component only spoke the native SSE proxy
(/api/visors/<pk>/skychat/proxy/sse + /message), which the wasm core doesn't
serve — so in the in-browser visor skychat sat 'Disconnected — retrying…' and
couldn't send. Feature #3 was broken in the flagship.

When the node is a wasm visor with the skywireVisor.skychat* hooks present:
- receive: poll skychatMessages() (JSON [{from,text,ts,out}]) on a 1.5s timer
  instead of the EventSource; mirror into the message list (out→'out'/'in').
- send: skychatSend(recipient, text); the sent message is buffered with
  out:true so the poll renders it. Force the network label to dmsg (dmsg:1).
- skip the native /history seed (no proxy in wasm; the poll seeds instead).
Native visors (incl. remote ones managed from the wasm HV) keep the SSE path.

* fix(hv-ui): skychat wasm poll — coalesce skychatMessages() null to []

skychatMessages() returns the JSON of the message buffer, which is the string
'null' (nil slice) when empty — not '[]'. JSON.parse('null') is null, so the
poll's Array.isArray guard bailed and connected never flipped true (skychat
sat 'Disconnected' even though the hook works). Coalesce to [].

* build(hv-ui): sync embedded pkg/visor/static with the session's UI changes

The HV UI is embedded from pkg/visor/static (//go:embed static in visor.go),
but this session's UI commits only captured static/skywire-manager-src/dist
(the raw build output). make build-ui updates both; pkg/visor/static was left
uncommitted, so the committed build lacked the UI changes even though the
locally-built ./skywire had them. This rebuilt bundle (main.87acfd6f) carries
the whole session: visualizer edges, fmtBytes/client-log, proxy verbose toggle,
usability tab-hiding, per-window skysocks-lite, and the skychat wasm hooks.

* fix(wasm-hv): skychat replies chat-ack so native send --wait resolves

The wasm in-process skychat surfaced incoming messages but never replied to
the chat-msg ack envelope native skychat's `send --wait` blocks on, so a
native peer messaging a browser visor saw a false 'not acked: timeout' even
though the message was delivered. readChatConn now decodes the ack id and
writes a chat-ack back on the same conn; a per-conn write mutex serializes
that reply against a concurrent outbound send on a cached conn.

Verified live: native -> wasm over dmsg now acks in ~81ms (was 8s timeout);
message still lands in the wasm buffer (out:false).

* fix(wasm-hv): resolving-proxy iframe handles <vhost>.<pk> hosts (name-based sites)

The in-tab resolving proxy (fetchDmsg) mis-parsed a name-based virtual-host
URL like http://bunkerofdoom.com.<pk>.dmsg/ — resolveFetchHost only stripped
the .dmsg/.skynet suffix and treated the whole remaining label as a public
key, so a <vhost>.<pk>.dmsg host failed with 'Invalid public key'. The native
socks5 resolving proxy parses these correctly via skynetweb.ParseResolverHost.

Wire the wasm resolver to the SAME shared parser so it extracts the dest PK
and the vhost identically (aliases + 66-hex AND 53-char base32 PK labels all
handled), and carry the vhost as the Host header: FetchOverDmsg now honors a
caller-supplied reqHeaders["Host"] (httpRoundTrip skips the host key when
emitting reqHeaders, so this is the only channel for it). https://<vhost>.<pk>.dmsg/
routes through the same path (browse.js dispatches by host suffix, not scheme;
dmsg already encrypts), so it resolves the same way.

Verified live: fetchDmsg("bunkerofdoom.com.<pk>.dmsg") → 200 with the site
HTML (was 'Invalid public key').

* fix(wasm-hv): iframe browser preserves the https scheme in the address bar

renderSite hardcoded 'http://' when echoing the navigated URL back into the
address bar, so an https://<site>.<pk>.dmsg/ entry displayed as http://. The
fetch is over dmsg either way (Noise-encrypted, no in-tab TLS), so the scheme
is cosmetic — but rewriting what the user typed is surprising. Thread the
scheme through browseTo → the history entry → renderSite; in-site link clicks
inherit the current site's scheme so navigation within an https site stays
https.

* fix(wasm-hv): allow fetched sites to run their own WebAssembly

A dmsg site that ships a wasm blob (e.g. TinyGo via wasm_exec.js +
WebAssembly.instantiateStreaming(fetch("/x.wasm"))) failed in the iframe
browser: the strict CSP allowed inline script but not wasm compilation, so
instantiateStreaming threw 'unsafe-eval is not an allowed source'. Add
'wasm-unsafe-eval' to script-src — it permits WASM compile/instantiate WITHOUT
enabling general eval(); WASM stays sandboxed and connect-src 'none' still
blocks egress. The site's runtime fetch('/x.wasm') already rides the injected
window.fetch shim (relayed over dmsg with the vhost), and the server sends
Content-Type application/wasm; add a .wasm→application/wasm mimeOf fallback for
servers that omit it (instantiateStreaming requires that exact type).

* perf(wasm-hv): lazy-load images instead of pre-inlining the whole catalog

renderSite inlined every same-site <img> as a base64 data: URL before showing
the page, so a media-heavy site (a product catalog) produced a tens-of-MB
srcdoc that stalled the render (magnetosphere.net: ~87MB). Defer images: rewrite
each to a data-dmsg-src with a transparent placeholder, and an injected
IntersectionObserver (reusing the existing dmsg fetch relay) pulls the real
bytes over dmsg only when the image scrolls near the viewport. A MutationObserver
covers images the page adds dynamically; srcset is dropped so the browser can't
load a non-rewritten (CSP-blocked) candidate. Scripts + <source> stay eager
(must exist before run / chosen at parse). Result on magnetosphere.net: srcdoc
87MB -> 1.8MB, page renders promptly, images fill in on scroll.

* feat(wasm-hv): proxy a fetched site's cross-origin fetch() through skysocks-lite

The injected window.fetch shim only relayed SAME-SITE requests over dmsg;
cross-origin (clearnet) fetches fell through to the real fetch and hit CSP
connect-src 'none'. Now a site's runtime fetch() to an external URL is routed
through the skysocks-lite upstream exit (IP-anonymous) when one is configured,
gated by the same clearnet policy as static sub-resources: block (default) →
403, direct → real fetch (CSP is off in direct mode), proxy → relayed to the
parent which fetchClearnets it. data:/blob: and non-http(s) schemes still use
the real fetch. Lets a dmsg-served site call an external JSON API without a
clearnet fallback path. (XHR is not shimmed; fetch only.)

* feat(wasm-hv): per-window request-log pane in the iframe browser proxy panel

The iframe browser's proxy panel crammed connection state into a one-line
ellipsized status ('clearnet: via skysocks 027087fe (anonymous)') with no room
to show it — forcing a trip to the main visor-log window to see what the
resolving proxy / skysocks-lite actually did. Replace it with a terminal-like,
scrollable per-window log pane: every request this window makes over the
resolving proxy (dmsg) or skysocks-lite (clearnet) is timestamped with
method/path/exit/status/bytes/ms, alongside upstream-set config events; a
'clear' button empties it. The engine's existing per-request log() calls now
also feed this pane (plus the shared visor log), and the relay handler logs the
site's own dmsg + clearnet sub-resource fetches. Verified live: example.com
loaded through the magnetosphere skysocks exit shows the full request trace.

* feat(wasm-hv): skysocks-lite connect log, stop button, 'skysocks proxy' label

Iterating on the iframe browser's ⚙ proxy panel:
- The wasm skysocks-lite path now pushes its own connect/route-setup lines
  ('connecting to exit … — setting up route…', 'route+session established (Xms)',
  dial-FAILED, stopped) into the originating browser window's log pane via a
  per-window hook (emitProxyLog → globalThis.__skywireProxyLog, dispatched by
  winId to the pane registered in browse.js). So the pane shows the connect step
  explicitly, like 'proxy start --verbose' surfaces session establishment.
- '■ stop' button releases this window's skysocks-lite route + session (calls
  closeWindow(winId); re-establishes on the next clearnet request).
- Renamed the label 'clearnet upstream proxy:' → 'skysocks proxy:'.

Verified live: setting magnetosphere as the exit shows the connecting/route-setup
line in the pane; label + stop button present.

* feat(wasm-hv): content-hosting tool window (multi-content, list, enable/disable)

Content hosting was a single path/file tucked into the iframe browser's 'host'
button. Make it a first-class capability:
- New wasm APIs (selfhost_js.go): hostedContent() lists every served entry
  ([{port,path,ct,size,enabled}]); unserveContent(path[,port]) removes one;
  setContentEnabled(path,enabled[,port]) disables a path (served 404) without
  discarding it. contentEntry gains an 'enabled' flag; the store already held a
  path→entry map per port, so multi-content was always supported.
- browse.js: a dedicated 'host' tool window (☰ menu, next to 'browser', wasm
  only) — add a text page OR upload a file OR a whole DIRECTORY (webkitdirectory,
  each file served at its relative path), plus a live list of what's hosted with
  per-path enable/disable, remove, and 'open in a browser window'. Removed the
  old per-browser-window host button/panel.

Verified live: served /hello + /page2.html + the default /, listed them,
disabled / (→ served 404), host button gone from the browser window.

* fix(wasm-hv): skywire.dmsg resolves to the tab's own visor (self-loopback alias)

The native resolving proxy seeds 'skywire' -> LocalPK (pkg/dmsgweb Aliases), so
http://skywire.dmsg serves the visor's own landing page. The wasm resolver built
only the deployment-service aliases (tpd/ar/rf/…), so skywire.dmsg had no mapping
and failed to resolve. initResolver now adds 'skywire' -> selfFetchPK, so
skywire.dmsg fetches the tab's own hosted '/' — matching native.

Verified live: fetchDmsg('skywire.dmsg','/') → 200 (self-hosted index); the iframe
browser renders http://skywire.dmsg/ with its linked page + inlined CSS, all
served from the tab over dmsg.

* feat(wasm-hv): skysocks-server dropdown from service discovery in the proxy panel

The proxy panel's exit was a bare PK field you had to know/paste. Add a '⌄ servers'
button that lazily fetches service-discovery type=proxy entries over dmsg
(fetchDmsg sd.dmsg /api/services?type=proxy) and populates a <select> with each
server's PK · country · version; picking one fills the PK field and sets it as
the exit. Lazy on click (no SD fetch for windows that never open the panel), logs
to the per-window pane. Verified live: 1057 skysocks servers listed with geo/version.
…ilds under js/wasm

The CXO/skyobject stack transitively imported bbolt (via cxds+idxdb on-disk
drives), which does not build for GOOS=js (arch-specific consts + mmap) — so the
CXO publisher could not run in a wasm visor even with InMemoryDB. Split the
bbolt-backed drive.go behind //go:build !js and add drive_js.go stubs providing
DriveOptions + the NewDrive* constructors (which error, since a wasm visor uses
the in-memory CXDS/IdxDB via skyobject's InMemoryDB path). Moved the shared
panicf helper out of drive.go into the untagged memory.go.

Result: pkg/cxo/treestore (the full publish path) now compiles under js/wasm;
native on-disk path unchanged (cxds/idxdb tests pass). Foundation for wasm-visor
in-memory CXO telemetry -> TPD.
…atency to TPD

The browser visor now runs a CXO TreeStore publisher (InMemoryDB — no filesystem)
that reports each transport's bandwidth + latency to TPD via its cxo-aggregator,
exactly like a native visor (init_stats.go) but without the bbolt stats.Store.
telemetry_js.go: builds the in-memory publisher, wires tpM.SetTPDLeafPublisher
(transports/<uuid>/entry + tombstone), samples transports/<uuid>/current
(liveSnapshot: sent/recv + latency min/max/avg) each minute, and announces to TPD
every 30s so it subscribes. Wired into bootEdge after the transport manager serves.

Also fix skyobject.NewContainer to skip the DataDir mkdir when InMemoryDB — the
default DataDir resolves to /tmp under js/wasm where 'mkdir not implemented on js'
was fatal; in-memory mode never touches those files anyway.

Runtime-verified: publisher comes up (feed=<self> → TPD), builds on native + js/wasm.
… stale deadcode nolint

Compliance pass against the Go coding standard's mechanical review engine
(gofmt / go vet / golangci-lint / native + js-wasm build):

- gofmt the wasm-visor JS-hooks map (alignment drift from telemetry wiring)
- pkg/dmsg/dmsg/util.go: //nolint:gosec G709 — gob frame arrives over a
  Noise-authenticated dmsg session, fixed internal decode target (false positive)
- usermanager cookies: //nolint:gosec G124 — Secure/HttpOnly/SameSite ARE set
  via s.c config accessors gosec's flow analysis can't see through (false positive)
- mux-probe-assert: drop stale ,deadcode from nolint (folded into unused upstream)

All pre-existing findings surfaced only by a newer-than-CI golangci-lint; CI
was already green. No behavioral change.
Routine dependency refresh — all minor/patch bumps, no major-version changes:
grpc 1.81.1→1.82.0, pion/webrtc v4.2.15→v4.2.16 (+sctp/rtp/rtcp/turn),
go-proxyproto 0.12.0→0.14.0, maxminddb v2.4.0→v2.4.1, starlark, genproto,
klauspost/cpuid, gopherjs, lufia/plan9stats, shoenig/go-m1cpu.

Verified: native build, GOOS=js GOARCH=wasm build (wasm-visor + CXO), and
go vet all clean.
…code table

- refresh docs/skywire-goda-graph.svg (goda graph | dot -Tsvg)
- append a Lines of Code section (gocloc, vendor/node_modules/.git excluded):
  1740 Go files / 267,754 LOC; ~403k LOC total across all languages
browse.js: add createChatWindow — a 1:1 skychat client as a WinBox desktop
window (the missing peer to the skynet browser), driving the existing
wasm-visor JS hooks skychatSend(peerPk,text)/skychatMessages(). Distinct
sender PKs from the message buffer surface as clickable chips so an incoming
message from an unknown peer is discoverable. Added to the app menu as 'chat',
gated on skywireVisor.skychatSend (wasm-visor only; native keeps its Angular
skychat tab), exactly like the host window. Rebuild ./skywire re-embeds it
(browse.js is go:embed'd in browseui, not in the wasm.gz blob).

docs/skychat-refactor-rfc.md: design for extracting a shared pkg/skychat core
(one message model + wire codec, federated-only groups, one Transport interface,
history behind a build-tag-split store) so native + wasm stop diverging — the
visorcore convergence pattern. Includes public-group service discovery via a
new servicedisc ServiceTypeSkychat='skychat' (same directory mechanism as
type=proxy), and causal-ish reorder-buffer display ordering.
… step 2)

The skychat length-prefixed frame protocol (4-byte big-endian length + payload,
64 KiB cap, chat-msg/chat-ack envelope) was implemented THREE times — the native
app (framedConn), the browser-tab wasm visor (chatConn), and the group relay
(session.go readFrame/writeFrame) — each with its own constant and write mutex,
free to drift. This collapses them into one codec: pkg/skychat/message.

New package pkg/skychat/message (pure Go, compiles under js/wasm):
- WriteFrame/ReadFrame (io.Writer/io.Reader), MaxFrameSize
- Conn: net.Conn + write mutex, WriteFrame/WriteFrameDeadline/ReadFrame
- Envelope (chat-msg/chat-ack) + TypeMsg/TypeAck + ParseEnvelope/Marshal
- message_test.go: wire round-trip, reject cases, Conn framing-compat, envelope parse

Consumers migrated (wire bytes unchanged):
- cmd/apps/skychat: framedConn is now a type alias for message.Conn; chatEnvelope/
  chatTypeMsg/chatTypeAck alias the message types; tryHandleChatEnvelope uses
  message.ParseEnvelope. Deleted the local frame methods + skychatMaxFrameSize.
- cmd/apps/skychat/group/session.go: relay framing uses message.ReadFrame/WriteFrame;
  deleted local readFrame/writeFrame + relayMaxFrameSize.
- cmd/wasm-visor/skychat_js.go: chatConn/readFrame/writeFrame/skychatMaxFrame/
  chatAckEnvelope replaced with message.Conn + message.Envelope. Blob regenerated.

Validated: message unit tests + full skychat suite (commands/group/pairing) pass;
native + js/wasm build/vet/lint clean; live harness — wasm visor boots, skychat
listens on dmsg:1, desktop chat window opens.
… (step 4)

Measured: a member joining a federated CXO group took ~30-40s to receive its
first message. Root cause: when two members start close together each fails the
other's INITIAL peer-sub dial (peer not listening yet), and the retry cadence
was slow — the in-visor Manager reconnect loop ticks every 30s, and the
standalone CXO-group driver re-attempted every 15s (20s timeout). A group.Session
does not self-retry a peer-sub whose first Connect failed, so first-message
latency was gated by whichever slow driver applied.

Fix (both drivers, same class):
- Manager.runReconnectLoop: adaptive cadence — tick at reconnectWarmupInterval
  (3s) while any peer is still 'warming' (never connected AND under its
  reconnectBackoffFailures1 budget), easing back to reconnectInterval (30s) once
  every peer is attached. New hasWarmingPeer() + peerReconnectFailures() gate it.
  Steady-state groups keep the low-churn 30s cadence; a dead peer stops
  qualifying after its budget and falls to the 5min/30min backoff, so it can't
  pin the loop fast.
- Standalone startCXOGroup driver: 15s->3s ticker, 20s->10s per-attempt timeout.
  It already skips attached peers, so a fast tick idles cheaply post-convergence.

Verified with two standalone CXO-TCP group processes (simultaneous start, the
hard case): member->owner first arrival 2s (was ~30-40s), owner->member 3s. Full
skychat suite + group tests pass; gofmt/vet/lint clean.
Prep for wasm-visor group chat. The group record store was bbolt-backed, and
bbolt can't compile under GOOS=js GOARCH=wasm (arch-const MaxAllocSize + mmap) —
so the group package (and thus group chat) couldn't build for the browser visor.

Split store.go the same way the CXO cxds/idxdb datastore was split:
- store_bbolt.go (//go:build !js): the existing bbolt impl, unchanged.
- store_memory.go (//go:build js): a wire-identical in-memory impl — same *Store
  type + method set, records held as JSON bytes keyed by group ID so value
  semantics (independent copies on read, legacy-Admins normalization, lexicographic
  List order) match bbolt exactly. Ephemeral (resets on tab reload; the federated
  network retains published messages). Designed so an IndexedDB-backed durable
  variant can hydrate/flush this map behind the identical API later.

The group package now compiles under js/wasm; native build + group tests unchanged.
No behavior change on native. Next: plumb InMemoryDB through group.Config and wire
group.Manager into the wasm visor.
…r wasm

Adds Config.InMemoryDB: when set, newPublisher runs the session's CXO tree fully
in memory over the dmsg transport (the native-TCP standalone path already forced
this internally). Relaxes the 'DataDir required' check when InMemoryDB is set.
This lets a browser (js/wasm) visor — which has no filesystem — be a full
federated group member: publish its own feed in memory, subscribe to peers over
dmsg. Native behavior unchanged (InMemoryDB defaults false). Builds native +
js/wasm; group tests pass.
…vity log pane

Two additions to the desktop skychat window (createChatWindow), matching the
browser window's controls:
- Transport selector (dmsg | skynet): skychatSend now takes an optional network
  arg; sendChat dials the chosen appnet.Type and caches conns per (network, peer)
  so the two transports don't clobber each other. Default dmsg (unchanged).
- Activity log pane (🐞): subscribes to the shared window.skywireLog ring
  (same source the 'logs' window reads), filtered to skychat lines, so the
  operator sees the dial/connect/send/receive steps — like the skysocks-lite
  route-setup log in the browser window. sendChat now vlog's those steps.

Verified live via hvinspect on the :8443 harness: selector has dmsg+skynet, log
pane shows 'skychat: dialing dmsg <pk>:1…' after a send. wasm blob regenerated.
docs/wasm-visor-ui-tour.md — a screenshot-driven tour of the browser-tab visor:
dashboard/visor-list, the WinBox mini-desktop app menu, and each window (skynet
browser, skychat with the dmsg/skynet transport selector + activity log pane,
host-content, console REPL, live logs). Captured headless via cmd/hvinspect
against a live 'skywire cli hv serve' harness; includes the regeneration recipe.
Images under docs/img/wasm-visor/.
The browser-tab visor is now a full federated group-chat member. group_js.go
starts a group.Manager over the wasm dmsg client with an in-memory store
(store_memory.go) + in-memory CXO tree (ManagerConfig.InMemoryDB) — no filesystem
needed. It publishes its own member feed and subscribes to peers' feeds over dmsg,
exactly like a native visor; local state is ephemeral (resets on tab reload; the
network retains what was published).

manager.go: plumb InMemoryDB through ManagerConfig → Manager → each opened
group.Config (relaxes the DataDir-required check when set). Native default false.

JS surface on skywireVisor: skychatGroupCreate(name[,mode]) → {id,name,invite},
skychatGroupJoin(inviteLink), skychatGroupSend(id,text), skychatGroupAddMember(
id,pk), skychatGroupList() (JSON), skychatGroupMessages([id]) (JSON). Inbound
group messages surface into an in-memory ring + the visor log.

Builds native + js/wasm; group tests + native full build + js vet + lint clean
(group_js.go lint-clean). Blob regenerated. Next: UI + wasm↔native demo.
…socks route setup

Two bugs found live during the wasm-visor demo.

1. WinBox z-order: makeWin gave every window the same fixed index (2147483000),
   so a newly-opened window stacked BEHIND the focused one and obstructed it
   (couldn't reach the covered window). Now each window gets an incrementing
   z-index (capped under the always-on taskbar) + an explicit wb.focus(), so a new
   window always opens in front.

2. skysocks-lite route setup held skysocksMu for the ENTIRE DialRoutes (up to 45s),
   serializing every concurrent session — a 2nd browser window dialing a different
   exit blocked ~45s behind the first, piling up goroutines (the pile-up that
   preceded the UI hang). The lock now only guards the session map: DialRoutes
   runs unlocked so different exits/windows establish in parallel; a same-key
   double-dial race is resolved by discarding the loser on re-publish.

Note: the residual 'whole UI freezes during a slow route setup under load' is
main-thread starvation (the visor wasm shares the browser's single main thread);
the real fix is migrating the visor into a Web Worker — a separate, larger item.

Builds js/wasm + vet clean; browse.js syntax ok; blob regenerated.
feat(wasm-hv): in-memory CXO telemetry — browser visor reports transport bandwidth+latency to TPD
…9bc0

chore(deployment): add dmsg server 02a49bc0 @ 143.42.59.213:30088 to services-config.json
…ore 3.2.1

Replicates dependabot PRs skycoin#3363, skycoin#3361, skycoin#3345 (transitive sigstore bumps in the
skywire-manager-src Angular UI lockfile) via npm update, so those PRs auto-close
on merge without cherry-picking their commits.
chore(deps): bump sigstore 4.1.1 / @sigstore/verify 3.1.1 / @sigstore/core 3.2.1
… the UI) (skycoin#3366)

The Go/wasm runtime — and the visor's occasionally-blocking work (dmsg/WS/WT
dials, route setup, SOCKS handshakes) — ran on the page's MAIN thread, so a slow
path starved the UI event loop: during the 2026-07-03 demo a slow skysocks route
setup froze the whole HV UI (unclickable, even CDP evals hung) until reload.

Move the runtime into a dedicated Web Worker (worker.js). hv-boot.js spawns it and
installs a main-thread globalThis.skywireVisor PROXY whose every method is a
postMessage round-trip to the worker. All call sites (Angular SkywireHttpBackend,
browse.js, the skychat component) already treat the API as async (Promises /
fire-and-forget), so the proxy is transparent — the UI now stays responsive no
matter what the visor is doing.

The Go side is UNCHANGED: syscall/js, fetch, WebSocket and WebTransport all work in
a dedicated worker, and self.location is same-origin as the page (so the HTTPS
mixed-content gating in bootEdge stays correct). The one worker-absent API,
RTCPeerConnection, is already Truthy()-guarded in Go (STUN self-IP + the WebRTC
transport degrade gracefully) — fine, since WS/WT are the browser carriers and
WebRTC is neither default-on nor the freeze cause.

Worker console output (all Go stdout + vlog) is forwarded to the page console so
the HV-UI log window still shows visor logs. Falls back to the in-page runtime if
Web Workers are unavailable or worker.js can't load (keeps the single-file
generator and exotic embeddings working).

Scope: the served model (hv serve / standalone-live / dev harness via hv-boot.js).
The single-file `hv gen` output keeps the in-page path (it can't load a separate
worker script). JS-only change — the committed wasm blob is unchanged.

Validated headless (hvinspect over hv serve): boots "in Web Worker", dmsg connects
+ WS transports dial from the worker, the dashboard renders its own PK via the
hvApi proxy, logs reach the page console, no clone/type errors.
…ly wired) (skycoin#3367)

The package header still described "Stage 2 scope … NO crypto wrapping yet … NO
router-side dispatch yet … no packet in the running system will reach it." All of
skycoin#2607 has since landed: per-datagram ChaCha20-Poly1305 + anti-replay (datagram_
crypto.go), router dispatch (router_packet.go handleDatagramPacket), the route-setup
sibling (datagram_setup.go), and consumers (visor forwarded_ports.udp / udp_bridge.go
and the app networker's DialPacketContext). Native unreliable carriage over QUIC
(RFC 9221), reliable-stream fallback elsewhere. The comment made a working feature
look half-built; correct it and note the real follow-ups (undriven auto-rekey needs
an on-wire epoch marker; single-peer/single-path).
…untime config, logs) (skycoin#3369)

The wasm HV core's selfRoute only handled a handful of subroutes, so several node-page
pieces errored on a browser visor:
  - Logs page → "self visor subroute not implemented in wasm core"
  - dmsg-sessions expander → "Failed to fetch dmsg sessions"
  - Router Settings + Runtime Configuration expanders → errors
  - the ports fetch → 404

Serve them from the tab's own state (SelfProvider gains SelfDmsgSessions /
SelfRouterSettings / SelfRuntimeConfig / SelfRuntimeLogs), matching the native
hypervisor's response shapes so the Angular UI renders instead of erroring:
  - dmsg/sessions  → {main:{pk,role,count,servers}} from dmsgC.AllSessions()
  - router-settings → {force_local_routes,existing_tp_only,mux_routes,min_hops} from the router
  - runtime-config  → a representative read-only config (a browser edge has no on-disk file)
  - runtime-logs    → an in-tab log ring buffer (vlog appends; served as the native
                      RuntimeLogsDelta {entries,latest,dropped} with ?since= cursor support)
  - ports           → [] (a browser edge hosts via serveContent, not forwarded_ports)

router.go threads the raw query string through visorRoute→selfRoute so runtime-logs
can read ?since=. Unit-tested (all five subroutes return 200; since cursor threaded).
Validated live via hvinspect over `hv serve`: the Logs page no longer errors and tails
the visor's own log lines. Embedded wasm blob regenerated (make embed-wasm-visor).

Remaining node-page items (separate follow-ups): the min-dmsg-sessions PUT control,
the empty apps page, the Angular-side title-bar/PK overlap + 13→6 tab-count flip, and
window-state persistence across restarts.
…route-death) (skycoin#3368)

* feat(skynet): hold+reuse multihop routes via a yamux skyfwd mux (fix route-death)

The skynet resolving proxy "only worked well for direct routes": over a multihop
route the routing rules expire and the whole route re-sets-up on every reconnect
(seconds). Root cause — a rule's 10-min TTL is refreshed only by an OPEN
RouteGroup's keepalive, and the native `.skynet` proxy dialed a FRESH route per
SOCKS5 connection with no caching (embedded_skynetweb.go), closing it when the
connection ended. Direct re-setup is instant; multihop isn't. skysocks-lite never
had this because it caches+yamux-muxes its route and holds it open.

Generalize that pattern:

- **pkg/skyroute.Pool** — holds ONE route group per destination PK and yamux-muxes
  logical connections over it, so the held route's keepalive keeps every hop warm
  and reconnects reuse it with zero setup. Idle groups are reclaimed after ~10 min
  (DefaultIdleTTL); a holder that's done (e.g. a closing iframe window) can Release
  one eagerly. Because the route lives on the visor, this needs no browser
  "page-open" signal — it fixes the external-browser SOCKS5 case and the in-tab
  iframe case identically.
- **skyfwd mux server** (skyenv.SkyForwardingMuxPort = 59) — one accepted route
  group carries a yamux session; each stream runs the SAME ready-byte + ClientMsg
  handshake + forward as the 1:1 SkyForwardingServerPort, reusing handleServerConn.
  The route group's peer PK is carried onto each stream (muxPeerConn) so the
  per-port PK whitelist still works. Additive + version-negotiated: a caller dials
  the mux port and falls back to the 1:1 port (ErrNoMux, negative-cached) against
  older visors.
- **routerSkynetDialer** dials through the pool for the route path (direct
  transport + explicit source routes are unchanged); a real route-setup failure is
  surfaced, only ErrNoMux falls through to the legacy 1:1 dial.

Unit-tested (pkg/skyroute): route-group reuse, ErrNoMux fallback + negative cache,
idle reap, eager Release. Full build + vet clean.

Design + the wider unified routing-control plan (skysocks-lite + skychat as the
other two consumers of RoutingPolicy, the iframe control surface, and a voice-chat
assessment) in docs/skynet-routing-control-rfc.md. This is Phase 1.

* test(skynet): integration test — pool reuse over the real mux forwarding server

Wires the REAL yamux mux forwarding server (serveSkyForwardingMuxSession) to the
REAL skyroute.Pool over one in-memory conn and asserts the core property: 5 logical
connections reuse ONE route group (dial count == 1), each reaching a registered
service through the real handleServerConn ready-byte + ClientMsg handshake +
dispatch. A RouteGroup already satisfies net.Conn and runs under yamux in production
(skysocks-lite), so this covers the mux-server↔pool integration the skyroute unit
tests (which mock the far end) don't — without standing up the full transport/
route-setup stack. Also asserts muxPeerConn carries the route group's peer PK
(keeps the per-port whitelist working over the muxed path).

* test(skynet): satisfy errcheck (nolint on best-effort Close/Copy in tests)

* test(skynet): gofmt
…ction spam, no IP) (skycoin#3370)

* fix(wasm-hv): skip WebRTC autoconnect when RTCPeerConnection is absent (Web Worker)

The worker migration (skycoin#3366) moved the wasm runtime into a Web Worker, where
RTCPeerConnection does not exist. The autoconnect WebRTC pass still attempted dials
there, failing every one with "syscall/js: call of Value.Get on undefined" and
spamming the visor log each cycle. Gate the WebRTC pass on webrtcAvailable() (checks
RTCPeerConnection) — skip it in the worker (log once) and keep the direct WT/WS
carriers, which work off-thread. Peers aren't cooled down when skipped, so they stay
eligible for a direct carrier. Restoring WebRTC from the worker needs a main-thread
PeerConnection proxy (follow-up).

* chore(wasm-hv): regenerate embedded blob (clean tree — no +dirty, includes WebRTC gate)

* fix(wasm-hv): learn public IP via a main-thread STUN bridge (worker mode)

STUN IP discovery needs RTCPeerConnection, absent in the Web Worker where the wasm
runtime now runs — so on an HTTPS page (where dmsg LookupIP is masked behind the wss
reverse-proxy) a worker-hosted wasm visor never learned its public IP. Add a bridge:
the worker's refreshSelfPublicIPViaSTUN calls self.__skywireStunIP (worker.js), which
postMessages the STUN request to the main thread; hv-boot.js runs the RTCPeerConnection
ICE gathering there and returns the srflx IP. awaitJSString bridges the JS Promise to
Go. Falls back to the direct RTCPeerConnection path in the in-page (non-worker) mode.

* chore(wasm-hv): regenerate embedded blob (STUN bridge)
…roxy (skycoin#3371)

* feat(wasm-hv): WebRTC transport via a main-thread RTCPeerConnection proxy

The worker migration (skycoin#3366) put the wasm runtime in a Web Worker, where
RTCPeerConnection is absent, so the WebRTC transport (pion's js backend looks up
window.RTCPeerConnection) couldn't run there — WebRTC autoconnect was gated off.

Restore it with a main-thread PeerConnection PROXY:
- pkg/transport/network: retag so the hand-rolled webrtc_browser.go carrier (not
  pion's webrtc_native.go) compiles for ALL js/wasm builds; native keeps pion.
  newPeerConnection prefers globalThis.__skywireRTC.newPC (the proxy) when present.
- worker.js installs __skywireRTC: newPC returns a proxy RTCPeerConnection/DataChannel
  whose methods + events (createOffer/Answer, set{Local,Remote}Description,
  addIceCandidate, createDataChannel, send/close, onicecandidate/ondatachannel/
  onopen/onmessage/onclose/onerror) postMessage to the main thread.
- hv-boot.js owns the real RTCPeerConnection/RTCDataChannel per pcId, applies the
  ops, and forwards ICE candidates + datachannel + message/open/close events back
  (DataChannel payloads transferred as ArrayBuffer). Signaling still rides dmsg in
  the worker; only the PeerConnection/DataChannel live on main.
- webrtcAvailable() now also returns true when __skywireRTC is present, so the
  autoconnect WebRTC pass runs again in worker mode.

Go WebRTC carrier logic (offer/answer/ICE trickle, DataChannel-as-net.Conn with
Noise+yamux on top) is UNCHANGED — only newPeerConnection routes through the proxy.

* chore(wasm-hv): regenerate embedded blob (WebRTC proxy)
…ds) (skycoin#3372)

* fix(hv-ui): apply the wasm-visor tab filter after node data loads (13→6 flip)

The per-node tab row is built by updateTabBar() on navigation, but the wasm-visor
filter that drops host-only tabs (bandwidth/uptime/rewards/web-proxy/resources/
terminal/wallet) is gated on this.node.arch — which isn't loaded yet on the first
navigation. So a wasm visor's /info showed all 13 tabs until you navigated (e.g. to
/routing), where the node was loaded and the row correctly dropped to 6. Re-run
updateTabBar() right after this.node is assigned so the filter applies immediately.

* chore(hv-ui): rebuild Angular bundle (tab-count fix)
…ent (PK line) (skycoin#3373)

The always-on-top desktop taskbar (#skywire-skynet-taskbar, position:fixed, z
2147483646) painted over the top ~BARH px of the underlying HV-UI — most visibly
the node-info page's first line, the visor public key. applyDock() set the WinBox
window bounds (barTop/barBottom) but never offset the normal-flow HV-UI content.
Pad the body by BARH on the docked edge so the page clears the bar (WinBox windows
are fixed-positioned and unaffected). Validated live: the /info PK line moved from
y=20 (top 16px under the bar) to y=56 (fully below the 0-36 bar).
0pcom added 24 commits July 4, 2026 07:15
…e-probed each flush (skycoin#3374)

DeleteTransports tries /transports/delete-batch first and falls back to sequential
per-ID deletes on 404. Against a TPD that predates the batch endpoint, EVERY flush
re-probed /delete-batch (a wasted round-trip that 404s again) before falling back —
and the sequential deletes then hit the TPD's 30-req/min rate limit (429s). Cache
the 404 result (atomic bool, logged once at Warn) so subsequent flushes skip the
doomed probe and go straight to sequential. The real fix for the 429s is updating
the deployed TPD to the build that serves /transports/delete-batch (present on
develop); this just stops the client wasting requests against an old one.
…XO is wired (skycoin#3375)

The transport manager dual-wrote deletes: an HTTP DeleteTransports (authoritative)
plus a CXO tombstone (best-effort mirror). But the CXO deregister path is complete
end to end — the visor publishes transports/<id>/tombstone on its TreeStore feed
(publishTPDTombstones) and TPD's cxoaggregator dispatches it to
DeregisterTransportFromCXO → store.DeregisterTransport. So the HTTP delete is
redundant, and against a deployed TPD without /transports/delete-batch it forces the
per-ID sequential fallback that hits the 30-req/min rate limit (429 spam) and
diverges the local↔TPD transport views for no benefit.

Flip deregister to CXO-authoritative: when the CXO leaf publisher is wired, publish
the tombstone and SKIP the HTTP delete. HTTP delete stays as the fallback only when
there is no CXO publisher (a TPD/visor pairing without CXO). Register still
dual-writes for now — a separate follow-up.
…is wired (skycoin#3376)

Symmetry with the deregister flip (skycoin#3375): when the CXO leaf publisher is wired,
reRegisterTransports publishes the entry leaves on the TreeStore feed (TPD ingests
them via RegisterTransportFromCXO) and skips the HTTP RegisterTransportsV3. HTTP
re-register stays as the fallback only when there is no CXO publisher. Together with
skycoin#3375 the visor is now CXO-only for transport CRUD when CXO is available — no HTTP
register/delete traffic to TPD.

Keeps the per-transport delta model (entry + tombstone leaves) for now; the cleaner
unification (single snapshot list leaf; TPD reconciles, absence = deletion) is noted
as a follow-up.
…PD reconciles (skycoin#3377)

Replaces the per-transport delta model (transports/<id>/entry register +
transports/<id>/tombstone deregister leaves, skycoin#3375/skycoin#3376) with a single declarative
snapshot, per the operator's design: the visor publishes its FULL transport list and
TPD reconciles — absence = deletion. One path instead of two, and self-healing (a
dropped update or unqueued delete is corrected by the next snapshot; the delta model
relied on every removal reliably emitting a tombstone). Matches how HTTP
RegisterTransportsV3 already worked (full list, reconcile).

Visor (pkg/transport/manager.go):
  - publishTPDList(entries) publishes one `transports/list` leaf ({version, entries}).
  - currentBareEntries() = live non-self-loop entries (excludes just-closed ones).
  - reRegisterTransports + flushDeletionQueue now publish the snapshot when the CXO
    publisher is wired (deletions = re-publish the shrunken list, no tombstone).
  - Removed the now-dead publishTPDEntries / publishTPDTombstones delta publishers.

TPD (pkg/transport-discovery):
  - cxoaggregator: transportListLeaf wire type + dispatch of `transports/list` to a
    new Sink.ReconcileTransportsFromCXO.
  - api.ReconcileTransportsFromCXO: register/refresh every entry the reporter edges,
    deregister any of the reporter's existing transports absent from the list, mirror
    edges + heartbeat.
  - aggregatorSink adapter + recordingSink test stub wired; dispatch test added.

The per-transport RegisterTransportFromCXO / DeregisterTransportFromCXO handlers stay
for backward-compat with visors still publishing delta leaves. Build + lint + tests
green.
…utdown 429s) (skycoin#3378)

Manager.Close() batch-deregistered every transport from TPD over HTTP
DeleteTransports on shutdown. Against a TPD without the batch-delete
endpoint that degrades to sequential per-transport deletes and trips the
429 rate limiter — the shutdown 429 storm seen when rolling a visor with
~90 transports.

Flip it to the CXO-authoritative path used everywhere else: when a CXO
transport-list publisher is wired, publish an empty snapshot so TPD
reconciles all of our transports away (absence = deletion). Best-effort
at process exit; stragglers age out on TPD's heartbeat timeout, same as a
crash. HTTP batch delete stays only as the no-CXO fallback.
…kycoin#3379)

Node pages replaced the whole hypervisor nav with the per-node tabs, so
moving between visors meant backing out to the list each time. Add two
persistent rows to the top bar on node pages (large screens):

- home-nav row: the top-level hypervisor tabs (visor list / rewards /
  resources / …) stay visible, minus the now-redundant standalone
  'local visor' tab, active tab by URL match.
- visor-switcher row: one horizontally-scrollable chip per visor in the
  hypervisor's list (first = local), current visor highlighted; jump
  straight to any visor's info page without returning to the list.

top-bar gains optional homeTabsData / switcherTabsData inputs (opt-in, so
other pages are unaffected). node.component builds the rows in
refreshNavRows(): home tabs from home-tabs util, switcher chips from a
single throttled (~10s) getNodes() call. Works on native + wasm HV UIs.
Rebuilt embedded bundle.
…ts (skycoin#3380)

CI (.github/workflows/test.yml) pinned golangci-lint-action to v2.11.4,
one patch behind the current release (v2.12.2, which is what runs
locally). Bump the pin to v2.12.2 in all three jobs so CI and local lint
with the same, latest version.

Also modernize the Makefile install helpers, which lagged on golangci-lint
v1: install-linters ran the install script pinned to v1.64.5 and
install-linters-windows used the old (v1) module path. Both now
'go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest',
matching the primary 'check' target — so 'make install-linters' no longer
downgrades a dev to v1.
…in#3381)

Run 'make format' (goimports + goimports-reviser) across the tree. These
files had drifted from the repo's canonical import grouping — local
project imports (github.com/skycoin/skywire/...) split into their own
block after third-party. Import reordering only; no logic changes,
go.mod/go.sum/vendor untouched. Builds clean on the default and js/wasm
targets.
…on (skycoin#3382)

Windows users reported the MSI for a new version not updating their
config on upgrade. Root cause: Product.wxs scheduled
<RemoveExistingProducts> but never populated the Upgrade table (no
<Upgrade>/<MajorUpgrade>, so no FindRelatedProducts). With nothing to act
on, the prior product was never detected or removed on upgrade; the shared
ApplicationFiles component (fixed GUID) was then treated as already
installed, so the new skywire.exe / skywire-autoconfig.bat were skipped
and the deferred postinstall CustomAction ran the OLD binary — config
never updated. Upgrade-only, matching the report.

Replace the bare RemoveExistingProducts with a proper <MajorUpgrade>
(afterInstallValidate — same scheduling point): it populates the Upgrade
table for our UpgradeCode, detects and removes any prior version before
the new files install, so the postinstall runs against the new binary.
AllowSameVersionUpgrades handles same-3-field reinstalls/repairs (Windows
Installer ignores the 4th version field). DowngradeErrorMessage blocks
installing over a newer build.
…kycoin#3383)

The switcher rows (skycoin#3379) only appeared on node detail pages, so from the
visor list — where the operator usually lands — there was no visible
change and no quick way to jump between visors.

Add the visor-switcher row to the list page's top bar: node-list builds
the same switcher chips (one per visor, same icons/labels as the node
page) from its already-loaded allNodes, bound via the top-bar's existing
switcherTabsData input. No chip is 'current' on the list page, so all
stay clickable → jump straight to any visor's info page. Rebuilt bundle.
…ep Local Visor tab (skycoin#3384)

The visor-switcher and home-nav rows render OUTSIDE .main-container, so
they never inherited the '.tab-button { color: $white }' rule scoped
there — the chips fell back to Material's default near-black text on the
dark top bar and were effectively invisible (present + laid out in the
DOM, but ~2.9:1 contrast). Restate the legible tab-button styling on
.secondary-nav-row (white text/icons, filled 'current' chip).

Also stop filtering the 'Local Visor' tab out of the node-page home-nav
row, per request — keep it alongside 'Visor list' everywhere. Rebuilt
bundle.
skycoin#3385)

Add a home button (⌂ → home.dmsg) to the skynet browser nav bar next to
back/forward/reload, and an ⓘ button that toggles an about/limitations
panel explaining WHY the browser behaves as it does: pages run in a
sandboxed opaque-origin frame, so cookies/localStorage/logins do not
persist and sites are isolated from each other and from the visor's keys.
This turns 'my login didn't stick' from a apparent bug into documented,
by-design behaviour.

docs/skynet-browser.md captures the full rationale plus the native
per-site-origin design (wildcard *.skynet.localhost reverse proxy, or
routing the webview through the resolving SOCKS5 proxy) that would lift
the storage limitation on desktop — not possible in a keyless browser tab.
…ps (skycoin#3386)

Per feedback, the visor-switcher row now renders BELOW the home nav (was
above it on the visor-list page). Each chip is two lines: the node's
label — or its abbreviated PK (first5...last5) when unlabeled — over
public-ip / lan-ip (both when known and distinct, else whichever is set).

Extract the chip building into a shared utils/visor-switcher helper so the
list and node-detail pages stay identical; add an optional 'sublabel' to
TabButtonData for the second line; style .switcher-chip as a two-line chip.
Rebuilt bundle.
…ont) (skycoin#3387)

Labels auto-populate (often to the IP), so the IP line was redundant.
Chip line 2 is now the full public key at a slightly smaller 9px monospace
font; line 1 stays the label (or abbreviated PK if somehow unset). Rebuilt
bundle.
…ad standalone UT) (skycoin#3388)

The transport-graph rendered only the geographic group circles and dropped
~1000 of ~1011 visors: with the standalone uptime tracker decommissioned,
/api/uptimes served an empty set, so every visor was 'unknown' status and
hidden by the show-unknown filter.

Point the uptime feed at the TPD's own /uptimes?v=v2 endpoint — the same
TPD (over dmsg) the graph already fetches transports from. Its
[]VisorSummary ({pk,on,version,daily}) response is exactly the UI's
UptimeEntry shape, so no remapping is needed. An operator-supplied UTURL
still overrides for back-compat; a TPD without the endpoint (older deploy)
degrades to an empty set instead of erroring. refreshCache keeps the
uptime cache warm from the same source.

Backend-only (no bundle rebuild). Takes effect once the reward server is
rebuilt and the prod TPD exposes+populates /uptimes.
…eeds (skycoin#3389)

Extract the visor's intermittent CXO subscription manager
(pkg/visor/cxo_subscription_manager.go, ~800 lines) into a new lightweight,
reusable package pkg/cxo/cxosub, decoupled from *Visor via an injected
Deps{Dmsg, FeedSpec, Log}. pkg/visor keeps working unchanged via type
aliases + a thin cxoFeedSpec shim; its netviz behavior and tests are
preserved.

Wire the STANDALONE reward server (tp-viz serving theskywirenetwork.net
over dmsg) to the same intermittent subscriber instead of HTTP-over-dmsg
polling: tpviz.SetCXOSubMgrFromDmsg builds a cxosub.Manager over the
server's dmsg client, resolving TPD/SD/DMSG-D feed publishers from the
config's dmsg:// service URLs (cxosub.FeedRoute centralizes the feed →
port/prefix mapping). handleUptimes + handleTransports try CXO first
(tryCXOUptimes/tryCXOTransports) and fall back to the HTTP-over-dmsg path
on a cache miss; services + dmsg-clients already had CXO walks that now
activate.

Also make the first-Root wait per-feed (cxosub.FeedFirstSyncTimeout): the
all-transports snapshot (~10MB, the most important feed for the graph)
intermittently exceeded the flat 10s bound and failed to sync over CXO;
it now gets 45s, other feeds keep 10s. Threaded through both the cycle and
the RPC fetch/refresh default.

Fixes the transport-graph showing ~9 of ~1011 visors (empty /api/uptimes
-> all 'unknown' -> hidden) via the correct mechanism: the TPD-integrated
feeds over CXO, intermittent subscribe->snapshot->unsubscribe (self-heals
across TPD restarts, no persistent subscriber state). Verified live: TPD
uptime feed publishes 1084 visors / 857 online. Supersedes skycoin#3388's
HTTP-over-dmsg patch (kept as fallback). Design: docs/tpviz-cxo-subscriber.md.
…URL (skycoin#3390)

Add a deep-link handler so the wasm visor (and native HV, same browse.js)
can be pointed straight at a dmsg site full-page from a shareable URL:
?skynet=<target>[&kiosk=1] (e.g. rewards.dmsg, or a raw <pk>.dmsg).

hv-boot.js — the first script — captures the query param into
window.__SKYWIRE_DEEPLINK__ before Angular's hash router boots and drops
location.search. browse.js mountPanel then opens the skynet browser to
<target> once the visor has a live dmsg session (whenVisorConnected), and
in kiosk mode hides the taskbar + maximizes the window so the site
obscures the HV UI.

This lets a clearnet Caddy redirect land a visitor straight in a dmsg
site — e.g. theskywirenetwork.net -> skywire.theskywirenetwork.net/?skynet=
<reward-pk>.dmsg&kiosk=1 — deprecating clearnet serving while keeping the
site reachable via the in-browser visor. Validated live: reward UI fetched
over dmsg (200, 341KB) and rendered full-page in kiosk.

Follow-ups: the 'rewards' resolver alias isn't wired into the wasm visor's
fetch path (raw PK works); Caddy splash-redirect + htmpl.go dead-link
triage are the deployment layer.
…ses (skycoin#3391)

The in-tab resolver (cmd/wasm-visor/resolve_js.go) was missing the reward
system, so browsing rewards.dmsg failed with 'Invalid public key' (the
deep-link gateway's target). Thread RewardSystem[Dmsg] through
visorcore.Services (from the deployment default / v1 override) and add the
'rewards' alias — mirroring the native resolving proxy's serviceAliasMap,
which already had it.

Also retire the dead clearnet-service links in the reward UI nav
(htmpl.go): the services/dmsg dropdowns hard-coded deployment.Prod.*
clearnet URLs (dmsg-only now → dead). Point them at the dmsg resolver
aliases (http://tpd.dmsg/all-transports, http://ut.dmsg/uptimes?v=v2,
http://dmsgd.dmsg/dmsg-discovery/*, http://ar.dmsg/) which resolve when the
reward UI is viewed through the wasm visor. External links (apt repo,
blog, telegram — all live) kept.

Source-only commit; wasm blob regen follows so the deployed wasm visor
resolves rewards.dmsg.
Add a dependency-free guided tour (spotlight + callout, Back/Next/Skip) to
browse.js mountPanel — reopenable from the Apps (☰) menu → tour, and offered
once on first run. Seven steps framed around WHAT this is (a full Skywire
visor running in a browser tab, talking peer-to-peer over dmsg with no
server, account, or IP handed out) — since it's not obvious to a first-time
visitor how unusual that is. Steps spotlight the apps menu, the live network
nav, the visor switcher, the skynet desktop/browser, and the visor identity.

pickTarget selects the first VISIBLE, non-tiny match so a hidden/loading
duplicate (e.g. a transient app-top-bar) isn't spotlighted. Works on wasm +
native HV (shared browse.js); absent-target steps skip. Validated live.
Rebuilds via go build (browse.js is embedded, not the wasm blob).
…ycoin#3393)

A crawlable splash (deployment artifact for the reward host's Caddy webroot,
not served by the reward server) that hands off theskywirenetwork.net to the
wasm visor deep-link (?skynet=rewards.dmsg&kiosk=1), which reaches the reward
UI over dmsg. Splash rather than a hard 302 to preserve SEO (crawlers can't
run the wasm visor) and give a loading UX for the ~10s wasm boot. Includes an
example Caddyfile. Completes the deep-link gateway plan (repo side).
…dges

The transport-graph UI only knew stcpr/sudph/dmsg. The fleet now runs
squicr (QUIC), swtr (WebTransport), swsr (WebSocket) and webrtc transports;
these fell through to the gray '#888888' fallback with no legend entry and
no filter toggle, so the graph read as near-monochrome and the new types
were invisible/unfilterable. Live TPD data is now ~54% sudph/stcpr, ~14%
webrtc, plus squicr/swtr.

- constants.ts: add squicr/swtr/swsr/webrtc to the edge color map
- index.html: legend swatches + CSS classes + Show-QUIC/WebTransport/
  WebSocket/WebRTC filter checkboxes
- filters.ts + globe.ts: honor the new per-type filters (flat + globe views)
- events.ts: wire the new checkboxes to applyFilters/globe refresh

Also switch the flat graph to straight edges (smooth:false). With ~20k
transports, 'continuous' smooth edges are re-tessellated every redraw and
were a major main-thread cost; straight lines render far cheaper and match
the Angular network-visualizer, which stays smooth at this scale.
…ualizer

Mirror the tpviz color additions in the wasm/native HV network-visualizer:
squicr (QUIC), swtr (WebTransport), swsr (WebSocket) and webrtc edges were
falling through to the gray default. Colors match the tpviz palette so the two
graphs read consistently.

Note: takes effect after `make build-ui` + `make embed-wasm-visor`.
…nsports set

The vis-network Flat view (Canvas2D) stabilizes its force layout on the main
thread and re-tessellates every edge per redraw; at the full all-transports
scale (~20k edges) that froze the UI for seconds — the "hang". Add a third view,
"WebGL", backed by @cosmograph/cosmos, which runs BOTH the force simulation and
rendering on the GPU. It draws the entire routable topology (all-transports —
the set the route finder actually builds routes from, not just the QoS-reporting
/metrics subset) without blocking the main thread.

- src/cosmos-graph.ts: cosmos renderer reading the same S.nodesDataset /
  S.edgesDataset the other views use, so it can't drift. Type-colored links
  (incl. squicr/swtr/swsr/webrtc), status-colored nodes, local-visor emphasis,
  isolated-node pruning, honors the per-type/per-status filters, click → node
  info, hover → tooltip (cosmos renders points only — no text labels — so node
  detail lives in a hover panel). Runs the sim a few seconds then pauses + fits
  to free the GPU.
- index.html: WebGL view button + cosmos container/canvas + CSS.
- events.ts / graph.ts: wire the view toggle, filter refresh, and data-refresh
  hooks alongside the existing Flat/Globe paths.

The Flat (vis-network) and Globe (three.js) views are unchanged. This is a first
iteration — it does not yet replicate every Flat-view affordance (always-on
labels are infeasible at this node count with a points renderer; clustering /
group boundaries / satellite orbits / TPS pick-modes are not ported).
…ault

Bring the WebGL (cosmos) view up to the Flat view's interactions and make it
the default, since at the full all-transports scale the vis-network Flat view
hangs while the WebGL view stays responsive.

- Default view: WebGL. Flat/Globe remain switchable; the flat datasets are
  still built so the WebGL view reads them and the user can switch.
- Shared pick-mode click (node-click.ts): clicking a node in either renderer
  honors the armed TPS/group/local-transport/multihop/dmsg-health/ping pick
  modes (fills the field), else opens the node-info panel.
- Hover: neighborhood highlight (selectNodeById, greys out the rest) + the
  detail tooltip — the WebGL analogue of the flat view's hover behavior.
- Fit / zoom+ / zoom- / zoom-fit buttons route to cosmos when it's active.
- focusNode (search, local-transport list) and the sidebar visor list route to
  cosmos zoomToNodeById + select when active.
- Layout tuned tighter (gravity/linkDistance) + fitView padding so the settled
  graph frames the viewport instead of a small central cluster.

Not ported (documented follow-ups): always-on labels (a points renderer can't
draw ~1k labels), clustering / country-grouping / group boundaries / satellite
orbits — those remain Flat-view-only affordances.
@0pcom

0pcom commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

Re-targeting to upstream skycoin/skywire:develop.

@0pcom 0pcom closed this Jul 5, 2026
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