pre-flight: bandwidth test + persistent config
First-run host launch now offers a one-time upstream measurement against speed.cloudflare.com/__up via ureq (~5 MB POST, ~5s). The result lives at ~/.config/pixelpass/config.toml under [bandwidth] and feeds the default --max-viewers calculation on subsequent runs. Sticky semantics for the dialog: - Unmeasured: first-run prompt (Run / Skip) - Measured / Skipped: silent — never re-prompts - Failed: ask again on next launch (Retry / give up → Skipped) `pixelpass --reconfigure` re-runs the test unconditionally for users whose connection has changed (new ISP, moved house, etc.). --max-viewers is now Option<u32>. When unset, host startup loads the saved measurement, runs recommended_max_viewers(safe_mbps, bitrate), and surfaces the source in the banner: "max viewers : N (auto: X.X Mbps measured upstream)" — or user-specified / default fallback. User verified end-to-end on 2026-05-21 16:54 EDT: first-run dialog, skip path, run path, --reconfigure refresh, and banner integration all work as expected. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+137
-1
@@ -4,6 +4,7 @@ use iroh_tickets::endpoint::EndpointTicket;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::cli::Cli;
|
||||
use crate::common::{bandwidth, config};
|
||||
use crate::{host, viewer};
|
||||
|
||||
pub async fn run(cli: Cli) -> Result<()> {
|
||||
@@ -20,7 +21,10 @@ pub async fn run(cli: Cli) -> Result<()> {
|
||||
.interact()?;
|
||||
|
||||
match choice {
|
||||
0 => host::run(cli.into_host_opts(true)).await,
|
||||
0 => {
|
||||
preflight_if_needed(&theme).await;
|
||||
host::run(cli.into_host_opts(true)).await
|
||||
}
|
||||
_ => {
|
||||
let ticket = prompt_ticket(&theme)?;
|
||||
viewer::run(ticket, cli.into_viewer_opts(true)).await
|
||||
@@ -28,6 +32,138 @@ pub async fn run(cli: Cli) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `pixelpass --reconfigure` entry point: unconditionally re-run the
|
||||
/// bandwidth pre-flight test, save the result, and return. Used to
|
||||
/// refresh a stale measurement (e.g. user moved house, changed ISP).
|
||||
pub async fn run_reconfigure() -> Result<()> {
|
||||
eprintln!();
|
||||
eprintln!("Re-running bandwidth pre-flight test…");
|
||||
let mut cfg = config::load().unwrap_or_default();
|
||||
run_bandwidth_test(&mut cfg).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// First-run pre-flight gate. Called once, when the user picks "Host" in
|
||||
/// the interactive menu. Behavior by saved status:
|
||||
/// - Unmeasured (first ever launch): explain + offer Run / Skip
|
||||
/// - Failed (previous attempt errored): offer Retry / give-up-and-skip
|
||||
/// - Measured or Skipped: silent — never re-prompts
|
||||
async fn preflight_if_needed(theme: &ColorfulTheme) {
|
||||
let mut cfg = config::load().unwrap_or_default();
|
||||
match cfg.bandwidth.status {
|
||||
config::BandwidthStatus::Measured | config::BandwidthStatus::Skipped => return,
|
||||
config::BandwidthStatus::Unmeasured => {
|
||||
eprintln!();
|
||||
eprintln!("First-time setup");
|
||||
eprintln!("────────────────");
|
||||
eprintln!("PixelPass can measure your upload speed to recommend a safe");
|
||||
eprintln!("default for how many viewers your connection can handle.");
|
||||
eprintln!("The test takes about 5 seconds and uploads ~5 MB to");
|
||||
eprintln!("Cloudflare's open speed-test endpoint.");
|
||||
eprintln!();
|
||||
eprintln!("If you skip, a conservative default (2 viewers) is used.");
|
||||
eprintln!("You can run the test later with `pixelpass --reconfigure`.");
|
||||
eprintln!();
|
||||
|
||||
let Ok(choice) = Select::with_theme(theme)
|
||||
.with_prompt("What would you like to do?")
|
||||
.items(&[
|
||||
"Run the bandwidth test (recommended)",
|
||||
"Skip — use the conservative default",
|
||||
])
|
||||
.default(0)
|
||||
.interact()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if choice == 1 {
|
||||
cfg.bandwidth = config::BandwidthEntry {
|
||||
status: config::BandwidthStatus::Skipped,
|
||||
upstream_mbps: None,
|
||||
measured_at: None,
|
||||
};
|
||||
let _ = config::save(&cfg);
|
||||
eprintln!("Pre-flight skipped.");
|
||||
return;
|
||||
}
|
||||
run_bandwidth_test(&mut cfg).await;
|
||||
}
|
||||
config::BandwidthStatus::Failed => {
|
||||
eprintln!();
|
||||
let Ok(choice) = Select::with_theme(theme)
|
||||
.with_prompt("Last bandwidth test failed. Try again?")
|
||||
.items(&[
|
||||
"Yes — retry now",
|
||||
"No — use the conservative default",
|
||||
])
|
||||
.default(0)
|
||||
.interact()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if choice == 1 {
|
||||
cfg.bandwidth = config::BandwidthEntry {
|
||||
status: config::BandwidthStatus::Skipped,
|
||||
upstream_mbps: None,
|
||||
measured_at: None,
|
||||
};
|
||||
let _ = config::save(&cfg);
|
||||
eprintln!("OK — using the conservative default.");
|
||||
return;
|
||||
}
|
||||
run_bandwidth_test(&mut cfg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_bandwidth_test(cfg: &mut config::Config) {
|
||||
eprintln!();
|
||||
eprintln!("Measuring upstream…");
|
||||
|
||||
let result = tokio::task::spawn_blocking(bandwidth::measure_upstream_blocking).await;
|
||||
let measurement = match result {
|
||||
Ok(Ok(m)) => m,
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Test failed: {e:#}");
|
||||
eprintln!("Marking as failed — you'll be asked again on next launch.");
|
||||
cfg.bandwidth = config::BandwidthEntry {
|
||||
status: config::BandwidthStatus::Failed,
|
||||
upstream_mbps: None,
|
||||
measured_at: None,
|
||||
};
|
||||
let _ = config::save(cfg);
|
||||
return;
|
||||
}
|
||||
Err(join_err) => {
|
||||
eprintln!("Test task panicked: {join_err}");
|
||||
cfg.bandwidth = config::BandwidthEntry {
|
||||
status: config::BandwidthStatus::Failed,
|
||||
upstream_mbps: None,
|
||||
measured_at: None,
|
||||
};
|
||||
let _ = config::save(cfg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"Measured {:.2} Mbps up (safe estimate {:.2} Mbps, took {:.1}s).",
|
||||
measurement.raw_mbps,
|
||||
measurement.safe_mbps,
|
||||
measurement.elapsed.as_secs_f64()
|
||||
);
|
||||
cfg.bandwidth = config::BandwidthEntry {
|
||||
status: config::BandwidthStatus::Measured,
|
||||
upstream_mbps: Some(measurement.safe_mbps),
|
||||
measured_at: Some(chrono::Utc::now()),
|
||||
};
|
||||
if let Err(e) = config::save(cfg) {
|
||||
eprintln!("Warning: failed to save result: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
fn print_welcome() {
|
||||
eprintln!();
|
||||
eprintln!("Welcome to PixelPass.");
|
||||
|
||||
Reference in New Issue
Block a user