diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/app.py b/app.py
index 805d15e..7b6f06f 100644
--- a/app.py
+++ b/app.py
@@ -403,6 +403,23 @@ def generate_failover_conf_for_client(client_id):
return "\n".join(lines) + "\n"
+def panel_version():
+ try:
+ return open(os.path.join(PANEL_DIR, ".version")).read().strip()
+ except Exception:
+ return "unknown"
+
+
+def run_update(arg):
+ import subprocess
+ try:
+ r = subprocess.run(["/opt/Phobos/server/update.sh", arg],
+ capture_output=True, text=True, timeout=150)
+ return (r.stdout + r.stderr)[-1200:]
+ except Exception as e:
+ return "update error: " + str(e)
+
+
def build_phone_config(cid, mode="android"):
"""Assemble a phone client config from the client's files.
android = WireGuard + [instance] obfuscator -> phobos:// link (PhobosWG app).
@@ -1088,6 +1105,16 @@ def labels_page():
def settings_page():
s = load_settings()
msg = ""
+ if request.method == "POST" and request.form.get("action") in ("update", "rollback"):
+ if request.form.get("action") == "update":
+ _out = run_update(request.form.get("target", "").strip() or "main")
+ else:
+ _out = run_update("--rollback")
+ s = load_settings()
+ return render(f'''
''')
if request.method == "POST":
new_pass = request.form.get("admin_pass", "").strip()
if new_pass:
@@ -1116,6 +1143,20 @@ def settings_page():
+
+
{tr("Версия и обновления","Version & updates")} {hlp("Скачивает свежие файлы PCA-слоя (панель + скрипты) с GitHub и перезапускает панель. Ключи WireGuard, server.env и клиенты НЕ трогаются. Пусто или main = последняя версия; можно указать тег (например v1.1.0). Откат восстанавливает предыдущую версию из автоматического бэкапа.", "Fetches the latest PCA files (panel + scripts) from GitHub and restarts the panel. WireGuard keys, server.env and clients are NOT touched. Empty or main = latest; you can pass a tag (e.g. v1.1.0). Rollback restores the previous version from an automatic backup.")}
+
{tr("Установленная версия","Installed version")}: {panel_version()}
+
+
+
+
{tr("Информация о сервере","Server info")}
diff --git a/install.sh b/install.sh
index b436aca..3cd789e 100755
--- a/install.sh
+++ b/install.sh
@@ -171,6 +171,8 @@ fetch overlay/phobos-pull.sh "$PHOBOS_DIR/repo/client/templat
fetch server/phobos-health.sh "$PHOBOS_DIR/server/phobos-health.sh" && chmod +x "$PHOBOS_DIR/server/phobos-health.sh"
fetch server/phobos-pull.sh "$PHOBOS_DIR/server/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/server/phobos-pull.sh"
fetch server/phobos-router-watchdog.py "$PHOBOS_DIR/server/phobos-router-watchdog.py"
+fetch update.sh "$PHOBOS_DIR/server/update.sh" && chmod +x "$PHOBOS_DIR/server/update.sh" && ln -sf "$PHOBOS_DIR/server/update.sh" /usr/local/bin/phobos-update
+mkdir -p "$PANEL_DIR"; fetch VERSION "$PANEL_DIR/.version" 2>/dev/null || true
[ -f "$PHOBOS_DIR/tokens/tokens.json" ] || echo '[]' > "$PHOBOS_DIR/tokens/tokens.json"
# ── 7. web panel ──
diff --git a/update.sh b/update.sh
new file mode 100755
index 0000000..17a2bbe
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+# ============================================================
+# PCA Phobos — updater / rollback
+#
+# phobos-update # обновить до последней версии (main)
+# phobos-update v1.1.0 # обновить до конкретной версии (git tag)
+# phobos-update --rollback # откатить на предыдущую версию (из бэкапа)
+# phobos-update --version # показать установленную и последнюю версию
+# phobos-update --list # список бэкапов для отката
+#
+# Обновляет ТОЛЬКО PCA-слой (панель + скрипты онбординга/служебные).
+# НЕ трогает ключи WireGuard, server.env, клиентов — они сохраняются.
+# Перед каждым обновлением делает бэкап; откат восстанавливает его.
+# ============================================================
+set -e
+PHOBOS_DIR=/opt/Phobos
+PANEL_DIR=/opt/phobos-panel
+REPO=andrey271192/PCA_Phobos
+RAW="https://raw.githubusercontent.com/$REPO"
+BACKUPS="$PHOBOS_DIR/updates"
+VERFILE="$PANEL_DIR/.version"
+CUR=$(cat "$VERFILE" 2>/dev/null || echo "unknown")
+
+managed_files() {
+cat </dev/null | tr -d ' \r\n'; }
+
+do_backup() {
+ local dir="$BACKUPS/${CUR}-$(date +%Y%m%d-%H%M%S)"
+ mkdir -p "$dir"
+ managed_files | while IFS='|' read -r lp rp; do
+ [ -f "$lp" ] && install -D "$lp" "$dir/$rp" 2>/dev/null || true
+ done
+ echo "$CUR" > "$dir/.fromversion"
+ echo "$dir"
+}
+
+apply_ref() {
+ local ref="$1"
+ managed_files | while IFS='|' read -r lp rp; do
+ local tmp; tmp=$(mktemp)
+ if curl -fsSL -m25 "$RAW/$ref/$rp" -o "$tmp" && [ -s "$tmp" ]; then
+ mkdir -p "$(dirname "$lp")"; mv "$tmp" "$lp"
+ case "$lp" in *.sh|*.py) chmod +x "$lp" 2>/dev/null || true;; esac
+ else
+ rm -f "$tmp"; echo " WARN: не удалось скачать $rp (пропуск)"
+ fi
+ done
+}
+
+case "${1:-}" in
+ --version|-v)
+ echo "Установлено: $CUR"
+ echo "Доступно: $(latest_version)"
+ ;;
+ --list)
+ echo "Бэкапы (для отката):"
+ ls -dt "$BACKUPS"/*/ 2>/dev/null | sed 's#.*/updates/##' || echo " нет"
+ ;;
+ --rollback)
+ last=$(ls -dt "$BACKUPS"/*/ 2>/dev/null | head -1)
+ [ -z "$last" ] && { echo "Нет бэкапа для отката."; exit 1; }
+ fromver=$(cat "$last/.fromversion" 2>/dev/null || echo "?")
+ echo "Откат на $fromver ($last)"
+ managed_files | while IFS='|' read -r lp rp; do
+ [ -f "$last/$rp" ] && { mkdir -p "$(dirname "$lp")"; cp "$last/$rp" "$lp"; chmod +x "$lp" 2>/dev/null || true; }
+ done
+ echo "$fromver" > "$VERFILE"
+ systemctl restart phobos-panel 2>/dev/null || true
+ echo "Откат выполнен -> $fromver"
+ ;;
+ *)
+ ref="${1:-main}"; [ "$ref" = "latest" ] && ref="main"
+ target=$(curl -fsSL -m10 "$RAW/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
+ echo "Текущая: $CUR -> целевая: $target ($ref)"
+ b=$(do_backup); echo "Бэкап: $b"
+ apply_ref "$ref"
+ echo "$target" > "$VERFILE"
+ systemctl restart phobos-panel 2>/dev/null || true
+ echo "Обновлено до $target. Откат: phobos-update --rollback"
+ ;;
+esac