diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 94d1b15..fd0e78f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,9 +2,9 @@ name: CI Checks
on:
push:
- branches: [ main ]
+ branches: [ main, dev ]
pull_request:
- branches: [ main ]
+ branches: [ main, dev ]
env:
CARGO_TERM_COLOR: always
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..666ecaa
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,43 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.1.0] - 2026-07-04
+
+### Added
+- **Zero-Friction Portable Workflow**: Redesigned the onboarding process by completely removing the setup key requirements. Added a welcoming onboarding screen (Step 0) in the browser before configuring the system.
+- **Cloudflare Tunnel Auto-Start**: Configured the Cloudflare tunnel to dynamically start immediately upon completing the setup wizard if the user opts in, removing any need for a server restart.
+- **User-Friendly Startup Console**: Updated the server CLI banner on launch to show a clean box-drawing layout directing the user to press Enter to instantly minimize the window to system tray and open their default browser.
+
+### Fixed
+- **Windows Console Flashing**: Resolved an issue where multiple empty Command Prompt/PowerShell terminal windows flashed on the screen on Windows during startup and periodic background operations (telemetry WMI polling, network/hardware status checks, registry queries, and Cloudflare tunnel spawning) by forcing child processes to spawn silently with the `CREATE_NO_WINDOW` process creation flag.
+
+## [1.0.1] - 2026-07-03
+
+### Fixed
+- **CI/CD Pipeline**: GitHub Actions build and linker failures on macOS and Linux runners.
+- **Linux Platform**: Added missing `libxdo-dev` dependency on build environments to fix input compilation/linking.
+- **macOS Platform**: Fixed key mapping error by conditionalizing `F21`–`F24` keys, which are not supported by Enigo on macOS.
+- **macOS Platform**: Silenced compiler warning for unused `brightness` variable during macOS-specific display operations.
+- **Frontend Code Quality**: Resolved all `oxlint` accessibility and linting warnings:
+ - Cleaned up unused imports and variables across multiple components.
+ - Linked form controls to labels for better screen reader compatibility.
+ - Added missing `aria-label` properties to toggle elements.
+ - Normalized React Hook dependency arrays.
+- **Backend Code Quality**: Removed unused imports (`std::io`, `std::ffi::c_void`), cleaned up unused `mut` specifiers, and resolved `Arc` borrow-after-move error in `main.rs`.
+
+## [1.0.0] - 2026-07-03
+
+### Added
+- **Core Architecture**: Lightweight remote system administration agent utilizing a Rust backend and embedded React/TypeScript single-page app (SPA).
+- **System Telemetry**: Real-time telemetry monitoring including CPU load, RAM usage, network interface speeds, disk usage, and battery capacity.
+- **File Manager**: Directory navigation, file downloads, secure path validation blocklists, and streaming uploads supporting files up to 500MB.
+- **Terminal Emulator**: Remote interactive terminal interface leveraging PTY and `xterm.js`.
+- **Script Engine**: Secure execution of PowerShell (Windows) and Bash (macOS/Linux) scripts with customizable execution timeouts.
+- **Power Management**: Support for remote Shutdown, Restart, Sleep, Hibernate, Sign Out, Lock, and Switch User actions with a 5-second grace period cancel queue.
+- **Cloudflare Tunnels**: Integrated `cloudflared` tunnel creation for secure remote access without port forwarding.
+- **System Tray Widget**: Native system tray application displaying active status, quick tunnel actions, and port configurations.
+- **Security & MFA**: Cryptographic password hashing (Argon2id), mandatory TOTP multi-factor authentication setup, recovery code generation, and automated lockout policies.
diff --git a/README.md b/README.md
index 60583bb..a7b119b 100644
--- a/README.md
+++ b/README.md
@@ -139,7 +139,8 @@ On first run, the backend:
2. Stores the JWT signing key in your OS keychain (Credential Manager / Keychain / Secret Service)
3. Downloads the correct `cloudflared` binary for your platform with SHA256 verification
4. Binds to `localhost:3939` (falls back to random port)
-5. Opens your browser to the setup wizard
+5. Generates a one-time **Setup Key** printed to the console window. This key acts as a security token to verify physical/owner access to the host machine.
+6. Opens your browser to the first-run Setup Wizard, where you must enter this Setup Key to unlock configuration steps (admin credentials, TOTP, and remote access relay).
### Production Build
@@ -264,7 +265,7 @@ sysdeck/
| POST | `/api/setup/password` | Set initial password (step 1) |
| POST | `/api/setup/totp` | Generate TOTP secret (step 2) |
| POST | `/api/setup/verify-totp` | Verify TOTP code (step 3) |
-| POST | `/api/setup/recovery-codes` | Generate recovery codes (step 4) |
+| POST | `/api/setup/relay` | Set Cloudflare relay opt-in (step 4) |
| POST | `/api/setup/finish` | Complete setup |
### Dashboard & Telemetry
diff --git a/backend/src/hardware.rs b/backend/src/hardware.rs
index d0e7004..8cbe263 100644
--- a/backend/src/hardware.rs
+++ b/backend/src/hardware.rs
@@ -2,7 +2,6 @@ use axum::extract::State;
use axum::response::{IntoResponse, Json};
use serde::{Deserialize, Serialize};
use serde_json::json;
-use std::process::Command;
use std::sync::atomic::Ordering;
use std::time::Duration;
@@ -82,7 +81,7 @@ pub struct ScheduledPowerRequest {
#[allow(dead_code)]
fn run_cmd(cmd: &str, args: &[&str]) -> Result {
- let output = Command::new(cmd)
+ let output = crate::new_command(cmd)
.args(args)
.output()
.map_err(|e| format!("Failed to execute {}: {}", cmd, e))?;
@@ -99,7 +98,7 @@ fn run_powershell(script: &str) -> Result {
use std::io::Write;
use std::process::Stdio;
- let mut child = Command::new("powershell")
+ let mut child = crate::new_command("powershell")
.args(["-NoProfile", "-Command", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -1130,7 +1129,7 @@ pub async fn schedule_power_handler(
crate::power::PowerAction::Restart => "/r",
_ => "/s",
};
- let _ = Command::new("shutdown")
+ let _ = crate::new_command("shutdown")
.args([flag, "/f", "/t", "1"])
.spawn();
} else {
diff --git a/backend/src/lib.rs b/backend/src/lib.rs
index 3e0c408..b884cdb 100644
--- a/backend/src/lib.rs
+++ b/backend/src/lib.rs
@@ -76,7 +76,33 @@ pub struct AppState {
pub terminal_state: Arc,
pub tunnel_state: Arc,
pub port: u16,
- pub setup_token: Arc,
+}
+
+pub fn new_command>(program: S) -> std::process::Command {
+ #[cfg(target_os = "windows")]
+ {
+ use std::os::windows::process::CommandExt;
+ let mut cmd = std::process::Command::new(program);
+ cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
+ cmd
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ std::process::Command::new(program)
+ }
+}
+
+pub fn new_tokio_command>(program: S) -> tokio::process::Command {
+ #[cfg(target_os = "windows")]
+ {
+ let mut cmd = tokio::process::Command::new(program);
+ cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
+ cmd
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ tokio::process::Command::new(program)
+ }
}
pub fn get_data_dir() -> PathBuf {
@@ -212,7 +238,7 @@ pub async fn find_available_port() -> (u16, tokio::net::TcpListener) {
fn is_startup_enabled() -> bool {
#[cfg(target_os = "windows")]
{
- std::process::Command::new("reg")
+ new_command("reg")
.args([
"query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
@@ -234,7 +260,7 @@ fn set_startup(enabled: bool) {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
if enabled {
- let _ = std::process::Command::new("reg")
+ let _ = new_command("reg")
.args([
"add",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
@@ -248,7 +274,7 @@ fn set_startup(enabled: bool) {
])
.output();
} else {
- let _ = std::process::Command::new("reg")
+ let _ = new_command("reg")
.args([
"delete",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
diff --git a/backend/src/main.rs b/backend/src/main.rs
index 91c53e2..435f05b 100644
--- a/backend/src/main.rs
+++ b/backend/src/main.rs
@@ -5,7 +5,6 @@
use std::sync::Arc;
-use rand::Rng;
use tokio::sync::{broadcast, oneshot, Mutex};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
@@ -202,14 +201,6 @@ async fn main() {
let jwt_key = Arc::new(auth::load_or_create_jwt_key().expect("Failed to load JWT signing key"));
println!(" ✓ JWT signing key loaded");
- // Generate setup token for headless/remote setup
- let setup_token: String = rand::thread_rng()
- .sample_iter(&rand::distributions::Alphanumeric)
- .take(16)
- .map(char::from)
- .collect();
- let setup_token = Arc::new(setup_token);
-
let (port, listener) = find_available_port().await;
println!(" ✓ Server bound to localhost:{}", port);
@@ -348,7 +339,6 @@ async fn main() {
terminal_state,
tunnel_state: tunnel_state.clone(),
port,
- setup_token: setup_token.clone(),
};
let app = build_router(app_state.clone());
@@ -411,49 +401,37 @@ async fn main() {
server_ready_rx.await.ok();
- // ── Startup banner (complete) ────────────────────────────────────────────
+ // ── Startup banner ────────────────────────────────────────────
println!();
- println!(" ──────────────────────────────────────────────────");
- println!(" Dashboard → http://localhost:{}", port);
- println!(" Manage → System tray icon");
- println!(" Setup key → {}", setup_token);
- println!(" ──────────────────────────────────────────────────");
-
- let is_headless = cfg!(target_os = "linux") && std::env::var("DISPLAY").is_err();
- if is_headless {
- println!();
- println!(" Headless mode — no display detected.");
- println!(" Use SSH port forwarding to reach the dashboard:");
- println!(" ssh -L {}:127.0.0.1:{} user@host", port, port);
- } else {
- println!();
- println!(" Press Enter to open the dashboard.");
- println!(" Close this window at any time — SysDeck keeps running.");
- }
+ println!("┌────────────────────────────────────────────────────────┐");
+ println!("│ ⚡ SysDeck is running! │");
+ println!("├────────────────────────────────────────────────────────┤");
+ println!("│ The app is now active in your system tray. │");
+ println!("│ │");
+ println!("│ Press [Enter] to open the setup wizard in your browser│");
+ println!("│ Press [Ctrl+C] to stop the app │");
+ println!("└────────────────────────────────────────────────────────┘");
println!();
- // ────────────────────────────────────────────────────────────────────────
// Spawn a blocking task to wait for Enter, then open the browser and
// detach the splash console. On Windows, if the user closes the window
// first (CTRL_CLOSE_EVENT), FreeConsole() closes stdin, read_line()
// returns EOF, and open_dashboard_once() is a no-op (already opened).
- if !is_headless {
- tokio::task::spawn_blocking(move || {
- let mut buf = String::new();
- // Returns Ok(0) on EOF (stdin closed by FreeConsole) or Ok(n) on Enter.
- let _ = std::io::stdin().read_line(&mut buf);
-
- #[cfg(windows)]
- {
- detach_splash_console();
- open_dashboard_once();
- }
- #[cfg(not(windows))]
- {
- let _ = open::that(format!("http://localhost:{}", port));
- }
- });
- }
+ tokio::task::spawn_blocking(move || {
+ let mut buf = String::new();
+ // Returns Ok(0) on EOF (stdin closed by FreeConsole) or Ok(n) on Enter.
+ let _ = std::io::stdin().read_line(&mut buf);
+
+ #[cfg(windows)]
+ {
+ detach_splash_console();
+ open_dashboard_once();
+ }
+ #[cfg(not(windows))]
+ {
+ let _ = open::that(format!("http://localhost:{}", port));
+ }
+ });
server_handle.await.expect("Server task panicked");
diff --git a/backend/src/network.rs b/backend/src/network.rs
index bde8b82..8eef1c8 100644
--- a/backend/src/network.rs
+++ b/backend/src/network.rs
@@ -1,7 +1,6 @@
use axum::response::{IntoResponse, Json};
use serde::{Deserialize, Serialize};
use serde_json::json;
-use std::process::Command;
use tracing;
#[derive(Serialize, Debug)]
@@ -51,7 +50,7 @@ fn default_wifi_security() -> String {
}
fn run_cmd(cmd: &str, args: &[&str]) -> Result {
- let output = Command::new(cmd)
+ let output = crate::new_command(cmd)
.args(args)
.output()
.map_err(|e| format!("Failed to execute {}: {}", cmd, e))?;
@@ -66,7 +65,7 @@ fn run_cmd(cmd: &str, args: &[&str]) -> Result {
fn run_powershell(script: &str) -> Result {
use std::io::Write;
use std::process::Stdio;
- let mut child = Command::new("powershell")
+ let mut child = crate::new_command("powershell")
.args(["-NoProfile", "-Command", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
diff --git a/backend/src/power.rs b/backend/src/power.rs
index 239bdb0..39c9f9d 100644
--- a/backend/src/power.rs
+++ b/backend/src/power.rs
@@ -31,73 +31,63 @@ impl SystemCommands for RealOs {
#[cfg(target_os = "windows")]
match action {
PowerAction::Shutdown => {
- let _ = std::process::Command::new("shutdown")
+ let _ = crate::new_command("shutdown")
.args(["/s", "/t", "1"])
.spawn();
}
PowerAction::Restart => {
- let _ = std::process::Command::new("shutdown")
+ let _ = crate::new_command("shutdown")
.args(["/r", "/t", "1"])
.spawn();
}
PowerAction::Sleep => {
- let _ = std::process::Command::new("rundll32.exe")
+ let _ = crate::new_command("rundll32.exe")
.args(["powrprof.dll,SetSuspendState", "0", "1", "0"])
.spawn();
}
PowerAction::Hibernate => {
- let _ = std::process::Command::new("rundll32.exe")
+ let _ = crate::new_command("rundll32.exe")
.args(["powrprof.dll,SetSuspendState", "1", "1", "0"])
.spawn();
}
PowerAction::SignOut => {
- let _ = std::process::Command::new("shutdown").args(["/l"]).spawn();
+ let _ = crate::new_command("shutdown").args(["/l"]).spawn();
}
PowerAction::Lock => {
- let _ = std::process::Command::new("rundll32.exe")
+ let _ = crate::new_command("rundll32.exe")
.args(["user32.dll,LockWorkStation"])
.spawn();
}
PowerAction::SwitchUser => {
// Switch to the login screen to allow another user to log in
- let _ = std::process::Command::new("tsdiscon.exe").spawn();
+ let _ = crate::new_command("tsdiscon.exe").spawn();
}
}
#[cfg(target_os = "linux")]
match action {
PowerAction::Shutdown => {
- let _ = std::process::Command::new("systemctl")
- .arg("poweroff")
- .spawn();
+ let _ = crate::new_command("systemctl").arg("poweroff").spawn();
}
PowerAction::Restart => {
- let _ = std::process::Command::new("systemctl")
- .arg("reboot")
- .spawn();
+ let _ = crate::new_command("systemctl").arg("reboot").spawn();
}
PowerAction::Sleep => {
- let _ = std::process::Command::new("systemctl")
- .arg("suspend")
- .spawn();
+ let _ = crate::new_command("systemctl").arg("suspend").spawn();
}
PowerAction::Hibernate => {
- let _ = std::process::Command::new("systemctl")
- .arg("hibernate")
- .spawn();
+ let _ = crate::new_command("systemctl").arg("hibernate").spawn();
}
PowerAction::SignOut => {
- let _ = std::process::Command::new("loginctl")
+ let _ = crate::new_command("loginctl")
.args(["terminate-session", "self"])
.spawn();
}
PowerAction::Lock => {
- let _ = std::process::Command::new("loginctl")
- .arg("lock-session")
- .spawn();
+ let _ = crate::new_command("loginctl").arg("lock-session").spawn();
}
PowerAction::SwitchUser => {
// Switch user via display manager
- let _ = std::process::Command::new("dm-tool")
+ let _ = crate::new_command("dm-tool")
.arg("switch-to-greeter")
.spawn();
}
@@ -105,41 +95,41 @@ impl SystemCommands for RealOs {
#[cfg(target_os = "macos")]
match action {
PowerAction::Shutdown => {
- let _ = std::process::Command::new("osascript")
+ let _ = crate::new_command("osascript")
.args(["-e", "tell application \"System Events\" to shut down"])
.spawn();
}
PowerAction::Restart => {
- let _ = std::process::Command::new("osascript")
+ let _ = crate::new_command("osascript")
.args(["-e", "tell application \"System Events\" to restart"])
.spawn();
}
PowerAction::Sleep => {
- let _ = std::process::Command::new("osascript")
+ let _ = crate::new_command("osascript")
.args(["-e", "tell application \"System Events\" to sleep"])
.spawn();
}
PowerAction::Hibernate => {
// macOS: enable hibernate mode then sleep
- let _ = std::process::Command::new("pmset")
+ let _ = crate::new_command("pmset")
.args(["-a", "hibernatemode", "25"])
.spawn();
- let _ = std::process::Command::new("osascript")
+ let _ = crate::new_command("osascript")
.args(["-e", "tell application \"System Events\" to sleep"])
.spawn();
}
PowerAction::SignOut => {
- let _ = std::process::Command::new("osascript")
+ let _ = crate::new_command("osascript")
.args(["-e", "tell application \"System Events\" to log out"])
.spawn();
}
PowerAction::Lock => {
- let _ = std::process::Command::new("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession")
+ let _ = crate::new_command("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession")
.arg("-suspend")
.spawn();
}
PowerAction::SwitchUser => {
- let _ = std::process::Command::new("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession")
+ let _ = crate::new_command("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession")
.arg("-suspend")
.spawn();
}
diff --git a/backend/src/process.rs b/backend/src/process.rs
index 1e8ca26..f31a12f 100644
--- a/backend/src/process.rs
+++ b/backend/src/process.rs
@@ -48,7 +48,7 @@ pub async fn kill_handler(Json(body): Json) -> impl IntoResponse {
#[cfg(target_os = "windows")]
{
let pid = body.pid;
- match std::process::Command::new("taskkill")
+ match crate::new_command("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.output()
{
diff --git a/backend/src/script.rs b/backend/src/script.rs
index 70ddbc0..5e12a88 100644
--- a/backend/src/script.rs
+++ b/backend/src/script.rs
@@ -10,7 +10,6 @@ use axum::response::{IntoResponse, Json, Response};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::io::AsyncBufReadExt;
-use tokio::process::Command;
use tokio::sync::broadcast;
use tokio::sync::Mutex;
use uuid::Uuid;
@@ -19,7 +18,7 @@ fn kill_process_tree(pid: Option) {
let Some(pid) = pid else { return };
#[cfg(target_os = "windows")]
{
- let _ = std::process::Command::new("taskkill")
+ let _ = crate::new_command("taskkill")
.args(["/T", "/F", "/PID", &pid.to_string()])
.spawn()
.map(|mut c| {
@@ -28,10 +27,10 @@ fn kill_process_tree(pid: Option) {
}
#[cfg(unix)]
{
- let _ = std::process::Command::new("pkill")
+ let _ = crate::new_command("pkill")
.args(["-P", &pid.to_string()])
.spawn();
- let _ = std::process::Command::new("kill")
+ let _ = crate::new_command("kill")
.args(["-9", &pid.to_string()])
.spawn();
}
@@ -223,7 +222,7 @@ async fn run_script(
"$OutputEncoding = [System.Text.Encoding]::UTF8; {}",
content
);
- Command::new("powershell")
+ crate::new_tokio_command("powershell")
.args(["-NoProfile", "-Command", &ps_content])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
@@ -232,7 +231,7 @@ async fn run_script(
#[cfg(target_os = "windows")]
{
let content = content.replace("\r\n", " & ").replace('\n', " & ");
- Command::new("cmd")
+ crate::new_tokio_command("cmd")
.args(["/C", &content])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
@@ -250,7 +249,7 @@ async fn run_script(
#[cfg(target_os = "windows")]
{
let content = content.replace("\r\n", " & ").replace('\n', " & ");
- Command::new("cmd")
+ crate::new_tokio_command("cmd")
.args(["/C", &content])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
@@ -258,7 +257,7 @@ async fn run_script(
}
#[cfg(not(target_os = "windows"))]
{
- Command::new("bash")
+ crate::new_tokio_command("bash")
.args(["-c", content])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
diff --git a/backend/src/setup.rs b/backend/src/setup.rs
index cdb7b57..72806ac 100644
--- a/backend/src/setup.rs
+++ b/backend/src/setup.rs
@@ -89,6 +89,23 @@ pub async fn api_password_handler(
State(state): State,
Json(body): Json,
) -> Response {
+ let needs_setup = {
+ let conn = state.db.lock().await;
+ db::is_setup_complete(&conn).map(|c| !c).unwrap_or(true)
+ };
+
+ if !needs_setup {
+ tracing::warn!(handler = "api_password_handler", "setup already complete");
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(serde_json::json!({
+ "success": false,
+ "error": "Setup is already completed"
+ })),
+ )
+ .into_response();
+ }
+
if body.password.len() < 8 {
tracing::warn!(handler = "api_password_handler", "password too short");
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({"success": false, "error": "Password must be at least 8 characters"}))).into_response();
@@ -292,6 +309,14 @@ pub async fn api_finish_handler(
);
let _ = db::wal_checkpoint(&conn);
drop(conn);
+
+ if flow.relay_opt_in {
+ let ts = state.tunnel_state.clone();
+ tokio::spawn(async move {
+ let _ = crate::tunnel::TunnelState::start(ts).await;
+ });
+ }
+
tracing::info!(handler = "api_finish_handler", "setup complete");
Json(serde_json::json!({"success": true})).into_response()
}
diff --git a/backend/src/telemetry.rs b/backend/src/telemetry.rs
index d3b71e1..6bc8d66 100644
--- a/backend/src/telemetry.rs
+++ b/backend/src/telemetry.rs
@@ -60,7 +60,7 @@ fn get_wmi_temperatures() -> (Option, Option) {
let mut gpu = None;
for (source, cmd) in &queries {
tracing::debug!(source, "querying WMI thermal zones");
- let output = match std::process::Command::new("powershell")
+ let output = match crate::new_command("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", cmd])
.output()
{
diff --git a/backend/src/tunnel.rs b/backend/src/tunnel.rs
index 721d0c4..83c8cb4 100644
--- a/backend/src/tunnel.rs
+++ b/backend/src/tunnel.rs
@@ -9,7 +9,6 @@ use serde::Serialize;
use sha2::{Digest, Sha256};
use tokio::fs;
use tokio::io::{AsyncBufReadExt, BufReader};
-use tokio::process::Command;
use tokio::sync::{broadcast, Mutex, RwLock};
use crate::AppState;
@@ -260,7 +259,7 @@ async fn run_tunnel_loop(
None => return,
};
- let mut child = match Command::new(&state.exe_path)
+ let mut child = match crate::new_tokio_command(&state.exe_path)
.args([
"tunnel",
"--no-autoupdate",
diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs
index 411b02b..73b332a 100644
--- a/backend/tests/common/mod.rs
+++ b/backend/tests/common/mod.rs
@@ -90,7 +90,6 @@ fn test_app_inner(
terminal_state: Arc::new(TerminalState::default()),
tunnel_state: Arc::new(tunnel_state),
port: 3939,
- setup_token: Arc::new("test-setup-token-123".to_string()),
};
let router = sysdeck_agent::build_router(app_state.clone());
diff --git a/frontend/src/pages/Setup.tsx b/frontend/src/pages/Setup.tsx
index 67e93fb..38d46dd 100644
--- a/frontend/src/pages/Setup.tsx
+++ b/frontend/src/pages/Setup.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { useNavigate } from "react-router-dom"
-import { Check, Copy, Download, Eye, EyeOff, ShieldAlert } from "lucide-react"
+import { Check, Copy, Download, Eye, EyeOff } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
@@ -347,6 +347,11 @@ function StepRelay({ onComplete }: { onComplete: (enabled: boolean) => void }) {
Optionally expose this agent to the internet via a Cloudflare Quick Tunnel.
This allows remote access from outside your local network without port forwarding.
+
+
+ *Note: Because this uses a free, zero-signup tunnel, your remote URL will change every time the app restarts. We are working on adding persistent domain support soon!
+
+
+ You can turn on the Cloudflare Relay to access this PC from anywhere in the world.
+
+
+ *Note: Because this uses a free, zero-signup tunnel, your remote URL will change every time the app restarts. We are working on adding persistent domain support soon!
+
+