Add tunnel mode and enhance servers UI

Add ngrok 'tunnel' mode to dev scripts and documentation, plus a large UI overhaul for the Servers pages.

- Add .gitattributes and new scripts/setup-wsl.sh helper.
- Extend dev.ps1 and dev.sh with a new 'tunnel' mode that starts backend+frontend and launches an ngrok tunnel (supports NGROK_DOMAIN, NGROK_AUTHTOKEN, and sets sensible CORS_ORIGINS). Includes polling of ngrok API, process cleanup, and helpful error messages.
- Update docs/LOCAL_DEVELOPMENT.md with an "Exposing Your Local Control Plane (ngrok)" guide and WSL setup notes.
- Revamp frontend pages: ServerDetail.jsx and Servers.jsx refactored with new header layouts, status/avatars, status pills, telemetry rows, selectable table rows, bulk actions, confirm dialogs, improved add-server modal, new icons (FolderTinyIcon, MoreIcon), utilities (formatLastSeen, clamp), and accessibility/interaction improvements (menus, selection, copy install command).
- Update servers SCSS to support the new layouts and loading states.

These changes add a developer-friendly tunnel mode for remote testing and significantly improve the server management UX in the frontend.
This commit is contained in:
Juan Denis committed 2026-04-29 02:56:23 -04:00
1 parent e9d8af241a
commit e25cf6f83e
8 files changed
+2504 -345

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
* text=auto eol=lf
*.sh text eol=lf
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
+124 -3
View File
@@ -3,19 +3,25 @@
.SYNOPSIS
ServerKit development launcher and validation tool.
.DESCRIPTION
Start backend, frontend, or both. Run validation checks.
Start backend, frontend, both, or expose backend via an ngrok tunnel.
Run validation checks.
.PARAMETER Mode
Operation mode: start (default), backend, frontend, validate
Operation mode: start (default), backend, frontend, tunnel, validate
.EXAMPLE
.\dev.ps1 # Start backend + frontend
.\dev.ps1 backend # Backend only
.\dev.ps1 frontend # Frontend only
.\dev.ps1 tunnel # Backend + frontend + ngrok tunnel for the backend
.\dev.ps1 validate # Run all linters/checks
.NOTES
Optional environment variables for tunnel mode:
NGROK_DOMAIN - reserved ngrok domain (e.g. my-app.ngrok-free.app)
NGROK_AUTHTOKEN - ngrok authtoken (alternatively run `ngrok config add-authtoken`)
#>
param(
[Parameter(Position = 0)]
[ValidateSet('start', 'backend', 'frontend', 'validate')]
[ValidateSet('start', 'backend', 'frontend', 'tunnel', 'validate')]
[string]$Mode = 'start'
)
@@ -120,6 +126,120 @@ function Start-Both {
}
}
function Start-Tunnel {
if (-not (Get-Command ngrok -ErrorAction SilentlyContinue)) {
Write-Host "Error: ngrok is not installed or not in PATH." -ForegroundColor Red
Write-Host ""
Write-Host "Install ngrok:"
Write-Host " - https://ngrok.com/download"
Write-Host " - Windows: choco install ngrok (or scoop install ngrok)"
Write-Host " - WSL: snap install ngrok (or download the .tgz)"
Write-Host ""
Write-Host "Then authenticate once: ngrok config add-authtoken <YOUR_TOKEN>"
exit 1
}
Write-Host ""
Write-Host "ServerKit Dev Server (with ngrok tunnel)" -ForegroundColor Cyan
Write-Host " Backend: http://localhost:5000"
Write-Host " Frontend: http://localhost:5173"
Write-Host " Tunnel: exposing backend (port 5000) via ngrok"
Write-Host ""
Write-Host "NOTE: " -ForegroundColor Yellow -NoNewline
Write-Host "Agents and remote callers should use the public ngrok URL"
Write-Host " printed below as their --server / control plane URL."
Write-Host ""
if (-not $env:CORS_ORIGINS) {
$env:CORS_ORIGINS = 'http://localhost:5173,http://localhost:5000,https://*.ngrok-free.app,https://*.ngrok.app,https://*.ngrok.io'
}
$envSnapshot = @{
CORS_ORIGINS = $env:CORS_ORIGINS
}
$backendJob = Start-Job -ScriptBlock {
param($dir, $envVars)
foreach ($k in $envVars.Keys) { Set-Item -Path "Env:$k" -Value $envVars[$k] }
Set-Location $dir
if (Test-Path 'venv\Scripts\Activate.ps1') {
& 'venv\Scripts\Activate.ps1'
}
python run.py
} -ArgumentList $BackendDir, $envSnapshot
Start-Sleep -Seconds 2
$frontendJob = Start-Job -ScriptBlock {
param($dir)
Set-Location $dir
npm run dev
} -ArgumentList $FrontendDir
Start-Sleep -Seconds 1
$ngrokArgs = @('http', '5000', '--log=stdout')
if ($env:NGROK_DOMAIN) { $ngrokArgs += "--domain=$($env:NGROK_DOMAIN)" }
if ($env:NGROK_AUTHTOKEN) { $ngrokArgs += "--authtoken=$($env:NGROK_AUTHTOKEN)" }
$ngrokProc = Start-Process -FilePath 'ngrok' -ArgumentList $ngrokArgs -PassThru -WindowStyle Hidden
$urlPrinted = $false
try {
Write-Host "Press Ctrl+C to stop..." -ForegroundColor DarkGray
$polls = 0
while ($true) {
Receive-Job $backendJob -ErrorAction SilentlyContinue
Receive-Job $frontendJob -ErrorAction SilentlyContinue
if (-not $urlPrinted -and $polls -lt 30) {
try {
$resp = Invoke-RestMethod -Uri 'http://127.0.0.1:4040/api/tunnels' -TimeoutSec 1 -ErrorAction Stop
$publicUrl = ($resp.tunnels | Where-Object { $_.public_url -like 'https://*' } | Select-Object -First 1).public_url
if ($publicUrl) {
Write-Host ""
Write-Host "=========================================================" -ForegroundColor Green
Write-Host " Public tunnel URL: " -ForegroundColor Green -NoNewline
Write-Host $publicUrl -ForegroundColor Cyan
Write-Host " Use this as your agent --server / control plane URL." -ForegroundColor Green
Write-Host "=========================================================" -ForegroundColor Green
Write-Host ""
$urlPrinted = $true
}
}
catch { }
$polls++
}
if ($backendJob.State -eq 'Failed') {
Write-Host "Backend crashed!" -ForegroundColor Red
Receive-Job $backendJob
break
}
if ($frontendJob.State -eq 'Failed') {
Write-Host "Frontend crashed!" -ForegroundColor Red
Receive-Job $frontendJob
break
}
if ($ngrokProc.HasExited) {
Write-Host "ngrok exited." -ForegroundColor Red
break
}
Start-Sleep -Seconds 1
}
}
finally {
Stop-Job $backendJob -ErrorAction SilentlyContinue
Stop-Job $frontendJob -ErrorAction SilentlyContinue
Remove-Job $backendJob -Force -ErrorAction SilentlyContinue
Remove-Job $frontendJob -Force -ErrorAction SilentlyContinue
if ($ngrokProc -and -not $ngrokProc.HasExited) {
Stop-Process -Id $ngrokProc.Id -ErrorAction SilentlyContinue
}
Write-Host "`nStopped." -ForegroundColor Yellow
}
}
function Invoke-Check {
param(
[string]$Name,
@@ -234,6 +354,7 @@ function Run-ValidateWatch {
switch ($Mode) {
'backend' { Start-Backend }
'frontend' { Start-Frontend }
'tunnel' { Start-Tunnel }
'validate' { Run-ValidateWatch }
default { Start-Both }
}
+94
View File
@@ -5,7 +5,12 @@
# ./dev.sh Start backend + frontend (default)
# ./dev.sh backend Backend only
# ./dev.sh frontend Frontend only
# ./dev.sh tunnel Start backend + frontend + expose backend via ngrok
# ./dev.sh validate Run all linters/checks
#
# Environment variables:
# NGROK_DOMAIN Optional reserved ngrok domain (e.g. my-app.ngrok-free.app)
# NGROK_AUTHTOKEN Optional ngrok authtoken (alternatively run `ngrok config add-authtoken`)
set -euo pipefail
@@ -83,6 +88,94 @@ start_both() {
wait
}
start_tunnel() {
if ! command -v ngrok &>/dev/null; then
echo -e "${RED}Error:${NC} ngrok is not installed or not in PATH."
echo ""
echo "Install ngrok:"
echo " - https://ngrok.com/download"
echo " - macOS: brew install ngrok/ngrok/ngrok"
echo " - Linux: snap install ngrok (or download the .tgz)"
echo " - Windows: choco install ngrok (or scoop install ngrok)"
echo ""
echo "Then authenticate once: ngrok config add-authtoken <YOUR_TOKEN>"
exit 1
fi
echo ""
echo -e "${CYAN}ServerKit Dev Server (with ngrok tunnel)${NC}"
echo " Backend: http://localhost:5000"
echo " Frontend: http://localhost:5173"
echo " Tunnel: exposing backend (port 5000) via ngrok"
echo ""
echo -e "${YELLOW}NOTE:${NC} Agents and remote callers should use the public ngrok URL"
echo " printed below as their --server / control plane URL."
echo ""
# Allow CORS from any ngrok domain by default unless caller already set it.
export CORS_ORIGINS="${CORS_ORIGINS:-http://localhost:5173,http://localhost:5000,https://*.ngrok-free.app,https://*.ngrok.app,https://*.ngrok.io}"
cd "$BACKEND_DIR"
if [ -f venv/bin/activate ]; then
source venv/bin/activate
fi
python run.py &
BACKEND_PID=$!
sleep 2
cd "$FRONTEND_DIR"
npm run dev &
FRONTEND_PID=$!
sleep 1
NGROK_ARGS=(http 5000 --log=stdout)
if [ -n "${NGROK_DOMAIN:-}" ]; then
NGROK_ARGS+=("--domain=$NGROK_DOMAIN")
fi
if [ -n "${NGROK_AUTHTOKEN:-}" ]; then
NGROK_ARGS+=("--authtoken=$NGROK_AUTHTOKEN")
fi
ngrok "${NGROK_ARGS[@]}" &
NGROK_PID=$!
# Poll the local ngrok API for the public URL and print it prominently.
(
for _ in $(seq 1 20); do
sleep 1
if command -v curl &>/dev/null; then
URL=$(curl -fsS http://127.0.0.1:4040/api/tunnels 2>/dev/null \
| grep -oE '"public_url":"https://[^"]+' \
| head -n1 \
| sed 's/"public_url":"//')
if [ -n "$URL" ]; then
echo ""
echo -e "${GREEN}=========================================================${NC}"
echo -e "${GREEN} Public tunnel URL: ${NC}${CYAN}$URL${NC}"
echo -e "${GREEN} Use this as your agent --server / control plane URL.${NC}"
echo -e "${GREEN}=========================================================${NC}"
echo ""
break
fi
fi
done
) &
cleanup_tunnel() {
echo ""
echo -e "${YELLOW}Stopping...${NC}"
kill "$BACKEND_PID" "$FRONTEND_PID" "$NGROK_PID" 2>/dev/null || true
wait "$BACKEND_PID" "$FRONTEND_PID" "$NGROK_PID" 2>/dev/null || true
echo "Stopped."
}
trap cleanup_tunnel INT TERM
echo -e "${DIM}Press Ctrl+C to stop...${NC}"
wait
}
run_validate() {
header "ServerKit Validation Suite"
local failed=0
@@ -173,6 +266,7 @@ MODE="${1:-start}"
case "$MODE" in
backend) start_backend ;;
frontend) start_frontend ;;
tunnel) start_tunnel ;;
validate) run_validate_watch ;;
start|*) start_both ;;
esac
+87
View File
@@ -252,3 +252,90 @@ sudo usermod -aG docker $USER
# Remove the lock file
rm backend/instance/serverkit.db-journal
```
---
## Exposing Your Local Control Plane (ngrok)
When developing locally on Windows/WSL you may want a remote ServerKit agent (or
a Docker host on another machine) to connect back to your machine. The `dev.sh`
and `dev.ps1` scripts include a `tunnel` mode that starts the backend, frontend,
and an ngrok tunnel pointed at the backend (port 5000).
### Prerequisites
1. Install ngrok and authenticate once:
```bash
# macOS / Linux / WSL
brew install ngrok/ngrok/ngrok # or: snap install ngrok
ngrok config add-authtoken <YOUR_TOKEN>
```
```powershell
# Windows
choco install ngrok # or: scoop install ngrok
ngrok config add-authtoken <YOUR_TOKEN>
```
2. (Optional) Reserve a domain in the ngrok dashboard so the URL stays stable
across restarts.
### Start with a tunnel
```bash
# Linux / macOS / WSL
./dev.sh tunnel
```
```powershell
# Windows
.\dev.ps1 tunnel
```
The public ngrok URL is printed in green once the tunnel is up. Use it as the
`--server` argument when registering an agent:
```bash
serverkit-agent register \
--token "sk_reg_xxx" \
--server "https://<your-tunnel>.ngrok-free.app"
```
### Optional environment variables
| Variable | Purpose |
|----------|---------|
| `NGROK_DOMAIN` | Use a reserved ngrok domain (e.g. `my-app.ngrok-free.app`). |
| `NGROK_AUTHTOKEN` | Override the authtoken (otherwise the one stored by `ngrok config` is used). |
| `CORS_ORIGINS` | Override CORS allow-list. The script defaults include `https://*.ngrok-free.app`, `https://*.ngrok.app`, `https://*.ngrok.io`. |
---
## Quick Start with WSL2 (Helper Script)
If you're on Windows and using WSL2 Ubuntu/Debian, you can skip the manual
steps above and use the bundled setup helper:
```bash
# from inside WSL, in the cloned repo
bash scripts/setup-wsl.sh # full setup
bash scripts/setup-wsl.sh --check # dry-run / diagnostics only
```
The script will:
- Detect WSL and warn if the repo lives under `/mnt/c` (slow file I/O for hot-reload).
- Verify Ubuntu/Debian and install missing apt packages
(`python3`, `python3-venv`, `python3-pip`, `nodejs`, `npm`, `git`, `curl`).
- Create `backend/venv` and install Python requirements.
- Run `npm install` in `frontend/`.
- Copy `backend/.env.example` to `backend/.env` if missing.
- Print WSL-specific tips (localhost forwarding, polling reloader, ngrok).
Once it finishes:
```bash
./dev.sh # start backend + frontend
./dev.sh tunnel # also expose backend via ngrok (see section above)
```
+46 -27
View File
@@ -152,13 +152,6 @@ const ServerDetail = () => {
);
}
const statusColors = {
online: '#10B981',
offline: '#EF4444',
connecting: '#F59E0B',
pending: '#6B7280'
};
const tabs = [
{ id: 'overview', label: 'Overview' },
{ id: 'docker', label: 'Docker' },
@@ -168,35 +161,55 @@ const ServerDetail = () => {
return (
<div className="server-detail-page">
<div className="page-header">
<div className="page-breadcrumb">
<Link to="/servers">Servers</Link>
<span className="breadcrumb-separator">/</span>
<span>{server.name}</span>
</div>
<div className="page-header-content">
<div className="server-title">
<div
className="status-dot large"
style={{ backgroundColor: statusColors[server.status] }}
title={server.status}
/>
<h1>{server.name}</h1>
<div className="page-breadcrumb">
<Link to="/servers">Servers</Link>
<span className="breadcrumb-separator">/</span>
<span>{server.name}</span>
</div>
<header className="server-detail-header">
<div className="server-detail-header__main">
<div className={`server-detail-header__avatar server-detail-header__avatar--${server.status || 'pending'}`}>
{(server.name || '?').charAt(0).toUpperCase()}
</div>
<div className="server-detail-header__identity">
<div className="server-detail-header__title-row">
<h1>{server.name}</h1>
<span className={`status-pill status-pill--${server.status || 'pending'}`}>
<span className="status-pill__dot" />
{server.status || 'pending'}
</span>
</div>
<div className="server-detail-header__meta">
<span className="mono">{server.hostname || server.ip_address || 'No endpoint configured'}</span>
{server.group_name && (
<span className="server-detail-header__chip"><FolderTinyIcon /> {server.group_name}</span>
)}
{server.os_type && <span className="server-detail-header__chip">{server.os_type}</span>}
{server.agent_version && <span className="server-detail-header__chip">agent {server.agent_version}</span>}
{server.last_seen && (
<span className="server-detail-header__chip">
last seen {new Date(server.last_seen).toLocaleString()}
</span>
)}
</div>
{server.description && (
<p className="server-detail-header__description">{server.description}</p>
)}
</div>
<p className="page-description">
{server.hostname || server.ip_address}
{server.description && ` - ${server.description}`}
</p>
</div>
<div className="page-header-actions">
<div className="server-detail-header__actions">
<button className="btn btn-secondary" onClick={handlePingServer}>
<RefreshIcon /> Ping
</button>
<Link className="btn btn-secondary" to={`/servers/${id}/docker`}>
Docker
</Link>
<button className="btn btn-primary" onClick={handleGenerateToken}>
<KeyIcon /> Generate Token
</button>
</div>
</div>
</header>
<div className="server-detail-tabs">
{tabs.map(t => (
@@ -1181,6 +1194,12 @@ Install-ServerKitAgent -Server "${window.location.origin}" -Token "${token}"` :
};
// Icons
const FolderTinyIcon = () => (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
);
const RefreshIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="23 4 23 10 17 10"/>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env bash
# ServerKit WSL setup helper.
#
# One-shot installer for Windows users running ServerKit dev inside
# WSL2 (Ubuntu/Debian). Installs system packages, creates the Python
# venv, installs npm dependencies, prepares .env files, and verifies
# common WSL pitfalls (filesystem location, localhost binding,
# polling-based reloader).
#
# Usage:
# bash scripts/setup-wsl.sh # full setup
# bash scripts/setup-wsl.sh --check # diagnostics only, no install
#
# After this finishes:
# ./dev.sh # start backend + frontend
# ./dev.sh tunnel # also expose backend through ngrok
set -euo pipefail
CYAN='\033[0;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
DIM='\033[2m'
NC='\033[0m'
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BACKEND_DIR="$PROJECT_ROOT/backend"
FRONTEND_DIR="$PROJECT_ROOT/frontend"
CHECK_ONLY=0
for arg in "$@"; do
case "$arg" in
--check|-c) CHECK_ONLY=1 ;;
--help|-h)
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*) echo -e "${RED}Unknown argument:${NC} $arg"; exit 2 ;;
esac
done
header() { echo ""; echo -e "${CYAN}=== $1 ===${NC}"; }
ok() { echo -e " ${GREEN}OK${NC} $1"; }
warn() { echo -e " ${YELLOW}WARN${NC} $1"; }
fail() { echo -e " ${RED}FAIL${NC} $1"; }
info() { echo -e " ${DIM}$1${NC}"; }
# -----------------------------------------------------------------------------
# Diagnostics
# -----------------------------------------------------------------------------
header "Environment diagnostics"
IN_WSL=0
if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then
IN_WSL=1
ok "Running inside WSL ($(uname -r))"
else
warn "Not detected as WSL. This script is intended for WSL2 but will still try to run."
fi
if [ "$IN_WSL" -eq 1 ]; then
case "$PROJECT_ROOT" in
/mnt/*)
warn "Repo is on the Windows filesystem ($PROJECT_ROOT)."
info "File I/O and Vite/Flask reloaders are noticeably slower here."
info "For best performance clone the repo into the Linux home, e.g.:"
info " cd ~ && git clone https://github.com/jhd3197/ServerKit.git"
;;
*)
ok "Repo is on the Linux filesystem ($PROJECT_ROOT)."
;;
esac
fi
# Distro detection
DISTRO_ID="unknown"
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO_ID="$ID"
ok "Distro: $PRETTY_NAME"
fi
if [ "$DISTRO_ID" != "ubuntu" ] && [ "$DISTRO_ID" != "debian" ]; then
warn "This script targets Ubuntu/Debian. Other distros may need manual adjustments."
fi
# -----------------------------------------------------------------------------
# Required tools
# -----------------------------------------------------------------------------
header "Required tooling"
NEED_APT=()
need() {
local cmd="$1" pkg="$2"
if command -v "$cmd" &>/dev/null; then
ok "$cmd ($(command -v "$cmd"))"
else
warn "$cmd not found (needs apt package: $pkg)"
NEED_APT+=("$pkg")
fi
}
need python3 python3
need pip3 python3-pip
need node nodejs
need npm npm
need git git
need curl curl
# venv module
if python3 -c 'import venv' 2>/dev/null; then
ok "python3-venv module available"
else
warn "python3-venv module missing"
NEED_APT+=("python3-venv")
fi
# Node version sanity
if command -v node &>/dev/null; then
NODE_MAJOR="$(node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/')"
if [ -n "$NODE_MAJOR" ] && [ "$NODE_MAJOR" -lt 18 ]; then
warn "Node.js version is $(node -v); ServerKit expects 18+ (20+ recommended)."
info "Upgrade with NodeSource: https://github.com/nodesource/distributions"
fi
fi
# -----------------------------------------------------------------------------
# Install missing apt packages
# -----------------------------------------------------------------------------
if [ "${#NEED_APT[@]}" -gt 0 ]; then
if [ "$CHECK_ONLY" -eq 1 ]; then
warn "Missing packages (would install): ${NEED_APT[*]}"
else
header "Installing missing apt packages"
info "Running: sudo apt update && sudo apt install -y ${NEED_APT[*]}"
sudo apt update
sudo apt install -y "${NEED_APT[@]}"
fi
fi
# -----------------------------------------------------------------------------
# Backend venv + requirements
# -----------------------------------------------------------------------------
header "Backend (Python venv)"
if [ ! -d "$BACKEND_DIR" ]; then
fail "backend directory not found at $BACKEND_DIR"
exit 1
fi
if [ "$CHECK_ONLY" -eq 1 ]; then
if [ -d "$BACKEND_DIR/venv" ]; then
ok "venv exists at backend/venv"
else
warn "venv not yet created (run without --check to create it)"
fi
else
if [ ! -d "$BACKEND_DIR/venv" ]; then
info "Creating venv in backend/venv"
python3 -m venv "$BACKEND_DIR/venv"
fi
# shellcheck disable=SC1091
source "$BACKEND_DIR/venv/bin/activate"
info "Upgrading pip"
pip install --quiet --upgrade pip
info "Installing backend requirements"
pip install --quiet -r "$BACKEND_DIR/requirements.txt"
deactivate
ok "Backend dependencies installed"
fi
# .env
if [ ! -f "$BACKEND_DIR/.env" ] && [ -f "$BACKEND_DIR/.env.example" ]; then
if [ "$CHECK_ONLY" -eq 1 ]; then
warn "backend/.env missing (would copy from .env.example)"
else
cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env"
ok "Created backend/.env from .env.example"
info "Edit backend/.env to set SECRET_KEY / JWT_SECRET_KEY before any production-like use."
fi
else
[ -f "$BACKEND_DIR/.env" ] && ok "backend/.env present"
fi
# -----------------------------------------------------------------------------
# Frontend npm install
# -----------------------------------------------------------------------------
header "Frontend (npm)"
if [ ! -d "$FRONTEND_DIR" ]; then
fail "frontend directory not found at $FRONTEND_DIR"
exit 1
fi
if [ "$CHECK_ONLY" -eq 1 ]; then
if [ -d "$FRONTEND_DIR/node_modules" ]; then
ok "node_modules present"
else
warn "node_modules missing (would run npm install)"
fi
else
if [ ! -d "$FRONTEND_DIR/node_modules" ]; then
info "Running npm install in frontend/"
(cd "$FRONTEND_DIR" && npm install)
else
info "node_modules already present (skipping npm install)"
fi
ok "Frontend dependencies ready"
fi
# -----------------------------------------------------------------------------
# WSL-specific advice
# -----------------------------------------------------------------------------
if [ "$IN_WSL" -eq 1 ]; then
header "WSL tips"
info "Backend already binds to 0.0.0.0 and uses a stat-based reloader (good for WSL)."
info "Vite dev server is reachable from Windows at http://localhost:5173 thanks to WSL2 localhost forwarding."
info "If hot-reload feels stuck, ensure the repo lives under your Linux home (~) rather than /mnt/c."
if command -v ngrok &>/dev/null; then
ok "ngrok is installed (use ./dev.sh tunnel to expose the backend)"
else
info "Install ngrok and then ./dev.sh tunnel will expose the backend so remote agents can connect."
fi
fi
# -----------------------------------------------------------------------------
# Done
# -----------------------------------------------------------------------------
header "Done"
if [ "$CHECK_ONLY" -eq 1 ]; then
echo "Diagnostics complete. Re-run without --check to install anything missing."
else
echo -e " ${GREEN}Setup complete.${NC}"
echo ""
echo "Next steps:"
echo " 1. (Optional) edit backend/.env to set unique SECRET_KEY / JWT_SECRET_KEY"
echo " 2. ./dev.sh # start backend + frontend"
echo " 3. ./dev.sh tunnel # also expose backend via ngrok"
fi