Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
13b38e0
hvui: fix invisible buttons on Transports tab controls
0pcom May 3, 2026
7a7ce88
hvui: Transports tab — fix tree icon, add edges show/hide toggle
0pcom May 3, 2026
6954626
hvui: Resources tab — stable row order, link-row table style
0pcom May 3, 2026
062d2cc
hvui: Transports tab — colorize + filter offline transports
0pcom May 3, 2026
a3c145a
visor+hvui: editable runtime config with validation
0pcom May 3, 2026
756afee
hvui: fix runtime-config edit-btn contrast, share form helpers,
0pcom May 3, 2026
1bd782b
visor+hvui: per-visor Bandwidth tab from local stats store
0pcom May 3, 2026
04dc4a3
hvui: Terminal / Web Proxy / Logs as own tabs
0pcom May 3, 2026
2134787
hvui: move Skychat tab next to Apps
0pcom May 3, 2026
47bfda0
hvui: skychat + skysocks-client dialogs match `skywire app` flag set
0pcom May 4, 2026
de63d90
cli: promote `got` to top level, fold `skynet curl` into it
0pcom May 4, 2026
2859be4
cli/proxy: --min-hops on `proxy start` for session-scoped routing floor
0pcom May 4, 2026
5fe3871
visor/logserver: expose dmsgpty UI at /pty, gated by dmsgpty whitelist
0pcom May 4, 2026
eecb570
visor: LocalUptimeStats RPC — tier bitmaps over a window
0pcom May 4, 2026
01c7e72
hvui/terminal: stop reloading the iframe on every node refresh
0pcom May 4, 2026
88aaef3
hvui: Uptime tabs (per-visor + network), backed by local bbolt store
0pcom May 4, 2026
a2e71e2
hvui/uptime: per-visor view groups by day with adjacent tier rows
0pcom May 4, 2026
416b443
visor+tpd: Network Uptime sourced from TPD's CXO uptime publisher
0pcom May 4, 2026
a2838ed
hvui/uptime: network view matches \`cli ut tpd graph\` shape
0pcom May 4, 2026
28f5010
hvui/uptime: connected-only by default + today-% colour badge
0pcom May 4, 2026
83b3a21
cli/visor info: 24h tier-uptime block bars from local bbolt store
0pcom May 4, 2026
056c519
cliuptime/graph: --shuffle flag for visual-pattern verification
0pcom May 4, 2026
5eb7203
visor/stats: clarify skynet tier as ≥2-transport (TPD's criterion)
0pcom May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions cmd/skywire-cli/cliuptime/cliuptime.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"math/rand"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -170,6 +171,8 @@ func newGraphCmd(cfg Config) *cobra.Command {
cacheDir string
cacheAge int
dr dateRange
shuffle bool
shuffleSeed int64
)
cmd := &cobra.Command{
Use: "graph",
Expand All @@ -191,17 +194,35 @@ and --per-day modes; --hours ignores them.`,
fatal(c, err)
}
filtered := applyFilters(entries, onlineOnly, pkFilter, versionEq, minVersion, 0)
// --shuffle: render in random row order. Useful for the
// "is the visual structure I'm seeing PK-correlated?" test
// — if banding patterns travel with the rows under shuffle,
// they're real visor-uptime patterns; if they break apart,
// the eye was just chunking runs in a high-density set.
if shuffle && len(filtered) > 1 {
seed := shuffleSeed
if seed == 0 {
seed = time.Now().UnixNano()
}
rng := rand.New(rand.NewSource(seed)) //nolint:gosec // not a security boundary
rng.Shuffle(len(filtered), func(i, j int) {
filtered[i], filtered[j] = filtered[j], filtered[i]
})
if verbose {
fmt.Fprintf(c.ErrOrStderr(), "# shuffled order, seed=%d\n", seed) //nolint:errcheck,gosec
}
}
if jsonOut {
_ = json.NewEncoder(os.Stdout).Encode(filtered) //nolint:errcheck,gosec
return
}
switch {
case hoursBack > 0:
printRollingTimelines(filtered, hoursBack, verbose)
printRollingTimelines(filtered, hoursBack, verbose, shuffle)
case perDay:
printTimelines(filtered, dr, verbose)
printTimelines(filtered, dr, verbose, shuffle)
default:
printSingleLineTimelines(filtered, dr, verbose)
printSingleLineTimelines(filtered, dr, verbose, shuffle)
}
},
}
Expand All @@ -223,6 +244,8 @@ and --per-day modes; --hours ignores them.`,
cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Second, "HTTP timeout")
cmd.Flags().StringVar(&cacheDir, "cache-dir", defaultCacheDir(cfg.DefaultURL), "cache directory (\"\" disables cache)")
cmd.Flags().IntVarP(&cacheAge, "cache-age", "m", 5, "re-fetch if cache is older than N minutes (0 disables)")
cmd.Flags().BoolVar(&shuffle, "shuffle", false, "render rows in random order (test: do visual banding patterns travel with the rows or with PK-sort?)")
cmd.Flags().Int64Var(&shuffleSeed, "shuffle-seed", 0, "seed for --shuffle; 0 = time-based (different every run)")
clirpc.RegisterFetchFlags(cmd)
// Graph's default date range differs from table's: the user
// typically wants the full available history when drawing a
Expand Down Expand Up @@ -477,7 +500,7 @@ func printVersions(entries []uptimestats.VisorSummary) {
// printTimelines renders v3 bitmaps with one row per day per visor
// (aka --per-day). Verbose prepends a per-visor header line with
// version + state; non-verbose just prints date + blocks + pct.
func printTimelines(entries []uptimestats.VisorSummary, dr dateRange, verbose bool) {
func printTimelines(entries []uptimestats.VisorSummary, dr dateRange, verbose, preserveOrder bool) {
if len(entries) == 0 {
return
}
Expand All @@ -486,7 +509,10 @@ func printTimelines(entries []uptimestats.VisorSummary, dr dateRange, verbose bo
for _, d := range dates {
dateSet[d] = struct{}{}
}
sorted := sortByPK(entries)
sorted := entries
if !preserveOrder {
sorted = sortByPK(entries)
}

first := true
for _, e := range sorted {
Expand Down Expand Up @@ -532,15 +558,18 @@ func printTimelines(entries []uptimestats.VisorSummary, dr dateRange, verbose bo
// printSingleLineTimelines is the default `graph` output: each visor
// on exactly one line as "<pk> <concatenated bar>". Verbose adds a
// range header line above.
func printSingleLineTimelines(entries []uptimestats.VisorSummary, dr dateRange, verbose bool) {
func printSingleLineTimelines(entries []uptimestats.VisorSummary, dr dateRange, verbose, preserveOrder bool) {
if len(entries) == 0 {
return
}
dates := collectDates(entries, dr)
if len(dates) == 0 {
return
}
sorted := sortByPK(entries)
sorted := entries
if !preserveOrder {
sorted = sortByPK(entries)
}

// Build all bars first so we can compute the global leading-
// space trim. When uptime tracking was deployed partway through
Expand Down Expand Up @@ -612,13 +641,16 @@ func printSingleLineTimelines(entries []uptimestats.VisorSummary, dr dateRange,
// printRollingTimelines draws the last N hours ending at now as one
// bar per visor. Verbose adds a tick-label row above; non-verbose is
// strictly `<pk> <bar>` to stay grep-friendly.
func printRollingTimelines(entries []uptimestats.VisorSummary, hoursBack int, verbose bool) {
func printRollingTimelines(entries []uptimestats.VisorSummary, hoursBack int, verbose, preserveOrder bool) {
if len(entries) == 0 || hoursBack <= 0 {
return
}
now := time.Now().UTC()
start := now.Add(-time.Duration(hoursBack) * time.Hour)
sorted := sortByPK(entries)
sorted := entries
if !preserveOrder {
sorted = sortByPK(entries)
}

if verbose {
fmt.Printf("# last %dh ending %s (tick labels below)\n",
Expand Down
77 changes: 61 additions & 16 deletions cmd/skywire-cli/commands/got/got.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,21 @@ func init() {
// RootCmd is the got command, defaults to download behavior.
var RootCmd = &cobra.Command{
Use: "got",
Short: "HTTP client with concurrent downloads",
Long: `HTTP client utility with concurrent chunked downloads (RFC 7233),
Short: "HTTP client with concurrent downloads (also speaks skynet:// and dmsg://)",
Long: `HTTP client with concurrent chunked downloads (RFC 7233),
SOCKS5 proxy support, and general-purpose HTTP requests.

Default (no subcommand): concurrent download
got dl <URL> concurrent chunked download
got req <METHOD> <URL> general HTTP request
got head <URL> HEAD request (show headers)`,
URL schemes:
http://, https:// standard HTTP, with chunked range downloads
skynet://<pk>:<port>/path routed through the local visor (SkynetHTTP RPC)
dmsg://<pk>:<port>/path routed through the local visor (DmsgHTTP RPC)

Subcommands:
got dl <URL> chunked download (HTTP); single GET (skywire)
got req <METHOD> <URL> general request, any method
got head <URL> HEAD (HTTP) / GET-headers-only (skywire)

Default (no subcommand) is download.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Default behavior: download
Expand All @@ -73,11 +80,30 @@ SOCKS5 proxy support, and general-purpose HTTP requests.

var dlCmd = &cobra.Command{
Use: "dl <URL> [URL...]",
Short: "Download files with concurrent chunks",
Long: `Download files using concurrent chunked HTTP range requests (RFC 7233).
Falls back to single-stream download when the server doesn't support ranges.`,
Short: "Download files (HTTP chunked, or single GET over skynet/dmsg)",
Long: `Download via HTTP using concurrent chunked range requests (RFC 7233),
falling back to single-stream when the server doesn't support ranges.

skynet:// and dmsg:// URLs are routed through the local visor's
SkynetHTTP / DmsgHTTP RPC and complete in a single GET (no chunking).`,
Args: cobra.MinimumNArgs(1),
Run: func(_ *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, args []string) {
// Skywire URLs go through the visor RPC. Process them first so
// we don't spin up an HTTP client for an http-only flow.
var httpURLs []string
for _, rawURL := range args {
if isSkywireURL(rawURL) {
if err := downloadSkywire(cmd, rawURL); err != nil {
fatal(err)
}
continue
}
httpURLs = append(httpURLs, rawURL)
}
if len(httpURLs) == 0 {
return
}

g, err := newGot()
if err != nil {
fatal(err)
Expand All @@ -90,7 +116,7 @@ Falls back to single-stream download when the server doesn't support ranges.`,

g.ProgressFunc = progressFunc()

for _, rawURL := range args {
for _, rawURL := range httpURLs {
url, err := got.NormalizeURL(rawURL)
if err != nil {
fatal(err)
Expand Down Expand Up @@ -118,18 +144,29 @@ Falls back to single-stream download when the server doesn't support ranges.`,

var reqCmd = &cobra.Command{
Use: "req <METHOD> <URL>",
Short: "Perform an HTTP request",
Short: "Perform an HTTP request (also accepts skynet:// and dmsg://)",
Long: `Perform a general HTTP request (GET, POST, PUT, DELETE, PATCH, etc).
URLs starting with skynet:// or dmsg:// are routed through the local
visor's SkynetHTTP / DmsgHTTP RPC.

Examples:
got req GET https://example.com/api/data
got req POST https://example.com/api -D '{"key":"value"}' -H "Content-Type: application/json"
got req PUT https://example.com/api -D @payload.json`,
got req PUT https://example.com/api -D @payload.json
got req GET skynet://02abc.../health
got req POST dmsg://02abc.../api -D '{"k":"v"}'`,
Args: cobra.ExactArgs(2),
Run: func(_ *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, args []string) {
method := strings.ToUpper(args[0])
rawURL := args[1]

if isSkywireURL(rawURL) {
if err := requestSkywireCmd(cmd, method, rawURL); err != nil {
fatal(err)
}
return
}

url, err := got.NormalizeURL(rawURL)
if err != nil {
fatal(err)
Expand Down Expand Up @@ -187,9 +224,17 @@ Examples:

var headCmd = &cobra.Command{
Use: "head <URL>",
Short: "Show response headers (HEAD request)",
Short: "Show response headers (HEAD on http; GET-headers on skynet/dmsg)",
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, args []string) {
if isSkywireURL(args[0]) {
// Visor RPC has no HEAD; we issue GET and only print
// headers, discarding the body.
if err := headSkywire(cmd, args[0]); err != nil {
fatal(err)
}
return
}
url, err := got.NormalizeURL(args[0])
if err != nil {
fatal(err)
Expand Down
Loading
Loading