Skip to content
Draft
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
6 changes: 6 additions & 0 deletions apps/staged/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,14 @@ just ci-local # Pre-push hook when Staged changed: same checks, frontend and R

# Maintenance
just clean # Remove build artifacts

# Browser / phone access (opt-in HTTPS server on :5175)
just dev-web # Dev mode plus the web server; Vite proxies /api to it
just run-web # Release build serving the production bundle
```

`dev-web` and `run-web` need `STAGED_WEB_CERT_PATH` and `STAGED_WEB_KEY_PATH` (PEM files) and, for `dev-web`, `STAGED_WEB_HOST`. The server is **unauthenticated** and binds `0.0.0.0:5175`: anyone who can reach the port can run every command the desktop window can, including repo actions. Only enable it on a network you trust.

## Architecture

### Backend (Rust / Tauri)
Expand Down
115 changes: 115 additions & 0 deletions apps/staged/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,121 @@ dev repo="": stage-sidecar
{{ if repo != "" { "export STAGED_REPO=" + repo } else { "" } }}
pnpm exec tauri dev --config "$TAURI_CONFIG"

# Run with the HTTPS web server enabled for phone/browser access.
# Requires PEM cert/key files and a hostname covered by the certificate.
# The server is unauthenticated and binds 0.0.0.0:5175: anyone who can reach
# the port can run repo actions. Only use it on a network you trust.
dev-web repo="":
#!/usr/bin/env bash
set -euo pipefail

[[ -d node_modules ]] || pnpm install

if [[ -z "${STAGED_WEB_CERT_PATH:-}" || -z "${STAGED_WEB_KEY_PATH:-}" || -z "${STAGED_WEB_HOST:-}" ]]; then
printf '%s\n' \
'Error: `just dev-web` serves browser access over HTTPS.' \
'Provide PEM certificate/key files and a hostname covered by the certificate:' \
'' \
' STAGED_WEB_CERT_PATH=/path/to/cert.pem \' \
' STAGED_WEB_KEY_PATH=/path/to/key.pem \' \
' STAGED_WEB_HOST=hostname.example.com \' \
' just dev-web' >&2
exit 1
fi

VITE_PORT=$(python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)")
export VITE_PORT
export STAGED_WEB_SERVER=1
TAURI_CONFIG="{\"build\":{\"devUrl\":\"https://${STAGED_WEB_HOST}:${VITE_PORT}\",\"beforeDevCommand\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort --host 0.0.0.0\"}}"

echo "Starting on https://${STAGED_WEB_HOST}:${VITE_PORT} (HTTPS web server on :5175)"
{{ if repo != "" { "export STAGED_REPO=" + repo } else { "" } }}
pnpm exec tauri dev --config "$TAURI_CONFIG"

# Build and run the release app with the HTTPS web server. Requires PEM cert/key files.
# Same warning as `dev-web`: unauthenticated on 0.0.0.0:5175, anyone who can
# reach the port can run repo actions.
run-web:
#!/usr/bin/env bash
set -euo pipefail

# Unlike `dev-web` there is no Vite/HMR: the desktop window and the phone
# get the same built bundle, so this is the path that exercises the service
# worker and the production frontend. One Staged process at a time — a
# `just dev`/`dev-web` instance elsewhere shares ~/.staged and the port.

[[ -d node_modules ]] || pnpm install

if [[ -z "${STAGED_WEB_CERT_PATH:-}" || -z "${STAGED_WEB_KEY_PATH:-}" ]]; then
printf '%s\n' \
'Error: `just run-web` serves browser access over HTTPS.' \
'Provide PEM certificate/key files covering the hostname you browse to:' \
'' \
' STAGED_WEB_CERT_PATH=/path/to/cert.pem \' \
' STAGED_WEB_KEY_PATH=/path/to/key.pem \' \
' just run-web' >&2
exit 1
fi

# `tauri build` runs `pnpm run build` first, so dist/ is always regenerated
# here. The window's assets are compiled into the binary while the server
# reads dist/ from disk — rebuilding both together keeps them in sync.
# `--bundles app` skips the DMG that `just build` would also produce.
pnpm exec tauri build --bundles app

# Cargo resolves a relative CARGO_TARGET_DIR against its own cwd (src-tauri)
TARGET_DIR="$(pwd)/src-tauri/target"
if [[ -n "${CARGO_TARGET_DIR:-}" ]]; then
case "$CARGO_TARGET_DIR" in
/*) TARGET_DIR="$CARGO_TARGET_DIR" ;;
*) TARGET_DIR="$(pwd)/src-tauri/$CARGO_TARGET_DIR" ;;
esac
fi

APP_BIN="$TARGET_DIR/release/bundle/macos/Staged.app/Contents/MacOS/Staged"
if [[ ! -x "$APP_BIN" ]]; then
echo "Error: no app bundle at $APP_BIN" >&2
exit 1
fi

export STAGED_WEB_SERVER=1

# A bundled release binary cannot find dist/ relative to itself, so point
# the server at it directly rather than relying on the launch cwd.
export STAGED_WEB_DIST="$(pwd)/dist"

"$APP_BIN" &
APP_PID=$!
trap 'kill "$APP_PID" 2>/dev/null || true' EXIT

# Release builds install no logger, so web server failures (bad cert path,
# :5175 already bound by another Staged instance) are completely silent.
# Probe the server rather than trusting that the app launched.
STATUS=000
for _ in $(seq 1 30); do
kill -0 "$APP_PID" 2>/dev/null || break
STATUS=$(curl -sk --max-time 2 -o /dev/null -w '%{http_code}' https://localhost:5175/sw.js) || true
[[ "$STATUS" == "000" ]] || break
sleep 1
done

case "$STATUS" in
2*|3*)
echo "Web server ready on https://${STAGED_WEB_HOST:-$(hostname)}:5175"
;;
404)
echo "Web server is up but is serving 404s — check STAGED_WEB_DIST=$STAGED_WEB_DIST holds the built bundle." >&2
;;
*)
printf '%s\n' \
'Web server never came up on :5175. Release builds log nothing, so check:' \
' - STAGED_WEB_CERT_PATH / STAGED_WEB_KEY_PATH point at readable PEM files' \
' - no other Staged instance already holds the port' >&2
;;
esac

wait "$APP_PID" || true

# Build the app for production
build:
pnpm run tauri:build
Expand Down
24 changes: 0 additions & 24 deletions apps/staged/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions apps/staged/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ tauri-plugin-store = "2.5.0"
# MCP server for project sessions
rmcp = { version = "3.5", features = ["server", "transport-streamable-http-server"] }
axum = { version = "0.8", features = ["ws", "http2"] }
axum-extra = { version = "0.12", features = ["cookie"] }
tower-http = { version = "0.7", features = ["fs", "cors", "compression-br", "compression-gzip"] }
tower-http = { version = "0.7", features = ["fs", "compression-br", "compression-gzip"] }
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "std", "tls12"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] }
# Random tokens (web_server::generate_token, used by the control server) and
# constant-time secret comparison in control_server; rand also backs the
# rate-limit scheduler jitter.
rand = "0.10"
hex = "0.4"
subtle = "2.6"
time = "0.3"
pikchr = "0.1.4"
Expand All @@ -81,9 +82,11 @@ resvg = "0.49"
usvg = "0.49"
tiny-skia = "0.12"

# Managed Node.js runtime: tarball extraction for runtime installs
# Managed Node.js runtime: tarball extraction and checksum verification for
# runtime installs
tar = "0.4"
flate2 = "1"
hex = "0.4"

# Note image dimensions (note_media::dimensions): header-only pixel size for
# every stored raster format, and the PNG pHYs chunk. Both are already in the
Expand Down
13 changes: 6 additions & 7 deletions apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2213,21 +2213,20 @@ pub fn run() {
project_transfer::prune_backups(transfer_store.as_deref());
});

// Web server startup is stubbed out in this build.
// TODO(web): restore web server startup from the `mobile-web` branch.
// Start the Axum web server only when opted in via environment variable.
// Once enabled it binds 0.0.0.0:5175 with TLS and no authentication:
// anyone who can reach the port can run every command the desktop
// window can, including repo and branch actions. The only boundary
// is network reachability, so keep it off unless browser access from
// a trusted network is wanted.
let web_server_enabled = std::env::var("STAGED_WEB_SERVER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);

if web_server_enabled {
let auth_token = web_server::generate_token();
web_server::start(web_server::WebAppState {
app_handle: app.handle().clone(),
event_tx,
auth_token,
sessions: std::sync::Arc::new(std::sync::Mutex::new(
std::collections::HashSet::new(),
)),
});
}

Expand Down
Loading
Loading