Skip to content

✨ feat(traffic): live network throughput in the status pill (#112) - #152

Merged
Cemoche merged 9 commits into
mainfrom
issue-112-throughput
Oct 21, 2026
Merged

Cemoche merged 9 commits into
mainfrom
issue-112-throughput

Conversation

@Cemoche

@Cemoche Cemoche commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Closes #112

Summary

The status pill shows the live network rate (read + written) of the active workspace in its right-hand slot. This covers PostgreSQL, MySQL and MariaDB, direct and through SSH. SQLite and MongoDB show nothing, and never a guessed number.

Backend: what is counted, and where

  • SSH tunnel: the forwarding loop counts the database protocol bytes it copies. The workspace's counter survives a tunnel that is reopened and a database switch.
  • PostgreSQL direct: there is no listener. The driver opens its own TCP stream, wraps it in a counting AsyncRead + AsyncWrite adapter, and hands it to tokio-postgres through connect_raw.
    • TLS modes, SNI and verification are unchanged: same make_tls_connect(host), same rustls config.
    • Every resolved address is tried in turn, under a single 10 s bound. Keepalive uses the OS default, which is 2 h, the same as tokio-postgres.
    • Cancels use cancel_query_raw to the session's own peer_addr(), with the configured host as TLS name, so round-robin DNS can't send a cancel to another server.
  • MySQL/MariaDB direct: mysql_async 0.37 can't take a caller stream, and its Unix-socket route skips TLS. Each connection (session, KILL QUERY, the Prefer retry) therefore dials a one-shot relay on 127.0.0.1:0.
    • The relay accepts exactly one connection, closes its listener at once and never re-accepts. If unused, it closes after 10 s, or immediately when its owner is dropped.
    • TLS stays end to end and is verified against the real host (tls_hostname_override), never 127.0.0.1. prefer_socket(false) is kept.
    • A second, unexpected connection is refused and never reaches the server. This is tested.
    • The automated security review flagged the first version, a persistent forwarder. The relay was hardened in response.
    • The remaining risk is a microsecond window between bind and the driver's connect, on a random port. The review judged it not exploitable in practice: it only gives a path to a server the attacker can already reach.
  • Rate: a 2 s sliding window, computed only when asked, with relaxed atomics and no per-read allocation. The new command workspace_throughput returns {readPerSec, writtenPerSec} | null (counts only, no query text or secret). capabilities.throughput says whether the workspace is counted. A PostgreSQL Unix-socket host reports false.
  • Errors: an unreachable server now reads "cannot connect to host:port: …", with no secret in the message.

Frontend

  • Polling: every 1 s, only for the active, connected workspace, and paused while the window is hidden. Stale answers are dropped.
  • Format: 0 B/s, 1.2 KB/s, 3.4 MB/s (1 KB = 1000 B). The slot is fixed-width with tabular figures and is reserved from connect, so the pill never shifts.
  • Accessibility: the rate is part of the pill's accessible name. While the pill has focus, that name is frozen per workspace, so screen readers don't re-announce it every second; it refreshes on blur.

Docs: spec §2.3 amendment #112. Capabilities stay core:default.

The pre-existing SSH tunnel loopback listener has the same open-forwarder exposure. It is filed separately as #151.

How it was tested

$ cargo test --lib                                   # 277 passed (single job)
$ cargo clippy --all-targets -- -D warnings          # clean (also compiles the integration tests)
$ pnpm exec vitest run --maxWorkers=1                # 110 files, 1446 tests passed; last round: StatusPill/throughput/AppToolbar 61 passed
$ pnpm exec tsc --noEmit                             # clean
$ bash scripts/check-no-secrets.sh                   # clean

The new integration tests need the test databases and sshd, so they run in CI's pnpm test:rust:

  • tests/throughput.rs: rate after 300 kB, idle exactly 0, cancel on the counted PG and MySQL paths, and verify-full for PG and MySQL, including a case that must fail on the wrong certificate name;
  • manager_ssh.rs::throughput_counts_the_tunnel_traffic.

The existing manager-level PG and MySQL tests now go through the counted paths.

Manual check list

  1. Open a direct PostgreSQL connection.
    • The pill's right end shows nothing for about 1 s, then 0 B/s.
    • The text does not move when the number appears.
  2. Open a big table, or run SELECT repeat('x', 5000000) in a SQL tab. The number climbs (KB/s, MB/s) while it loads, then falls back to 0 B/s within about 2 s.
  3. Same with direct MySQL 8 and MariaDB.
    • A long SELECT SLEEP(30) can still be cancelled.
    • Opening with a wrong port says "cannot connect to host:port: Connection refused".
  4. An SSH connection (PG and MySQL): the number moves the same way. The SSH keepalive alone keeps it at 0 B/s.
  5. SQLite: no number and no reserved space; the text is centred as before.
  6. Switch between two open workspaces. Each pill shows only its own rate (briefly blank after switching). A background workspace is never polled.
  7. Narrow the window: the text gets an ellipsis before it runs under the number. The number never wraps or overflows.
  8. Screen reader (Orca): focus the pill. It reads "LOCAL | PostgreSQL … | network 0 B/s", and nothing is announced on its own while the number changes.
  9. A PostgreSQL connection with TLS verify-full and the right host connects. With a wrong host name, it fails with the certificate error (not a relay error).
  10. Minimise the window for a few seconds, then restore it: polling stops while hidden (no workspace_throughput calls) and the number is blank for about a second on restore, then live again. This checks that WebKitGTK sends the visibility change.
  11. With Orca, Tab to the pill while a large page loads: the name is read once and not re-read as the number moves. Switch workspace or disconnect while it keeps focus: the name never keeps the old rate.

Checklist

  • pnpm test green
  • pnpm lint green (tsc + clippy -D warnings)
  • pnpm test:rust green (CI; test databases not available locally)
  • No secret, token, real password or private key in the diff
  • User-visible strings in src/lib/strings.ts
  • Commits follow <emoji> <type>(<scope>): <description>

🤖 Generated with Claude Code

Cemoche and others added 9 commits October 21, 2026 13:08
…ack relay

Traffic counts bytes read from and written to a database server with one
relaxed atomic add per read or write call. pump() forwards a connection
and counts its server side; RateWindow turns samples into bytes/s over a
2 s window. Relay forwards loopback connections to host:port and keeps
why the server could not be reached (#112).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rate

Every network connection of a PostgreSQL, MySQL or MariaDB workspace now
dials a local end the app owns: the SSH tunnel, which counts what it
forwards, or for a direct connection a loopback relay opened with the
workspace. The new workspace_throughput command returns bytes/s over a
2 s window (null for SQLite and MongoDB, or before a reference exists);
the capability 'throughput' tells the frontend which workspaces have
one. When the relay cannot reach the server, the error says why instead
of the driver's end-of-file (#112).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The workspace-wide relay listened on 127.0.0.1 for the workspace's whole
life and forwarded any local connection to the database host. Direct
connections are now counted by the drivers instead:

- PostgreSQL opens its own TCP stream, wraps it in Counted and hands it
  to connect_raw; cancels use cancel_query_raw on a counted stream. No
  listener.
- mysql_async cannot take a stream (and over a Unix socket it skips TLS
  and sends the password in clear), so each MySQL / MariaDB connection
  (session, KILL QUERY, Prefer retry) dials a one-shot relay: it accepts
  exactly one connection, closes its listener at once, and closes after
  10 s if nobody dials it. A second connection is refused (tested).

Integration tests cover cancel and verify-full on both counted paths
(#112).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
While a PostgreSQL, MySQL or MariaDB workspace is connected, the pill
polls workspace_throughput every second and shows read + written bytes
per second in its right-hand slot (0 B/s, 1.2 KB/s, 3.4 MB/s; 1 KB =
1000 B). The slot is 8 characters wide, right-aligned with tabular
figures and reserved from the start, so the pill never shifts. The rate
is part of the pill's accessible name, not a live region. SQLite and
MongoDB have no slot; no number is shown until the backend has one
(#112).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t capability

- OneShot carries a cancel handle: dropped before anyone dials it (the
  driver's connect future was dropped), its listener closes at once
  instead of after 10 s. An accepted connection keeps being forwarded.
- A counted PostgreSQL session's cancel requests dial the address the
  session reached (peer_addr), not a new DNS resolution that could pick
  another server.
- WorkspaceInfo.capabilities.throughput is what is actually counted: a
  PostgreSQL Unix-socket directory as host reports false, so the pill
  keeps no slot that would stay blank.
- MySQL verify-full through the relay: the wrong-name case fails with
  the certificate error (CI) (#112 review).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le focused

- useThroughput stops polling while the window is hidden (minimized)
  and shows nothing then; shown again it starts over blank. An answer
  that arrives after the stop is dropped.
- The pill's accessible name keeps the rate it had when the pill got
  focus until it loses it, so screen readers do not re-announce it every
  second; the visible slot keeps moving (#112 review).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rop, PG cancel

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The focus snapshot of the rate now belongs to one workspace, and a
workspace that is not counted (disconnected) names no rate: switching
workspace or disconnecting while the pill keeps focus no longer leaves
the previous rate in its accessible name (#112 review).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Cemoche
Cemoche merged commit cea168e into main Oct 21, 2026
6 checks passed
@Cemoche
Cemoche deleted the issue-112-throughput branch October 21, 2026 11:55
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.

[Refonte UI] Live network throughput in the status pill

1 participant