Add dev launcher and update dev docs
- Introduce dev.sh as the local backend + Vite launcher - Document the separate backend/frontend development flow - Note that the dev Gunicorn config restarts Python on file changes - Note that Vite hot reloads React changes in webui
This commit is contained in:
parent
d1e95a0558
commit
018a554f35
4 changed files with 309 additions and 9 deletions
45
README.md
45
README.md
|
|
@ -278,19 +278,38 @@ The template points at `boulderbadgedad/soulsync:latest` (stable) by default. To
|
|||
```bash
|
||||
git clone https://github.com/Nezreka/SoulSync
|
||||
cd SoulSync
|
||||
pip install -r requirements.txt
|
||||
python -m pip install -r requirements.txt
|
||||
gunicorn -c gunicorn.conf.py wsgi:application
|
||||
# Open http://localhost:8008
|
||||
```
|
||||
|
||||
For local development and tests:
|
||||
### Local Development
|
||||
|
||||
Use two terminals so the backend and Vite stay independent:
|
||||
|
||||
1. Backend
|
||||
```bash
|
||||
python -m pip install -r requirements-dev.txt
|
||||
gunicorn -c gunicorn.dev.conf.py wsgi:application
|
||||
```
|
||||
The dev Gunicorn config watches backend files and restarts the Python server when they change.
|
||||
2. Frontend
|
||||
```bash
|
||||
cd webui
|
||||
npm ci
|
||||
npm run dev -- --host 127.0.0.1 --port 5173
|
||||
```
|
||||
Vite hot reloads the React side when you change webui files.
|
||||
|
||||
Run tests separately when needed:
|
||||
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
pytest
|
||||
gunicorn -c gunicorn.dev.conf.py wsgi:application
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
If you want a convenience launcher, `./dev.sh` starts both halves together.
|
||||
It is most useful on Linux, macOS, and WSL.
|
||||
|
||||
---
|
||||
|
||||
## Setup Guide
|
||||
|
|
@ -413,18 +432,28 @@ SoulSync uses a `dev` → `main` flow:
|
|||
2. Branch off `dev`: `git checkout -b fix/your-change dev`
|
||||
3. Make your changes and commit
|
||||
4. Push and open a PR against **`dev`** (not `main`)
|
||||
5. CI (`build-and-test.yml`) runs ruff lint + compile + pytest on your branch — wait for green
|
||||
5. CI (`build-and-test.yml`) runs ruff lint + compile + `python -m pytest` on your branch — wait for green
|
||||
6. A maintainer reviews and merges
|
||||
|
||||
### Running locally
|
||||
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
python -m pip install -r requirements-dev.txt
|
||||
python -m ruff check . # must be 0 errors
|
||||
python -m pytest # all tests must pass
|
||||
gunicorn -c gunicorn.dev.conf.py wsgi:application
|
||||
```
|
||||
|
||||
For web UI development, keep the backend and Vite dev server in separate terminals:
|
||||
|
||||
```bash
|
||||
gunicorn -c gunicorn.dev.conf.py wsgi:application
|
||||
cd webui
|
||||
npm install
|
||||
npm run dev -- --host 127.0.0.1 --port 5173
|
||||
```
|
||||
|
||||
If you want a convenience wrapper, `./dev.sh` starts both halves together.
|
||||
|
||||
Ruff config lives in `pyproject.toml`. The ruleset is intentionally lenient — it catches real bugs (undefined names, import shadowing, closure-in-loop) without style nits.
|
||||
|
||||
### Reporting bugs / requesting features
|
||||
|
|
|
|||
235
dev.sh
Executable file
235
dev.sh
Executable file
|
|
@ -0,0 +1,235 @@
|
|||
#!/bin/bash
|
||||
# SoulSync Development Launcher Script
|
||||
# Starts the Python backend and Vite dev server together for local work.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
mkdir -p "$SCRIPT_DIR/logs"
|
||||
|
||||
DEV_GUNICORN_CONFIG="$SCRIPT_DIR/gunicorn.dev.conf.py"
|
||||
GUNICORN_CONFIG="$DEV_GUNICORN_CONFIG"
|
||||
|
||||
VITE_URL="${SOULSYNC_WEBUI_VITE_URL:-http://127.0.0.1:5173}"
|
||||
VITE_LOG_FILE="${SOULSYNC_WEBUI_VITE_LOG:-$SCRIPT_DIR/logs/webui-vite.log}"
|
||||
|
||||
VITE_PID=""
|
||||
SERVER_PID=""
|
||||
SHUTTING_DOWN="0"
|
||||
SHUTDOWN_GRACE_SECONDS="${SOULSYNC_SHUTDOWN_GRACE_SECONDS:-10}"
|
||||
FORCE_KILL_ON_SHUTDOWN="${SOULSYNC_FORCE_KILL_ON_SHUTDOWN:-1}"
|
||||
|
||||
stop_process_group() {
|
||||
local pid="$1"
|
||||
local label="$2"
|
||||
|
||||
if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Stopping ${label}..."
|
||||
kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
|
||||
|
||||
local max_checks=$((SHUTDOWN_GRACE_SECONDS * 10))
|
||||
for _ in $(seq 1 "$max_checks"); do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
if [[ "$FORCE_KILL_ON_SHUTDOWN" != "1" ]]; then
|
||||
echo "${label} did not exit in time; skipping forced kill for this test run."
|
||||
wait "$pid" 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
echo "${label} did not exit in time; forcing shutdown..."
|
||||
kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
wait "$pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ "$SHUTTING_DOWN" == "1" ]]; then
|
||||
return
|
||||
fi
|
||||
SHUTTING_DOWN="1"
|
||||
|
||||
stop_process_group "${SERVER_PID}" "SoulSync web server"
|
||||
stop_process_group "${VITE_PID}" "Vite dev server"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
start_in_own_session() {
|
||||
local pid_file="$1"
|
||||
shift
|
||||
|
||||
local log_file=""
|
||||
if [[ "${1:-}" == "--log-file" ]]; then
|
||||
log_file="$2"
|
||||
shift 2
|
||||
fi
|
||||
|
||||
python3 - "$pid_file" "$log_file" "$@" <<'PY'
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
pid_file = sys.argv[1]
|
||||
log_file = sys.argv[2]
|
||||
cmd = sys.argv[3:]
|
||||
stdout = None
|
||||
stderr = None
|
||||
log_handle = None
|
||||
|
||||
if log_file:
|
||||
log_handle = open(log_file, "ab")
|
||||
stdout = log_handle
|
||||
stderr = log_handle
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(cmd, start_new_session=True, stdout=stdout, stderr=stderr)
|
||||
with open(pid_file, "w", encoding="utf-8") as pid_handle:
|
||||
pid_handle.write(str(process.pid))
|
||||
finally:
|
||||
if log_handle is not None:
|
||||
log_handle.close()
|
||||
PY
|
||||
}
|
||||
|
||||
start_server() {
|
||||
echo "Starting SoulSync web server..."
|
||||
echo "Using Gunicorn config: ${GUNICORN_CONFIG}"
|
||||
local pid_file
|
||||
pid_file="$(mktemp "$SCRIPT_DIR/logs/.gunicorn-pid.XXXXXX")"
|
||||
start_in_own_session "$pid_file" gunicorn -c "${GUNICORN_CONFIG}" wsgi:application
|
||||
SERVER_PID="$(<"$pid_file")"
|
||||
rm -f "$pid_file"
|
||||
}
|
||||
|
||||
stop_server() {
|
||||
stop_process_group "${SERVER_PID}" "SoulSync web server"
|
||||
SERVER_PID=""
|
||||
}
|
||||
|
||||
compute_backend_watch_state() {
|
||||
python3 - "$SCRIPT_DIR" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
excluded_dirs = {
|
||||
root / '.git',
|
||||
root / 'logs',
|
||||
root / 'webui' / 'node_modules',
|
||||
root / 'webui' / 'static' / 'dist',
|
||||
}
|
||||
included_suffixes = {'.py', '.html', '.jinja', '.jinja2'}
|
||||
rows = []
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
current_dir = Path(dirpath)
|
||||
if current_dir in excluded_dirs:
|
||||
dirnames[:] = []
|
||||
continue
|
||||
if any(part == '__pycache__' for part in current_dir.parts):
|
||||
dirnames[:] = []
|
||||
continue
|
||||
|
||||
dirnames[:] = [
|
||||
name
|
||||
for name in dirnames
|
||||
if (current_dir / name) not in excluded_dirs and name != '__pycache__'
|
||||
]
|
||||
|
||||
for filename in filenames:
|
||||
path = current_dir / filename
|
||||
if any(part == '__pycache__' for part in path.parts):
|
||||
continue
|
||||
if path.suffix not in included_suffixes:
|
||||
continue
|
||||
try:
|
||||
stat = path.stat()
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
rows.append(f'{stat.st_mtime_ns} {path}')
|
||||
|
||||
for row in sorted(rows):
|
||||
print(row)
|
||||
PY
|
||||
}
|
||||
|
||||
watch_and_run_server() {
|
||||
local last_state=""
|
||||
local current_state=""
|
||||
|
||||
last_state="$(compute_backend_watch_state)"
|
||||
start_server
|
||||
|
||||
while true; do
|
||||
sleep 1
|
||||
|
||||
if [[ "$SHUTTING_DOWN" == "1" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "${SERVER_PID}" ]] && ! kill -0 "${SERVER_PID}" 2>/dev/null; then
|
||||
echo "SoulSync web server exited. Restarting..."
|
||||
start_server
|
||||
last_state="$(compute_backend_watch_state)"
|
||||
continue
|
||||
fi
|
||||
|
||||
current_state="$(compute_backend_watch_state)"
|
||||
if [[ "$current_state" != "$last_state" ]]; then
|
||||
echo "Detected backend file changes. Restarting SoulSync web server..."
|
||||
last_state="$current_state"
|
||||
stop_server
|
||||
start_server
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [[ ! -d "$SCRIPT_DIR/webui/node_modules" ]]; then
|
||||
echo "webui/node_modules is missing."
|
||||
echo "Run: cd webui && npm install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting Vite dev server at ${VITE_URL}..."
|
||||
mkdir -p "$(dirname "$VITE_LOG_FILE")"
|
||||
VITE_PID_FILE="$(mktemp "$SCRIPT_DIR/logs/.vite-pid.XXXXXX")"
|
||||
start_in_own_session "$VITE_PID_FILE" --log-file "$VITE_LOG_FILE" npm --prefix "$SCRIPT_DIR/webui" run dev -- --host 127.0.0.1 --port 5173
|
||||
VITE_PID="$(<"$VITE_PID_FILE")"
|
||||
rm -f "$VITE_PID_FILE"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
READY_URL="${VITE_URL}/static/dist/@vite/client"
|
||||
vite_ready="0"
|
||||
for _ in {1..50}; do
|
||||
if ! kill -0 "${VITE_PID}" 2>/dev/null; then
|
||||
echo "Warning: Vite dev server exited before it became ready."
|
||||
break
|
||||
fi
|
||||
if curl -fsS "$READY_URL" >/dev/null 2>&1; then
|
||||
vite_ready="1"
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
if [[ "$vite_ready" == "1" ]]; then
|
||||
echo "Vite dev server is ready."
|
||||
else
|
||||
echo "Warning: timed out waiting for the Vite dev server."
|
||||
echo "The backend will still start, but the frontend may not hot-reload yet."
|
||||
fi
|
||||
else
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "Vite log: $VITE_LOG_FILE"
|
||||
echo "Backend file watching is enabled."
|
||||
watch_and_run_server
|
||||
|
|
@ -1,11 +1,27 @@
|
|||
"""Gunicorn configuration for local development."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
bind = "127.0.0.1:8008"
|
||||
worker_class = "gthread"
|
||||
workers = 1
|
||||
threads = 4
|
||||
reload = True
|
||||
raw_env = ["SOULSYNC_WEB_DEV_NO_CACHE=1"]
|
||||
|
||||
_ROOT_DIR = Path(__file__).resolve().parent
|
||||
_VITE_URL = os.environ.get('SOULSYNC_WEBUI_VITE_URL', 'http://127.0.0.1:5173').rstrip('/')
|
||||
_VITE_LOG = os.environ.get('SOULSYNC_WEBUI_VITE_LOG', str(_ROOT_DIR / 'logs' / 'webui-vite.log'))
|
||||
|
||||
# Dev Gunicorn config and Vite dev server are paired on purpose.
|
||||
raw_env = [
|
||||
"SOULSYNC_WEB_DEV_NO_CACHE=1",
|
||||
"SOULSYNC_WEBUI_VITE_DEV=1",
|
||||
f"SOULSYNC_CONFIG_PATH={os.environ.get('SOULSYNC_CONFIG_PATH', str(_ROOT_DIR / 'config' / 'config.json'))}",
|
||||
f"SOULSYNC_LOG_LEVEL={os.environ.get('SOULSYNC_LOG_LEVEL', '')}",
|
||||
f"SOULSYNC_WEBUI_VITE_URL={_VITE_URL}",
|
||||
f"SOULSYNC_WEBUI_VITE_LOG={_VITE_LOG}",
|
||||
]
|
||||
|
||||
# Keep requests from hanging forever on slow external services.
|
||||
timeout = 120
|
||||
|
|
|
|||
|
|
@ -64,3 +64,23 @@ That order avoids load-time references to missing globals and keeps the React si
|
|||
- bridge typings in `webui/src/platform/shell/globals.d.ts`
|
||||
- a legacy fallback path in `webui/static/init.js`
|
||||
- bridge glue or handoff logic in `webui/static/shell-bridge.js`
|
||||
|
||||
## Development
|
||||
|
||||
The recommended dev flow keeps the backend and frontend separate:
|
||||
|
||||
1. Start the Python backend:
|
||||
```bash
|
||||
gunicorn -c gunicorn.dev.conf.py wsgi:application
|
||||
```
|
||||
The dev Gunicorn config watches backend files and restarts the Python server when they change.
|
||||
2. Start the Vite dev server in another terminal:
|
||||
```bash
|
||||
cd webui
|
||||
npm ci
|
||||
npm run dev -- --host 127.0.0.1 --port 5173
|
||||
```
|
||||
Vite hot reloads the React side when you change webui files.
|
||||
|
||||
If you want a convenience wrapper, the repo root also includes `./dev.sh`.
|
||||
It starts both halves together and is most useful on Linux, macOS, and WSL.
|
||||
|
|
|
|||
Loading…
Reference in a new issue