From 7129fc0aa0f1b4d3301d249c14ce67b446c20f7b Mon Sep 17 00:00:00 2001 From: phenix3443 Date: Tue, 14 Apr 2026 16:56:21 +0800 Subject: [PATCH 1/6] feat: add telepresence local dev script --- README.md | 11 + README.zh-CN.md | 11 + telepresence.sh | 817 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 839 insertions(+) create mode 100755 telepresence.sh diff --git a/README.md b/README.md index 7bb33a4..36818af 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,15 @@ bash <(curl -fsSL https://raw.githubusercontent.com/perfect-panel/ppanel-script/ bash <(wget -qO- https://raw.githubusercontent.com/perfect-panel/ppanel-script/refs/heads/main/install.sh) ``` +### Local Telepresence Workflow + +Use [`telepresence.sh`](./telepresence.sh) to bring up the local frontend/backend workflow used with Telepresence-style dev domains. + +```bash +VITE_ALLOWED_HOSTS=.home.arpa \ +VITE_DEVTOOLS_PORT=42170 \ +./telepresence.sh up frontend --frontend user +``` + +You can also override `PPANEL_ROOT`, `FRONTEND_ROOT`, or `SERVER_ROOT` if your local checkout layout differs from the default sibling-repo structure. diff --git a/README.zh-CN.md b/README.zh-CN.md index 0bb08f9..370f652 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -22,3 +22,14 @@ bash <(curl -fsSL https://raw.githubusercontent.com/perfect-panel/ppanel-script/ bash <(wget -qO- https://raw.githubusercontent.com/perfect-panel/ppanel-script/refs/heads/main/install.sh) ``` +## 本地 Telepresence 联调 + +使用 [`telepresence.sh`](./telepresence.sh) 启动本地前后端联调环境,并配合 Telepresence 风格的开发域名使用。 + +```sh +VITE_ALLOWED_HOSTS=.home.arpa \ +VITE_DEVTOOLS_PORT=42170 \ +./telepresence.sh up frontend --frontend user +``` + +如果你的本地目录结构不是默认的同级仓库布局,也可以通过 `PPANEL_ROOT`、`FRONTEND_ROOT` 或 `SERVER_ROOT` 覆盖默认路径。 diff --git a/telepresence.sh b/telepresence.sh new file mode 100755 index 0000000..463ffb9 --- /dev/null +++ b/telepresence.sh @@ -0,0 +1,817 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +ACTION="${1:-help}" +TARGET="${2:-}" +shift $(( $# > 0 ? 1 : 0 )) || true +shift $(( $# > 0 ? 1 : 0 )) || true + +PPANEL_ROOT="${PPANEL_ROOT:-$(cd -- "${SCRIPT_DIR}/.." && pwd)}" +FRONTEND_ROOT="${FRONTEND_ROOT:-$PPANEL_ROOT/ppanel-frontend}" +SERVER_ROOT="${SERVER_ROOT:-$PPANEL_ROOT/ppanel-server}" + +STATE_DIR="${STATE_DIR:-$HOME/.cache/ppanel-local-dev}" +SERVER_PID_FILE="$STATE_DIR/server.pid" +FRONTEND_PID_FILE="$STATE_DIR/frontend.pid" +SERVER_LOG_FILE="$STATE_DIR/server.log" +FRONTEND_LOG_FILE="$STATE_DIR/frontend.log" +DEV_NETWORK="${DEV_NETWORK:-ppanel-local-dev}" +SERVER_CONTAINER="${SERVER_CONTAINER:-ppanel-local-server}" +SERVER_IMAGE="${SERVER_IMAGE:-ppanel-server-ppanel}" + +FRONTEND_APP="${FRONTEND_APP:-user}" +LOCAL_SERVER_HOST="${LOCAL_SERVER_HOST:-127.0.0.1}" +LOCAL_SERVER_PORT="${LOCAL_SERVER_PORT:-8080}" +LOCAL_FRONTEND_HOST="${LOCAL_FRONTEND_HOST:-127.0.0.1}" +LOCAL_USER_PORT="${LOCAL_USER_PORT:-3000}" +LOCAL_ADMIN_PORT="${LOCAL_ADMIN_PORT:-3001}" + +MYSQL_CONTAINER="${MYSQL_CONTAINER:-ppanel-local-mysql}" +MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4.5}" +MYSQL_PORT="${MYSQL_PORT:-13306}" +MYSQL_DATABASE="${MYSQL_DATABASE:-ppanel_dev}" +MYSQL_USER="${MYSQL_USER:-ppanel_dev}" +MYSQL_PASSWORD="${MYSQL_PASSWORD:-ppanel-dev-password}" +MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-dev-root-password}" +SERVER_DB_USER="${SERVER_DB_USER:-root}" +SERVER_DB_PASSWORD="${SERVER_DB_PASSWORD:-$MYSQL_ROOT_PASSWORD}" + +REDIS_CONTAINER="${REDIS_CONTAINER:-ppanel-local-redis}" +REDIS_IMAGE="${REDIS_IMAGE:-redis:7.4.2}" +REDIS_PORT="${REDIS_PORT:-16379}" + +ADMIN_EMAIL="${ADMIN_EMAIL:-admin@ppanel.dev}" +ADMIN_PASSWORD="${ADMIN_PASSWORD:-password}" + +# These defaults are intentionally local placeholders. Replace them with real +# provider credentials in your shell env if you want to complete the provider +# callback instead of just validating button exposure and redirect generation. +GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID:-ppanel-local-google-client.apps.googleusercontent.com}" +GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET:-ppanel-local-google-secret}" +TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-123456789:ppanel-local-dev-bot-token}" + +SERVER_URL="http://${LOCAL_SERVER_HOST}:${LOCAL_SERVER_PORT}" +CONFIG_PATH="$SERVER_ROOT/etc/ppanel.yaml" + +usage() { + cat <&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +ensure_state_dir() { + mkdir -p "$STATE_DIR" +} + +ensure_docker_network() { + if ! docker network inspect "$DEV_NETWORK" >/dev/null 2>&1; then + docker network create "$DEV_NETWORK" >/dev/null + fi +} + +frontend_port() { + if [[ "$FRONTEND_APP" == "admin" ]]; then + printf '%s\n' "$LOCAL_ADMIN_PORT" + else + printf '%s\n' "$LOCAL_USER_PORT" + fi +} + +frontend_devtools_port() { + if [[ -n "${VITE_DEVTOOLS_PORT:-}" ]]; then + printf '%s\n' "$VITE_DEVTOOLS_PORT" + return + fi + + if [[ "$FRONTEND_APP" == "admin" ]]; then + printf '42070\n' + else + printf '42069\n' + fi +} + +frontend_dir() { + if [[ "$FRONTEND_APP" == "admin" ]]; then + printf '%s/apps/admin\n' "$FRONTEND_ROOT" + else + printf '%s/apps/user\n' "$FRONTEND_ROOT" + fi +} + +is_pid_running() { + local pid="$1" + [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1 +} + +stop_pid_if_running() { + local pid="$1" + if is_pid_running "$pid"; then + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" 2>/dev/null || true + fi +} + +read_pid() { + local file="$1" + if [[ -f "$file" ]]; then + tr -d '[:space:]' <"$file" + fi +} + +wait_for_http() { + local url="$1" + local attempts="${2:-60}" + local sleep_seconds="${3:-1}" + local i + for ((i=1; i<=attempts; i++)); do + if curl -fsS "$url" >/dev/null 2>&1; then + return 0 + fi + sleep "$sleep_seconds" + done + return 1 +} + +wait_for_http_status() { + local url="$1" + local attempts="${2:-60}" + local sleep_seconds="${3:-1}" + local i + local status + for ((i=1; i<=attempts; i++)); do + status="$(curl -sS -o /dev/null -w '%{http_code}' "$url" || true)" + if [[ "$status" != "000" ]]; then + return 0 + fi + sleep "$sleep_seconds" + done + return 1 +} + +json_extract() { + local json_payload="$1" + local path="$2" + JSON_PAYLOAD="$json_payload" python3 - "$path" <<'PY' +import json +import os +import sys + +path = sys.argv[1].split(".") +data = json.loads(os.environ["JSON_PAYLOAD"]) + +node = data +for part in path: + if part == "": + continue + if isinstance(node, list): + node = node[int(part)] + else: + node = node[part] + +if isinstance(node, (dict, list)): + print(json.dumps(node)) +elif node is None: + print("") +else: + print(node) +PY +} + +json_has_oauth_method() { + local json_payload="$1" + local method="$2" + JSON_PAYLOAD="$json_payload" python3 - "$method" <<'PY' +import json +import os +import sys + +method = sys.argv[1] +payload = json.loads(os.environ["JSON_PAYLOAD"]) +methods = payload.get("data", {}).get("oauth_methods", []) +sys.exit(0 if method in methods else 1) +PY +} + +ensure_python() { + require_cmd python3 +} + +ensure_basic_tools() { + require_cmd curl + require_cmd docker + require_cmd go + ensure_python +} + +frontend_dev_command() { + if command -v bun >/dev/null 2>&1; then + printf 'exec bun dev --host %s --port %s\n' "$LOCAL_FRONTEND_HOST" "$(frontend_port)" + return + fi + + if [[ -x "${FRONTEND_ROOT}/node_modules/.bin/vite" ]]; then + printf 'exec "%s/node_modules/.bin/vite" --host %s --port %s\n' "$FRONTEND_ROOT" "$LOCAL_FRONTEND_HOST" "$(frontend_port)" + return + fi + + if command -v npm >/dev/null 2>&1; then + printf 'exec npm exec -- vite --host %s --port %s\n' "$LOCAL_FRONTEND_HOST" "$(frontend_port)" + return + fi + + die "Missing frontend runner: neither bun nor npm is available" +} + +start_detached_process() { + local pid_file="$1" + local log_file="$2" + local cwd="$3" + shift 3 + + DETACHED_PID_FILE="$pid_file" \ + DETACHED_LOG_FILE="$log_file" \ + DETACHED_CWD="$cwd" \ + python3 - "$@" <<'PY' +import os +import subprocess +import sys + +pid_file = os.environ["DETACHED_PID_FILE"] +log_file = os.environ["DETACHED_LOG_FILE"] +cwd = os.environ["DETACHED_CWD"] +command = sys.argv[1:] + +env = os.environ.copy() + +with open(os.devnull, "rb") as devnull, open(log_file, "ab", buffering=0) as log_handle: + process = subprocess.Popen( + command, + cwd=cwd, + env=env, + stdin=devnull, + stdout=log_handle, + stderr=subprocess.STDOUT, + start_new_session=True, + close_fds=True, + ) + +with open(pid_file, "w", encoding="utf-8") as handle: + handle.write(str(process.pid)) +PY +} + +find_listener_pid() { + local port="$1" + if ! command -v lsof >/dev/null 2>&1; then + return 0 + fi + + lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n 1 || true +} + +ensure_frontend_port_available() { + local port="$1" + local label="$2" + local pid command + + pid="$(find_listener_pid "$port")" + [[ -n "$pid" ]] || return 0 + + command="$(ps -p "$pid" -o command= 2>/dev/null || true)" + if [[ "$command" == *"$FRONTEND_ROOT"* ]] && [[ "$command" == *"vite"* || "$command" == *"bun"* || "$command" == *"node"* ]]; then + log "Stopping stale frontend process on ${label} port ${port} (PID ${pid})" + stop_pid_if_running "$pid" + sleep 1 + pid="$(find_listener_pid "$port")" + fi + + [[ -z "$pid" ]] || die "${label} port ${port} is already in use by: ${command}" +} + +docker_container_running() { + local name="$1" + docker ps --format '{{.Names}}' | grep -qx "$name" +} + +docker_container_exists() { + local name="$1" + docker ps -a --format '{{.Names}}' | grep -qx "$name" +} + +ensure_container_on_network() { + local name="$1" + if ! docker inspect -f '{{range $k, $_ := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$name" | grep -qx "$DEV_NETWORK"; then + docker network connect "$DEV_NETWORK" "$name" >/dev/null 2>&1 || true + fi +} + +sync_mysql_user_grants() { + docker exec "$MYSQL_CONTAINER" mysql -uroot "-p${MYSQL_ROOT_PASSWORD}" </dev/null +CREATE DATABASE IF NOT EXISTS \`${MYSQL_DATABASE}\`; +CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; +ALTER USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; +CREATE USER IF NOT EXISTS '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; +ALTER USER '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; +GRANT ALL PRIVILEGES ON \`${MYSQL_DATABASE}\`.* TO '${MYSQL_USER}'@'%'; +FLUSH PRIVILEGES; +EOF +} + +start_mysql() { + ensure_docker_network + if docker_container_running "$MYSQL_CONTAINER"; then + log "MySQL container is already running: $MYSQL_CONTAINER" + ensure_container_on_network "$MYSQL_CONTAINER" + sync_mysql_user_grants + return + fi + + if docker_container_exists "$MYSQL_CONTAINER"; then + log "Starting existing MySQL container: $MYSQL_CONTAINER" + docker start "$MYSQL_CONTAINER" >/dev/null + else + log "Creating local MySQL container: $MYSQL_CONTAINER" + docker run -d \ + --name "$MYSQL_CONTAINER" \ + --network "$DEV_NETWORK" \ + -e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" \ + -e MYSQL_DATABASE="$MYSQL_DATABASE" \ + -e MYSQL_USER="$MYSQL_USER" \ + -e MYSQL_PASSWORD="$MYSQL_PASSWORD" \ + -p "${MYSQL_PORT}:3306" \ + "$MYSQL_IMAGE" \ + --mysql-native-password=ON \ + --bind-address=0.0.0.0 >/dev/null + fi + + log "Waiting for MySQL to become ready" + local i + for ((i=1; i<=90; i++)); do + if docker exec "$MYSQL_CONTAINER" mysqladmin ping -h 127.0.0.1 -uroot "-p${MYSQL_ROOT_PASSWORD}" --silent >/dev/null 2>&1; then + sync_mysql_user_grants + return + fi + sleep 1 + done + die "MySQL did not become ready in time" +} + +start_redis() { + if docker_container_running "$REDIS_CONTAINER"; then + log "Redis container is already running: $REDIS_CONTAINER" + ensure_container_on_network "$REDIS_CONTAINER" + return + fi + + if docker_container_exists "$REDIS_CONTAINER"; then + log "Starting existing Redis container: $REDIS_CONTAINER" + docker start "$REDIS_CONTAINER" >/dev/null + else + log "Creating local Redis container: $REDIS_CONTAINER" + docker run -d \ + --name "$REDIS_CONTAINER" \ + --network "$DEV_NETWORK" \ + -p "${REDIS_PORT}:6379" \ + "$REDIS_IMAGE" >/dev/null + fi + + log "Waiting for Redis to become ready" + local i + for ((i=1; i<=60; i++)); do + if docker exec "$REDIS_CONTAINER" redis-cli ping >/dev/null 2>&1; then + return + fi + sleep 1 + done + die "Redis did not become ready in time" +} + +stop_conflicting_compose_server() { + if docker_container_running "ppanel-server"; then + log "Stopping existing dockerized ppanel-server to free port ${LOCAL_SERVER_PORT}" + docker stop ppanel-server >/dev/null + fi +} + +ensure_server_process() { + ensure_state_dir + ensure_docker_network + + if docker_container_running "$SERVER_CONTAINER"; then + log "Local server container is already running: $SERVER_CONTAINER" + return + fi + + stop_conflicting_compose_server + normalize_server_config + + if docker_container_exists "$SERVER_CONTAINER"; then + docker rm -f "$SERVER_CONTAINER" >/dev/null 2>&1 || true + fi + + log "Starting local ppanel-server container" + docker run -d \ + --name "$SERVER_CONTAINER" \ + --network "$DEV_NETWORK" \ + -p "${LOCAL_SERVER_PORT}:8080" \ + -v "${SERVER_ROOT}/etc:/app/etc" \ + -e PPANEL_DB="${SERVER_DB_USER}:${SERVER_DB_PASSWORD}@tcp(${MYSQL_CONTAINER}:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" \ + -e PPANEL_REDIS="redis://${REDIS_CONTAINER}:6379/0" \ + "$SERVER_IMAGE" >/dev/null + + if server_config_initialized; then + if ! wait_for_http "${SERVER_URL}/v1/common/site/config" 90 1; then + docker logs --tail=120 "$SERVER_CONTAINER" >"$SERVER_LOG_FILE" 2>&1 || true + die "Local server failed to become ready. See $SERVER_LOG_FILE" + fi + return + fi + + if ! wait_for_container_http "http://127.0.0.1:8080/init" 90 1; then + docker logs --tail=120 "$SERVER_CONTAINER" >"$SERVER_LOG_FILE" 2>&1 || true + die "Init server did not become ready inside container. See $SERVER_LOG_FILE" + fi +} + +normalize_server_config() { + if [[ ! -s "$CONFIG_PATH" ]]; then + return + fi + + python3 - "$CONFIG_PATH" "$MYSQL_CONTAINER" "$MYSQL_DATABASE" "$SERVER_DB_USER" "$SERVER_DB_PASSWORD" "$REDIS_CONTAINER" <<'PY' +import re +import sys + +path, mysql_host, mysql_db, mysql_user, mysql_password, redis_host = sys.argv[1:] +text = open(path, "r", encoding="utf-8").read() + +replacements = [ + (r"(?m)^ Addr: .*$", f" Addr: {mysql_host}:3306"), + (r"(?m)^ Username: .*$", f" Username: {mysql_user}"), + (r"(?m)^ Password: .*$", f" Password: {mysql_password}"), + (r"(?m)^ Dbname: .*$", f" Dbname: {mysql_db}"), + (r"(?m)^ Host: .*$", f" Host: {redis_host}:6379"), +] + +for pattern, replacement in replacements: + text = re.sub(pattern, replacement, text) + +with open(path, "w", encoding="utf-8") as fh: + fh.write(text) +PY +} + +wait_for_container_http() { + local url="$1" + local attempts="${2:-60}" + local sleep_seconds="${3:-1}" + local i + for ((i=1; i<=attempts; i++)); do + if docker run --rm --network "container:${SERVER_CONTAINER}" curlimages/curl:8.12.1 -fsS "$url" >/dev/null 2>&1; then + return 0 + fi + sleep "$sleep_seconds" + done + return 1 +} + +server_config_initialized() { + [[ -s "$CONFIG_PATH" ]] && grep -q 'AccessSecret' "$CONFIG_PATH" 2>/dev/null +} + +initialize_backend_if_needed() { + if server_config_initialized; then + log "Backend config already initialized" + return + fi + + log "Initializing backend via ${SERVER_URL}/init/config" + docker run --rm \ + --network "container:${SERVER_CONTAINER}" \ + curlimages/curl:8.12.1 \ + -fsS \ + -H 'Content-Type: application/json' \ + -X POST \ + "http://127.0.0.1:8080/init/config" \ + -d "$(cat </dev/null + + if ! wait_for_http "${SERVER_URL}/v1/common/site/config" 120 1; then + docker logs --tail=120 "$SERVER_CONTAINER" >"$SERVER_LOG_FILE" 2>&1 || true + die "Backend initialization finished but API did not become ready. See $SERVER_LOG_FILE" + fi +} + +login_admin() { + curl -fsS \ + -H 'Content-Type: application/json' \ + -X POST \ + "${SERVER_URL}/v1/auth/login" \ + -d "$(cat </dev/null || true + + docker exec "$MYSQL_CONTAINER" mysql -uroot "-p${MYSQL_ROOT_PASSWORD}" "$MYSQL_DATABASE" </dev/null +UPDATE user AS u +JOIN user_auth_methods AS m ON m.user_id = u.id +SET u.is_admin = 1 +WHERE m.auth_type = 'email' AND m.auth_identifier = '${ADMIN_EMAIL}'; +EOF +} + +admin_token() { + local response code token + response="$(login_admin)" + code="$(json_extract "$response" "code")" + + if [[ "$code" != "200" ]]; then + ensure_admin_user + response="$(login_admin)" + code="$(json_extract "$response" "code")" + fi + + [[ "$code" == "200" ]] || die "Admin login failed: $response" + token="$(json_extract "$response" "data.token")" + printf '%s\n' "$token" +} + +update_auth_method() { + local token="$1" + local method="$2" + local enabled="$3" + local config_json="$4" + + curl -fsS \ + -H 'Content-Type: application/json' \ + -H "Authorization: ${token}" \ + -X PUT \ + "${SERVER_URL}/v1/admin/auth-method/config" \ + -d "$(cat </dev/null +} + +configure_oauth_methods() { + local token + token="$(admin_token)" + [[ -n "$token" ]] || die "Failed to obtain admin token" + + log "Enabling Google auth method" + update_auth_method "$token" "google" "true" "$(cat <"$FRONTEND_LOG_FILE" + VITE_API_BASE_URL="$SERVER_URL" \ + VITE_API_PREFIX="" \ + start_detached_process \ + "$FRONTEND_PID_FILE" \ + "$FRONTEND_LOG_FILE" \ + "$(frontend_dir)" \ + /bin/sh -lc "$(frontend_dev_command)" + + if ! wait_for_http "http://${LOCAL_FRONTEND_HOST}:$(frontend_port)" 90 1; then + die "Frontend did not become ready. See $FRONTEND_LOG_FILE" + fi +} + +stop_pid_file() { + local file="$1" + local label="$2" + local pid + pid="$(read_pid "$file")" + if is_pid_running "$pid"; then + log "Stopping $label (PID $pid)" + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" 2>/dev/null || true + fi + rm -f "$file" +} + +leave_server() { + if docker_container_exists "$SERVER_CONTAINER"; then + log "Stopping local server container: $SERVER_CONTAINER" + docker rm -f "$SERVER_CONTAINER" >/dev/null 2>&1 || true + fi +} + +leave_frontend() { + stop_pid_file "$FRONTEND_PID_FILE" "frontend dev server" +} + +status() { + ensure_state_dir + + local server_pid frontend_pid + server_pid="$(read_pid "$SERVER_PID_FILE")" + frontend_pid="$(read_pid "$FRONTEND_PID_FILE")" + + printf 'Server process: %s\n' "$(docker_container_running "$SERVER_CONTAINER" && printf 'running (container %s)' "$SERVER_CONTAINER" || printf 'stopped')" + printf 'Frontend process: %s\n' "$(is_pid_running "$frontend_pid" && printf 'running (pid %s)' "$frontend_pid" || printf 'stopped')" + printf 'MySQL container: %s\n' "$(docker_container_running "$MYSQL_CONTAINER" && printf 'running' || printf 'stopped')" + printf 'Redis container: %s\n' "$(docker_container_running "$REDIS_CONTAINER" && printf 'running' || printf 'stopped')" + printf 'Server URL: %s\n' "$SERVER_URL" + printf 'Frontend URL: %s\n' "http://${LOCAL_FRONTEND_HOST}:$(frontend_port)" + printf 'Server log: %s\n' "$SERVER_LOG_FILE" + printf 'Frontend log: %s\n' "$FRONTEND_LOG_FILE" +} + +case "$ACTION" in + up) + case "$TARGET" in + frontend) + while [[ $# -gt 0 ]]; do + case "$1" in + --frontend) + FRONTEND_APP="${2:-user}" + shift 2 + ;; + *) + die "Unknown argument: $1" + ;; + esac + done + start_frontend + ;; + server) + ensure_backend + ;; + both) + while [[ $# -gt 0 ]]; do + case "$1" in + --frontend) + FRONTEND_APP="${2:-user}" + shift 2 + ;; + *) + die "Unknown argument: $1" + ;; + esac + done + ensure_backend + start_frontend + ;; + *) + usage + exit 1 + ;; + esac + ;; + test-auth) + ensure_backend + ;; + status) + status + ;; + leave|down) + case "$TARGET" in + frontend) + leave_frontend + ;; + server) + leave_server + ;; + both|"") + leave_frontend + leave_server + ;; + *) + usage + exit 1 + ;; + esac + ;; + help|-h|--help|"") + usage + ;; + *) + usage + exit 1 + ;; +esac From 728e2e9355567e673a33a899e9b5e182a79a22e4 Mon Sep 17 00:00:00 2001 From: phenix3443 Date: Thu, 16 Apr 2026 08:50:26 +0800 Subject: [PATCH 2/6] Fix telepresence local intercept host --- README.md | 25 ++- README.zh-CN.md | 26 ++- telepresence.sh | 510 +++++++++++++++++++++++++++++++----------------- 3 files changed, 373 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index 36818af..204e433 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ bash <(wget -qO- https://raw.githubusercontent.com/perfect-panel/ppanel-script/r ### Local Telepresence Workflow -Use [`telepresence.sh`](./telepresence.sh) to bring up the local frontend/backend workflow used with Telepresence-style dev domains. +Use [`telepresence.sh`](./telepresence.sh) with Telepresence-style dev domains to switch traffic to a local frontend or local backend. ```bash VITE_ALLOWED_HOSTS=.home.arpa \ @@ -32,5 +32,26 @@ VITE_DEVTOOLS_PORT=42170 \ ./telepresence.sh up frontend --frontend user ``` -You can also override `PPANEL_ROOT`, `FRONTEND_ROOT`, or `SERVER_ROOT` if your local checkout layout differs from the default sibling-repo structure. +- `up frontend`: route traffic to a local frontend while keeping the k3s backend +- `up server`: route traffic to a local backend while keeping shared MySQL / Redis +- `up both`: route traffic to both a local frontend and a local backend while still using shared MySQL / Redis +By default the script connects to shared dependencies through `host.docker.internal:13306` and `host.docker.internal:16379`. +The script uses Telepresence to connect to the `ppanel-dev` namespace and create intercepts for the frontend or backend workloads. + +If you want to make the shared dependency targets explicit, pass them through CLI options instead of environment variables, for example: + +```bash +./telepresence.sh up server \ + --mysql-host host.docker.internal \ + --mysql-port 13306 \ + --mysql-database ppanel_dev \ + --mysql-user root \ + --mysql-password dev-root-password \ + --redis-host host.docker.internal \ + --redis-port 16379 +``` + +If the cluster does not have a Telepresence `traffic-manager` yet, append `--install-traffic-manager` on the first run. + +The script assumes `ppanel-script`, `ppanel-frontend`, and `ppanel-server` live as sibling directories. diff --git a/README.zh-CN.md b/README.zh-CN.md index 370f652..c4ddc73 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -24,7 +24,7 @@ bash <(wget -qO- https://raw.githubusercontent.com/perfect-panel/ppanel-script/r ## 本地 Telepresence 联调 -使用 [`telepresence.sh`](./telepresence.sh) 启动本地前后端联调环境,并配合 Telepresence 风格的开发域名使用。 +使用 [`telepresence.sh`](./telepresence.sh) 配合 Telepresence 风格的开发域名切换本地前端或本地后端。 ```sh VITE_ALLOWED_HOSTS=.home.arpa \ @@ -32,4 +32,26 @@ VITE_DEVTOOLS_PORT=42170 \ ./telepresence.sh up frontend --frontend user ``` -如果你的本地目录结构不是默认的同级仓库布局,也可以通过 `PPANEL_ROOT`、`FRONTEND_ROOT` 或 `SERVER_ROOT` 覆盖默认路径。 +- `up frontend`:切到本地前端,后端继续使用 k3s 中的部署 +- `up server`:切到本地后端,依赖继续使用共享的 MySQL / Redis +- `up both`:前后端都切到本地;依赖仍使用共享的 MySQL / Redis + +默认会通过 `host.docker.internal:13306` 和 `host.docker.internal:16379` 连接共享依赖。 +脚本会使用 Telepresence 连接 `ppanel-dev` 命名空间,并为前端或后端创建 intercept。 + +如果要显式指定共享依赖,请直接通过命令行参数传入,而不是依赖环境变量,例如: + +```sh +./telepresence.sh up server \ + --mysql-host host.docker.internal \ + --mysql-port 13306 \ + --mysql-database ppanel_dev \ + --mysql-user root \ + --mysql-password dev-root-password \ + --redis-host host.docker.internal \ + --redis-port 16379 +``` + +如果集群里还没有安装 Telepresence `traffic-manager`,可以在首次执行时追加 `--install-traffic-manager`。 + +脚本默认假设 `ppanel-script`、`ppanel-frontend`、`ppanel-server` 是同级目录。 diff --git a/telepresence.sh b/telepresence.sh index 463ffb9..62a3f02 100755 --- a/telepresence.sh +++ b/telepresence.sh @@ -10,39 +10,38 @@ TARGET="${2:-}" shift $(( $# > 0 ? 1 : 0 )) || true shift $(( $# > 0 ? 1 : 0 )) || true -PPANEL_ROOT="${PPANEL_ROOT:-$(cd -- "${SCRIPT_DIR}/.." && pwd)}" -FRONTEND_ROOT="${FRONTEND_ROOT:-$PPANEL_ROOT/ppanel-frontend}" -SERVER_ROOT="${SERVER_ROOT:-$PPANEL_ROOT/ppanel-server}" +PPANEL_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +FRONTEND_ROOT="$PPANEL_ROOT/ppanel-frontend" +SERVER_ROOT="$PPANEL_ROOT/ppanel-server" -STATE_DIR="${STATE_DIR:-$HOME/.cache/ppanel-local-dev}" +STATE_DIR="$HOME/.cache/ppanel-local-dev" SERVER_PID_FILE="$STATE_DIR/server.pid" FRONTEND_PID_FILE="$STATE_DIR/frontend.pid" SERVER_LOG_FILE="$STATE_DIR/server.log" FRONTEND_LOG_FILE="$STATE_DIR/frontend.log" -DEV_NETWORK="${DEV_NETWORK:-ppanel-local-dev}" -SERVER_CONTAINER="${SERVER_CONTAINER:-ppanel-local-server}" -SERVER_IMAGE="${SERVER_IMAGE:-ppanel-server-ppanel}" - -FRONTEND_APP="${FRONTEND_APP:-user}" -LOCAL_SERVER_HOST="${LOCAL_SERVER_HOST:-127.0.0.1}" -LOCAL_SERVER_PORT="${LOCAL_SERVER_PORT:-8080}" -LOCAL_FRONTEND_HOST="${LOCAL_FRONTEND_HOST:-127.0.0.1}" -LOCAL_USER_PORT="${LOCAL_USER_PORT:-3000}" -LOCAL_ADMIN_PORT="${LOCAL_ADMIN_PORT:-3001}" - -MYSQL_CONTAINER="${MYSQL_CONTAINER:-ppanel-local-mysql}" -MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4.5}" -MYSQL_PORT="${MYSQL_PORT:-13306}" -MYSQL_DATABASE="${MYSQL_DATABASE:-ppanel_dev}" -MYSQL_USER="${MYSQL_USER:-ppanel_dev}" -MYSQL_PASSWORD="${MYSQL_PASSWORD:-ppanel-dev-password}" -MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-dev-root-password}" -SERVER_DB_USER="${SERVER_DB_USER:-root}" -SERVER_DB_PASSWORD="${SERVER_DB_PASSWORD:-$MYSQL_ROOT_PASSWORD}" - -REDIS_CONTAINER="${REDIS_CONTAINER:-ppanel-local-redis}" -REDIS_IMAGE="${REDIS_IMAGE:-redis:7.4.2}" -REDIS_PORT="${REDIS_PORT:-16379}" +DEV_NETWORK="ppanel-local-dev" +SERVER_CONTAINER="ppanel-local-server" +SERVER_IMAGE="ppanel-server-ppanel" + +FRONTEND_APP="user" +LOCAL_SERVER_HOST="127.0.0.1" +LOCAL_SERVER_PORT="8080" +LOCAL_FRONTEND_HOST="127.0.0.1" +LOCAL_USER_PORT="3000" +LOCAL_ADMIN_PORT="3001" +REMOTE_MYSQL_HOST="host.docker.internal" +REMOTE_REDIS_HOST="host.docker.internal" +K8S_NAMESPACE="ppanel-dev" +TELEPRESENCE_MANAGER_NAMESPACE="ppanel-dev" +INSTALL_TRAFFIC_MANAGER="false" + +MYSQL_PORT="13306" +MYSQL_DATABASE="ppanel_dev" +SERVER_DB_USER="root" +SERVER_DB_PASSWORD="dev-root-password" + +REDIS_PORT="16379" +REDIS_PASSWORD="" ADMIN_EMAIL="${ADMIN_EMAIL:-admin@ppanel.dev}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-password}" @@ -60,29 +59,43 @@ CONFIG_PATH="$SERVER_ROOT/etc/ppanel.yaml" usage() { cat </dev/null 2>&1 || die "Missing required command: $1" } +require_option_value() { + local option="$1" + local value="${2:-}" + [[ -n "$value" ]] || die "Missing value for ${option}" +} + +parse_frontend_option() { + local option="$1" + local value="$2" + require_option_value "$option" "$value" + case "$value" in + admin|user) + FRONTEND_APP="$value" + ;; + *) + die "Unsupported frontend app: $value" + ;; + esac +} + +parse_shared_dependency_options() { + while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) + require_option_value "$1" "${2:-}" + K8S_NAMESPACE="$2" + shift 2 + ;; + --manager-namespace) + require_option_value "$1" "${2:-}" + TELEPRESENCE_MANAGER_NAMESPACE="$2" + shift 2 + ;; + --install-traffic-manager) + INSTALL_TRAFFIC_MANAGER="true" + shift + ;; + --mysql-host) + require_option_value "$1" "${2:-}" + REMOTE_MYSQL_HOST="$2" + shift 2 + ;; + --mysql-port) + require_option_value "$1" "${2:-}" + MYSQL_PORT="$2" + shift 2 + ;; + --mysql-database) + require_option_value "$1" "${2:-}" + MYSQL_DATABASE="$2" + shift 2 + ;; + --mysql-user) + require_option_value "$1" "${2:-}" + SERVER_DB_USER="$2" + shift 2 + ;; + --mysql-password) + require_option_value "$1" "${2:-}" + SERVER_DB_PASSWORD="$2" + shift 2 + ;; + --redis-host) + require_option_value "$1" "${2:-}" + REMOTE_REDIS_HOST="$2" + shift 2 + ;; + --redis-port) + require_option_value "$1" "${2:-}" + REDIS_PORT="$2" + shift 2 + ;; + --redis-password) + require_option_value "$1" "${2:-}" + REDIS_PASSWORD="$2" + shift 2 + ;; + *) + die "Unknown argument: $1" + ;; + esac + done +} + +parse_frontend_and_dependency_options() { + while [[ $# -gt 0 ]]; do + case "$1" in + --frontend) + parse_frontend_option "$1" "${2:-}" + shift 2 + ;; + --namespace|--manager-namespace|--install-traffic-manager|--mysql-host|--mysql-port|--mysql-database|--mysql-user|--mysql-password|--redis-host|--redis-port|--redis-password) + parse_shared_dependency_options "$@" + return + ;; + *) + die "Unknown argument: $1" + ;; + esac + done +} + ensure_state_dir() { mkdir -p "$STATE_DIR" } @@ -243,6 +358,103 @@ ensure_basic_tools() { ensure_python } +ensure_frontend_tools() { + require_cmd curl + ensure_python +} + +ensure_telepresence_tools() { + require_cmd kubectl + require_cmd telepresence +} + +mysql_runtime_host() { + printf '%s\n' "$REMOTE_MYSQL_HOST" +} + +mysql_runtime_port() { + printf '%s\n' "$MYSQL_PORT" +} + +redis_runtime_host() { + printf '%s\n' "$REMOTE_REDIS_HOST" +} + +redis_runtime_port() { + printf '%s\n' "$REDIS_PORT" +} + +redis_runtime_url() { + printf 'redis://%s:%s/0\n' "$(redis_runtime_host)" "$(redis_runtime_port)" +} + +frontend_service_name() { + if [[ "$FRONTEND_APP" == "admin" ]]; then + printf 'ppanel-admin-web\n' + else + printf 'ppanel-user-web\n' + fi +} + +traffic_manager_installed() { + kubectl get deployment traffic-manager -n "$TELEPRESENCE_MANAGER_NAMESPACE" >/dev/null 2>&1 +} + +ensure_traffic_manager() { + if traffic_manager_installed; then + return + fi + + if [[ "$INSTALL_TRAFFIC_MANAGER" != "true" ]]; then + die "Telepresence traffic-manager is not installed in namespace ${TELEPRESENCE_MANAGER_NAMESPACE}. Rerun with --install-traffic-manager, or install it manually with: telepresence helm install --manager-namespace ${TELEPRESENCE_MANAGER_NAMESPACE} -n ${K8S_NAMESPACE}" + fi + + log "Installing Telepresence traffic-manager into namespace ${TELEPRESENCE_MANAGER_NAMESPACE}" + telepresence helm install --manager-namespace "$TELEPRESENCE_MANAGER_NAMESPACE" -n "$K8S_NAMESPACE" +} + +telepresence_connect() { + ensure_telepresence_tools + ensure_traffic_manager + + if telepresence status 2>&1 | grep -q 'file stale and removed'; then + telepresence quit --stop-daemons >/dev/null 2>&1 || true + fi + + log "Connecting Telepresence to namespace ${K8S_NAMESPACE} (manager namespace ${TELEPRESENCE_MANAGER_NAMESPACE})" + telepresence connect -n "$K8S_NAMESPACE" --manager-namespace "$TELEPRESENCE_MANAGER_NAMESPACE" +} + +telepresence_leave_intercept() { + local name="$1" + telepresence leave "$name" >/dev/null 2>&1 || true +} + +intercept_frontend_traffic() { + local service + service="$(frontend_service_name)" + + telepresence_connect + telepresence_leave_intercept "$service" + + log "Intercepting frontend service ${K8S_NAMESPACE}/${service} to ${LOCAL_FRONTEND_HOST}:$(frontend_port)" + telepresence intercept "$service" \ + --service "$service" \ + --port "$(frontend_port):3000" \ + --address "$LOCAL_FRONTEND_HOST" +} + +intercept_server_traffic() { + telepresence_connect + telepresence_leave_intercept "ppanel-server" + + log "Intercepting backend service ${K8S_NAMESPACE}/ppanel-server to ${LOCAL_SERVER_HOST}:${LOCAL_SERVER_PORT}" + telepresence intercept "ppanel-server" \ + --service "ppanel-server" \ + --port "${LOCAL_SERVER_PORT}:8080" \ + --address "$LOCAL_SERVER_HOST" +} + frontend_dev_command() { if command -v bun >/dev/null 2>&1; then printf 'exec bun dev --host %s --port %s\n' "$LOCAL_FRONTEND_HOST" "$(frontend_port)" @@ -338,95 +550,6 @@ docker_container_exists() { docker ps -a --format '{{.Names}}' | grep -qx "$name" } -ensure_container_on_network() { - local name="$1" - if ! docker inspect -f '{{range $k, $_ := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$name" | grep -qx "$DEV_NETWORK"; then - docker network connect "$DEV_NETWORK" "$name" >/dev/null 2>&1 || true - fi -} - -sync_mysql_user_grants() { - docker exec "$MYSQL_CONTAINER" mysql -uroot "-p${MYSQL_ROOT_PASSWORD}" </dev/null -CREATE DATABASE IF NOT EXISTS \`${MYSQL_DATABASE}\`; -CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; -ALTER USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; -GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; -CREATE USER IF NOT EXISTS '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; -ALTER USER '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; -GRANT ALL PRIVILEGES ON \`${MYSQL_DATABASE}\`.* TO '${MYSQL_USER}'@'%'; -FLUSH PRIVILEGES; -EOF -} - -start_mysql() { - ensure_docker_network - if docker_container_running "$MYSQL_CONTAINER"; then - log "MySQL container is already running: $MYSQL_CONTAINER" - ensure_container_on_network "$MYSQL_CONTAINER" - sync_mysql_user_grants - return - fi - - if docker_container_exists "$MYSQL_CONTAINER"; then - log "Starting existing MySQL container: $MYSQL_CONTAINER" - docker start "$MYSQL_CONTAINER" >/dev/null - else - log "Creating local MySQL container: $MYSQL_CONTAINER" - docker run -d \ - --name "$MYSQL_CONTAINER" \ - --network "$DEV_NETWORK" \ - -e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" \ - -e MYSQL_DATABASE="$MYSQL_DATABASE" \ - -e MYSQL_USER="$MYSQL_USER" \ - -e MYSQL_PASSWORD="$MYSQL_PASSWORD" \ - -p "${MYSQL_PORT}:3306" \ - "$MYSQL_IMAGE" \ - --mysql-native-password=ON \ - --bind-address=0.0.0.0 >/dev/null - fi - - log "Waiting for MySQL to become ready" - local i - for ((i=1; i<=90; i++)); do - if docker exec "$MYSQL_CONTAINER" mysqladmin ping -h 127.0.0.1 -uroot "-p${MYSQL_ROOT_PASSWORD}" --silent >/dev/null 2>&1; then - sync_mysql_user_grants - return - fi - sleep 1 - done - die "MySQL did not become ready in time" -} - -start_redis() { - if docker_container_running "$REDIS_CONTAINER"; then - log "Redis container is already running: $REDIS_CONTAINER" - ensure_container_on_network "$REDIS_CONTAINER" - return - fi - - if docker_container_exists "$REDIS_CONTAINER"; then - log "Starting existing Redis container: $REDIS_CONTAINER" - docker start "$REDIS_CONTAINER" >/dev/null - else - log "Creating local Redis container: $REDIS_CONTAINER" - docker run -d \ - --name "$REDIS_CONTAINER" \ - --network "$DEV_NETWORK" \ - -p "${REDIS_PORT}:6379" \ - "$REDIS_IMAGE" >/dev/null - fi - - log "Waiting for Redis to become ready" - local i - for ((i=1; i<=60; i++)); do - if docker exec "$REDIS_CONTAINER" redis-cli ping >/dev/null 2>&1; then - return - fi - sleep 1 - done - die "Redis did not become ready in time" -} - stop_conflicting_compose_server() { if docker_container_running "ppanel-server"; then log "Stopping existing dockerized ppanel-server to free port ${LOCAL_SERVER_PORT}" @@ -456,8 +579,8 @@ ensure_server_process() { --network "$DEV_NETWORK" \ -p "${LOCAL_SERVER_PORT}:8080" \ -v "${SERVER_ROOT}/etc:/app/etc" \ - -e PPANEL_DB="${SERVER_DB_USER}:${SERVER_DB_PASSWORD}@tcp(${MYSQL_CONTAINER}:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" \ - -e PPANEL_REDIS="redis://${REDIS_CONTAINER}:6379/0" \ + -e PPANEL_DB="${SERVER_DB_USER}:${SERVER_DB_PASSWORD}@tcp($(mysql_runtime_host):$(mysql_runtime_port))/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" \ + -e PPANEL_REDIS="$(redis_runtime_url)" \ "$SERVER_IMAGE" >/dev/null if server_config_initialized; then @@ -479,19 +602,20 @@ normalize_server_config() { return fi - python3 - "$CONFIG_PATH" "$MYSQL_CONTAINER" "$MYSQL_DATABASE" "$SERVER_DB_USER" "$SERVER_DB_PASSWORD" "$REDIS_CONTAINER" <<'PY' + python3 - "$CONFIG_PATH" "$(mysql_runtime_host)" "$(mysql_runtime_port)" "$MYSQL_DATABASE" "$SERVER_DB_USER" "$SERVER_DB_PASSWORD" "$(redis_runtime_host)" "$(redis_runtime_port)" "$REDIS_PASSWORD" <<'PY' import re import sys -path, mysql_host, mysql_db, mysql_user, mysql_password, redis_host = sys.argv[1:] +path, mysql_host, mysql_port, mysql_db, mysql_user, mysql_password, redis_host, redis_port, redis_password = sys.argv[1:] text = open(path, "r", encoding="utf-8").read() replacements = [ - (r"(?m)^ Addr: .*$", f" Addr: {mysql_host}:3306"), + (r"(?m)^ Addr: .*$", f" Addr: {mysql_host}:{mysql_port}"), (r"(?m)^ Username: .*$", f" Username: {mysql_user}"), (r"(?m)^ Password: .*$", f" Password: {mysql_password}"), (r"(?m)^ Dbname: .*$", f" Dbname: {mysql_db}"), - (r"(?m)^ Host: .*$", f" Host: {redis_host}:6379"), + (r"(?m)^ Host: .*$", f" Host: {redis_host}:{redis_port}"), + (r"(?m)^ Pass: .*$", f" Pass: {redis_password}"), ] for pattern, replacement in replacements: @@ -535,7 +659,7 @@ initialize_backend_if_needed() { -X POST \ "http://127.0.0.1:8080/init/config" \ -d "$(cat </dev/null @@ -567,12 +691,7 @@ ensure_admin_user() { EOF )" >/dev/null || true - docker exec "$MYSQL_CONTAINER" mysql -uroot "-p${MYSQL_ROOT_PASSWORD}" "$MYSQL_DATABASE" </dev/null -UPDATE user AS u -JOIN user_auth_methods AS m ON m.user_id = u.id -SET u.is_admin = 1 -WHERE m.auth_type = 'email' AND m.auth_identifier = '${ADMIN_EMAIL}'; -EOF + die "Admin login failed and the script cannot auto-promote a user in shared MySQL. Set ADMIN_EMAIL/ADMIN_PASSWORD to an existing admin account before rerunning." } admin_token() { @@ -661,10 +780,9 @@ run_auth_self_check() { log "Telegram redirect: $telegram_redirect" } -ensure_backend() { +ensure_server_backend() { ensure_basic_tools - start_mysql - start_redis + log "Using shared dependencies: MySQL $(mysql_runtime_host):$(mysql_runtime_port), Redis $(redis_runtime_host):$(redis_runtime_port)" ensure_server_process initialize_backend_if_needed configure_oauth_methods @@ -672,9 +790,16 @@ ensure_backend() { } start_frontend() { - ensure_backend + local api_base_url="${1:-}" + + ensure_frontend_tools ensure_state_dir + if [[ -z "$api_base_url" ]] && wait_for_http_status "http://${LOCAL_FRONTEND_HOST}:$(frontend_port)" 5 1; then + log "Frontend is already reachable at http://${LOCAL_FRONTEND_HOST}:$(frontend_port); reusing existing dev server" + return + fi + local pid pid="$(read_pid "$FRONTEND_PID_FILE")" if is_pid_running "$pid"; then @@ -688,19 +813,44 @@ start_frontend() { log "Starting local frontend (${FRONTEND_APP})" : >"$FRONTEND_LOG_FILE" - VITE_API_BASE_URL="$SERVER_URL" \ - VITE_API_PREFIX="" \ - start_detached_process \ - "$FRONTEND_PID_FILE" \ - "$FRONTEND_LOG_FILE" \ - "$(frontend_dir)" \ - /bin/sh -lc "$(frontend_dev_command)" + if [[ -n "$api_base_url" ]]; then + VITE_API_BASE_URL="$api_base_url" \ + VITE_API_PREFIX="" \ + start_detached_process \ + "$FRONTEND_PID_FILE" \ + "$FRONTEND_LOG_FILE" \ + "$(frontend_dir)" \ + /bin/sh -lc "$(frontend_dev_command)" + else + start_detached_process \ + "$FRONTEND_PID_FILE" \ + "$FRONTEND_LOG_FILE" \ + "$(frontend_dir)" \ + /bin/sh -lc "$(frontend_dev_command)" + fi if ! wait_for_http "http://${LOCAL_FRONTEND_HOST}:$(frontend_port)" 90 1; then die "Frontend did not become ready. See $FRONTEND_LOG_FILE" fi } +up_frontend() { + start_frontend + intercept_frontend_traffic +} + +up_server() { + ensure_server_backend + intercept_server_traffic +} + +up_both() { + ensure_server_backend + start_frontend "$SERVER_URL" + intercept_server_traffic + intercept_frontend_traffic +} + stop_pid_file() { local file="$1" local label="$2" @@ -715,6 +865,7 @@ stop_pid_file() { } leave_server() { + telepresence_leave_intercept "ppanel-server" if docker_container_exists "$SERVER_CONTAINER"; then log "Stopping local server container: $SERVER_CONTAINER" docker rm -f "$SERVER_CONTAINER" >/dev/null 2>&1 || true @@ -722,6 +873,8 @@ leave_server() { } leave_frontend() { + telepresence_leave_intercept "ppanel-admin-web" + telepresence_leave_intercept "ppanel-user-web" stop_pid_file "$FRONTEND_PID_FILE" "frontend dev server" } @@ -734,48 +887,36 @@ status() { printf 'Server process: %s\n' "$(docker_container_running "$SERVER_CONTAINER" && printf 'running (container %s)' "$SERVER_CONTAINER" || printf 'stopped')" printf 'Frontend process: %s\n' "$(is_pid_running "$frontend_pid" && printf 'running (pid %s)' "$frontend_pid" || printf 'stopped')" - printf 'MySQL container: %s\n' "$(docker_container_running "$MYSQL_CONTAINER" && printf 'running' || printf 'stopped')" - printf 'Redis container: %s\n' "$(docker_container_running "$REDIS_CONTAINER" && printf 'running' || printf 'stopped')" + printf 'MySQL target: %s:%s\n' "$(mysql_runtime_host)" "$(mysql_runtime_port)" + printf 'Redis target: %s:%s\n' "$(redis_runtime_host)" "$(redis_runtime_port)" + printf 'K8s namespace: %s\n' "$K8S_NAMESPACE" + printf 'TP manager ns: %s\n' "$TELEPRESENCE_MANAGER_NAMESPACE" printf 'Server URL: %s\n' "$SERVER_URL" printf 'Frontend URL: %s\n' "http://${LOCAL_FRONTEND_HOST}:$(frontend_port)" printf 'Server log: %s\n' "$SERVER_LOG_FILE" printf 'Frontend log: %s\n' "$FRONTEND_LOG_FILE" + + if command -v telepresence >/dev/null 2>&1; then + printf '\nTelepresence:\n' + telepresence status 2>&1 || true + telepresence list --intercepts 2>&1 || true + fi } case "$ACTION" in up) case "$TARGET" in frontend) - while [[ $# -gt 0 ]]; do - case "$1" in - --frontend) - FRONTEND_APP="${2:-user}" - shift 2 - ;; - *) - die "Unknown argument: $1" - ;; - esac - done - start_frontend + parse_frontend_and_dependency_options "$@" + up_frontend ;; server) - ensure_backend + parse_shared_dependency_options "$@" + up_server ;; both) - while [[ $# -gt 0 ]]; do - case "$1" in - --frontend) - FRONTEND_APP="${2:-user}" - shift 2 - ;; - *) - die "Unknown argument: $1" - ;; - esac - done - ensure_backend - start_frontend + parse_frontend_and_dependency_options "$@" + up_both ;; *) usage @@ -784,7 +925,8 @@ case "$ACTION" in esac ;; test-auth) - ensure_backend + parse_shared_dependency_options "$@" + ensure_server_backend ;; status) status From 619ae769f6b87c8973b4f0958a4540ed89c5cb2a Mon Sep 17 00:00:00 2001 From: phenix3443 Date: Thu, 16 Apr 2026 13:32:29 +0800 Subject: [PATCH 3/6] chore: adopt dev branch strategy --- AGENT.md | 26 ++++++++++++++++++++++++++ CLAUDE.md | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 AGENT.md create mode 100644 CLAUDE.md diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..9334ec1 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,26 @@ +# Branch Strategy + +This repository uses a simplified Git Flow model. + +## Long-lived branches +- `main` is the production branch. +- `dev` is the integration branch for daily development. + +## Working branches +- Create feature work on `feat/*` from `dev`. +- Create bug-fix work on `fix/*` from `dev`. +- Merge `feat/*` and `fix/*` back into `dev` first. +- Promote changes from `dev` to `main` through a pull request. + +## Protection and deployment rules +- Never push directly to `main`. +- `main` must only be updated by a `dev -> main` pull request. +- The k3s prod environment deploys the latest `main`. +- The k3s dev environment deploys the latest `dev`. + +## Worktree workflow +```bash +git fetch origin +git worktree add ../ppanel-script-dev dev +git worktree add -b feat/your-change ../ppanel-script-feat dev +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9334ec1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,26 @@ +# Branch Strategy + +This repository uses a simplified Git Flow model. + +## Long-lived branches +- `main` is the production branch. +- `dev` is the integration branch for daily development. + +## Working branches +- Create feature work on `feat/*` from `dev`. +- Create bug-fix work on `fix/*` from `dev`. +- Merge `feat/*` and `fix/*` back into `dev` first. +- Promote changes from `dev` to `main` through a pull request. + +## Protection and deployment rules +- Never push directly to `main`. +- `main` must only be updated by a `dev -> main` pull request. +- The k3s prod environment deploys the latest `main`. +- The k3s dev environment deploys the latest `dev`. + +## Worktree workflow +```bash +git fetch origin +git worktree add ../ppanel-script-dev dev +git worktree add -b feat/your-change ../ppanel-script-feat dev +``` From d5a818813525b2f0580a84b64485cb402ab80a36 Mon Sep 17 00:00:00 2001 From: phenix3443 Date: Thu, 16 Apr 2026 13:46:59 +0800 Subject: [PATCH 4/6] docs: deduplicate agent instructions --- AGENT.md | 28 ++++------------------------ BRANCH_STRATEGY.md | 26 ++++++++++++++++++++++++++ CLAUDE.md | 28 ++++------------------------ 3 files changed, 34 insertions(+), 48 deletions(-) create mode 100644 BRANCH_STRATEGY.md diff --git a/AGENT.md b/AGENT.md index 9334ec1..f785639 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,26 +1,6 @@ -# Branch Strategy +# AGENT Entry -This repository uses a simplified Git Flow model. +Common branch and worktree instructions are maintained in [BRANCH_STRATEGY.md](./BRANCH_STRATEGY.md). -## Long-lived branches -- `main` is the production branch. -- `dev` is the integration branch for daily development. - -## Working branches -- Create feature work on `feat/*` from `dev`. -- Create bug-fix work on `fix/*` from `dev`. -- Merge `feat/*` and `fix/*` back into `dev` first. -- Promote changes from `dev` to `main` through a pull request. - -## Protection and deployment rules -- Never push directly to `main`. -- `main` must only be updated by a `dev -> main` pull request. -- The k3s prod environment deploys the latest `main`. -- The k3s dev environment deploys the latest `dev`. - -## Worktree workflow -```bash -git fetch origin -git worktree add ../ppanel-script-dev dev -git worktree add -b feat/your-change ../ppanel-script-feat dev -``` +Agent-specific note: +- Follow `BRANCH_STRATEGY.md` as the source of truth for branching, promotion, and deployment rules in this repository. diff --git a/BRANCH_STRATEGY.md b/BRANCH_STRATEGY.md new file mode 100644 index 0000000..9334ec1 --- /dev/null +++ b/BRANCH_STRATEGY.md @@ -0,0 +1,26 @@ +# Branch Strategy + +This repository uses a simplified Git Flow model. + +## Long-lived branches +- `main` is the production branch. +- `dev` is the integration branch for daily development. + +## Working branches +- Create feature work on `feat/*` from `dev`. +- Create bug-fix work on `fix/*` from `dev`. +- Merge `feat/*` and `fix/*` back into `dev` first. +- Promote changes from `dev` to `main` through a pull request. + +## Protection and deployment rules +- Never push directly to `main`. +- `main` must only be updated by a `dev -> main` pull request. +- The k3s prod environment deploys the latest `main`. +- The k3s dev environment deploys the latest `dev`. + +## Worktree workflow +```bash +git fetch origin +git worktree add ../ppanel-script-dev dev +git worktree add -b feat/your-change ../ppanel-script-feat dev +``` diff --git a/CLAUDE.md b/CLAUDE.md index 9334ec1..bcf75ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,26 +1,6 @@ -# Branch Strategy +# CLAUDE Entry -This repository uses a simplified Git Flow model. +Common branch and worktree instructions are maintained in [BRANCH_STRATEGY.md](./BRANCH_STRATEGY.md). -## Long-lived branches -- `main` is the production branch. -- `dev` is the integration branch for daily development. - -## Working branches -- Create feature work on `feat/*` from `dev`. -- Create bug-fix work on `fix/*` from `dev`. -- Merge `feat/*` and `fix/*` back into `dev` first. -- Promote changes from `dev` to `main` through a pull request. - -## Protection and deployment rules -- Never push directly to `main`. -- `main` must only be updated by a `dev -> main` pull request. -- The k3s prod environment deploys the latest `main`. -- The k3s dev environment deploys the latest `dev`. - -## Worktree workflow -```bash -git fetch origin -git worktree add ../ppanel-script-dev dev -git worktree add -b feat/your-change ../ppanel-script-feat dev -``` +Claude-specific note: +- Follow `BRANCH_STRATEGY.md` as the shared source of truth before making branch, PR, or deployment decisions in this repository. From 38f365c2c4e5da55b95579ef3553fc6b924a9510 Mon Sep 17 00:00:00 2001 From: phenix3443 Date: Fri, 17 Apr 2026 17:14:28 +0800 Subject: [PATCH 5/6] docs: update branch strategy --- BRANCH_STRATEGY.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/BRANCH_STRATEGY.md b/BRANCH_STRATEGY.md index 9334ec1..7464dee 100644 --- a/BRANCH_STRATEGY.md +++ b/BRANCH_STRATEGY.md @@ -1,26 +1,23 @@ # Branch Strategy -This repository uses a simplified Git Flow model. +This repository uses a main-first task branch model. ## Long-lived branches -- `main` is the production branch. -- `dev` is the integration branch for daily development. +- `main` is the only long-lived development branch. ## Working branches -- Create feature work on `feat/*` from `dev`. -- Create bug-fix work on `fix/*` from `dev`. -- Merge `feat/*` and `fix/*` back into `dev` first. -- Promote changes from `dev` to `main` through a pull request. +- Create feature work on `feat/*` directly from `main`. +- Create bug-fix work on `fix/*` directly from `main`. +- Open pull requests from `feat/*` and `fix/*` back into `main`. ## Protection and deployment rules - Never push directly to `main`. -- `main` must only be updated by a `dev -> main` pull request. +- `main` must only be updated by task branch pull requests. - The k3s prod environment deploys the latest `main`. -- The k3s dev environment deploys the latest `dev`. ## Worktree workflow ```bash git fetch origin -git worktree add ../ppanel-script-dev dev -git worktree add -b feat/your-change ../ppanel-script-feat dev +git worktree add ../ppanel-script-main main +git worktree add -b feat/your-change ../ppanel-script-feat main ``` From a8de8dc56941d45df86650355fa06762e98a55ca Mon Sep 17 00:00:00 2001 From: phenix3443 Date: Fri, 17 Apr 2026 18:39:56 +0800 Subject: [PATCH 6/6] ignore local agent instruction files --- .gitignore | 8 ++++++++ AGENT.md | 6 ------ BRANCH_STRATEGY.md | 23 ----------------------- CLAUDE.md | 6 ------ 4 files changed, 8 insertions(+), 35 deletions(-) create mode 100644 .gitignore delete mode 100644 AGENT.md delete mode 100644 BRANCH_STRATEGY.md delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..771d7a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Local agent instructions +AGENT.md +AGENTS.md +agent.md +agents.md +CLAUDE.md +claude.md +BRANCH_STRATEGY.md diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index f785639..0000000 --- a/AGENT.md +++ /dev/null @@ -1,6 +0,0 @@ -# AGENT Entry - -Common branch and worktree instructions are maintained in [BRANCH_STRATEGY.md](./BRANCH_STRATEGY.md). - -Agent-specific note: -- Follow `BRANCH_STRATEGY.md` as the source of truth for branching, promotion, and deployment rules in this repository. diff --git a/BRANCH_STRATEGY.md b/BRANCH_STRATEGY.md deleted file mode 100644 index 7464dee..0000000 --- a/BRANCH_STRATEGY.md +++ /dev/null @@ -1,23 +0,0 @@ -# Branch Strategy - -This repository uses a main-first task branch model. - -## Long-lived branches -- `main` is the only long-lived development branch. - -## Working branches -- Create feature work on `feat/*` directly from `main`. -- Create bug-fix work on `fix/*` directly from `main`. -- Open pull requests from `feat/*` and `fix/*` back into `main`. - -## Protection and deployment rules -- Never push directly to `main`. -- `main` must only be updated by task branch pull requests. -- The k3s prod environment deploys the latest `main`. - -## Worktree workflow -```bash -git fetch origin -git worktree add ../ppanel-script-main main -git worktree add -b feat/your-change ../ppanel-script-feat main -``` diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index bcf75ed..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,6 +0,0 @@ -# CLAUDE Entry - -Common branch and worktree instructions are maintained in [BRANCH_STRATEGY.md](./BRANCH_STRATEGY.md). - -Claude-specific note: -- Follow `BRANCH_STRATEGY.md` as the shared source of truth before making branch, PR, or deployment decisions in this repository.