Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment on lines +142 to +143

### Production Build

Expand Down Expand Up @@ -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) |
Comment on lines 266 to +268
| POST | `/api/setup/finish` | Complete setup |

### Dashboard & Telemetry
Expand Down
7 changes: 3 additions & 4 deletions backend/src/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -82,7 +81,7 @@ pub struct ScheduledPowerRequest {

#[allow(dead_code)]
fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(cmd)
let output = crate::new_command(cmd)
.args(args)
.output()
.map_err(|e| format!("Failed to execute {}: {}", cmd, e))?;
Expand All @@ -99,7 +98,7 @@ fn run_powershell(script: &str) -> Result<String, String> {
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())
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 30 additions & 4 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,33 @@ pub struct AppState {
pub terminal_state: Arc<TerminalState>,
pub tunnel_state: Arc<TunnelState>,
pub port: u16,
pub setup_token: Arc<String>,
}

pub fn new_command<S: AsRef<std::ffi::OsStr>>(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<S: AsRef<std::ffi::OsStr>>(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 {
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
70 changes: 24 additions & 46 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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));
}
});
Comment on lines +420 to +434

server_handle.await.expect("Server task panicked");

Expand Down
5 changes: 2 additions & 3 deletions backend/src/network.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -51,7 +50,7 @@ fn default_wifi_security() -> String {
}

fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(cmd)
let output = crate::new_command(cmd)
.args(args)
.output()
.map_err(|e| format!("Failed to execute {}: {}", cmd, e))?;
Expand All @@ -66,7 +65,7 @@ fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
fn run_powershell(script: &str) -> Result<String, String> {
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())
Expand Down
Loading
Loading