Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc83d90a35 | ||
|
|
e0ea54e83a | ||
|
|
628973374e | ||
|
|
041a533457 | ||
|
|
48ae0c2fd8 |
10
README.md
10
README.md
@@ -39,6 +39,7 @@
|
|||||||
- `/add <имя>` — создать клиента (бот пришлёт команду установки)
|
- `/add <имя>` — создать клиента (бот пришлёт команду установки)
|
||||||
- `/del <имя>` — удалить клиента
|
- `/del <имя>` — удалить клиента
|
||||||
- `/list` — список клиентов
|
- `/list` — список клиентов
|
||||||
|
- **Кнопки** — постоянная клавиатура: ➕ Создать клиента · 📋 Список · 🗑 Удалить (инлайн-выбор) · 📊 Статус · ❓ Помощь.
|
||||||
(Работает только для чата из настроек `tg_chat_id`.)
|
(Работает только для чата из настроек `tg_chat_id`.)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -76,9 +77,16 @@ bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main
|
|||||||
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
||||||
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
||||||
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
||||||
|
| `CHANNEL` | `stable` | канал: `stable` (ветка main) или `beta` |
|
||||||
|
|
||||||
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
||||||
|
|
||||||
|
> **Канал:** по умолчанию ставится **стабильная** версия (ветка `main`). Бета — по желанию:
|
||||||
|
> ```bash
|
||||||
|
> CHANNEL=beta bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||||
|
> ```
|
||||||
|
> Переключение после установки: `phobos-update beta` / `phobos-update main`.
|
||||||
|
|
||||||
### Вторичный сервер (для failover / балансировки)
|
### Вторичный сервер (для failover / балансировки)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -168,7 +176,7 @@ phobos-update --list # список бэкапов
|
|||||||
|
|
||||||
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
|
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
|
||||||
- 💳 **Ozon Bank (СБП):** [ссылка](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
- 💳 **Ozon Bank (СБП):** [ссылка](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
||||||
- ✉️ **Telegram:** [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, баги, идеи
|
- ✉️ **Telegram:** [@PCAdministration](https://t.me/PCAdministration) — вопросы, баги, идеи
|
||||||
|
|
||||||
## На основе и благодарности
|
## На основе и благодарности
|
||||||
|
|
||||||
|
|||||||
239
app.py
239
app.py
@@ -129,10 +129,102 @@ def delete_client_cli(cid):
|
|||||||
return True, "\U0001F5D1 Клиент <b>%s</b> удалён." % cid
|
return True, "\U0001F5D1 Клиент <b>%s</b> удалён." % cid
|
||||||
|
|
||||||
|
|
||||||
def telegram_bot():
|
def tg_api(method, payload, token=None):
|
||||||
"""Poll Telegram getUpdates; allow the admin chat to /add, /del, /list clients."""
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
if token is None:
|
||||||
|
token = load_settings().get("tg_bot_token", "")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request("https://api.telegram.org/bot%s/%s" % (token, method),
|
||||||
|
data=data, headers={"Content-Type": "application/json"})
|
||||||
|
return json.loads(urllib.request.urlopen(req, timeout=15).read())
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
MAIN_KB = {
|
||||||
|
"keyboard": [
|
||||||
|
["➕ Создать клиента", "\U0001F4CB Список"],
|
||||||
|
["\U0001F5D1 Удалить", "\U0001F4CA Статус"],
|
||||||
|
["❓ Помощь"],
|
||||||
|
],
|
||||||
|
"resize_keyboard": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
_bot_state = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _bot_status_text():
|
||||||
|
servers = [{"ip": SERVER_IP, "is_primary": True}] + load_servers()
|
||||||
|
lines = ["\U0001F4CA <b>Серверы</b>:"]
|
||||||
|
for sv in servers:
|
||||||
|
ip = sv.get("ip", "")
|
||||||
|
st = "ok" if ip == SERVER_IP else server_stats_cache.get(ip, {}).get("status", "?")
|
||||||
|
emoji = "\U0001F7E2" if st == "ok" else ("\U0001F534" if st == "down" else "⚪")
|
||||||
|
lines.append("%s %s%s" % (emoji, ip, " (primary)" if sv.get("is_primary") else ""))
|
||||||
|
online = set()
|
||||||
|
try:
|
||||||
|
for p, info in get_wg_peers().items():
|
||||||
|
if is_peer_online(info.get("latest handshake", "")):
|
||||||
|
online.add(p)
|
||||||
|
for _ip, hs in server_handshakes_cache.items():
|
||||||
|
for p, tsv in hs.items():
|
||||||
|
if tsv and (time.time() - tsv) < 180:
|
||||||
|
online.add(p)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
lines.append("\n\U0001F4F1 <b>Клиенты</b>:")
|
||||||
|
cl = get_clients()
|
||||||
|
if not cl:
|
||||||
|
lines.append("нет")
|
||||||
|
for c in cl:
|
||||||
|
on = c.get("public_key", "") in online
|
||||||
|
lines.append(("\U0001F7E2 " if on else "⚪ ") + c.get("client_id", ""))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def qr_png_bytes(data):
|
||||||
|
import qrcode, io
|
||||||
|
buf = io.BytesIO(); qrcode.make(data).save(buf, format="PNG"); return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def tg_send_photo(chat, png, caption=""):
|
||||||
|
import urllib.request, secrets as _sec
|
||||||
|
token = load_settings().get("tg_bot_token", "")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
boundary = "----phobos" + _sec.token_hex(8)
|
||||||
|
def fld(name, val):
|
||||||
|
return ("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n" % (boundary, name, val)).encode()
|
||||||
|
body = fld("chat_id", str(chat)) + fld("caption", caption) + fld("parse_mode", "HTML")
|
||||||
|
body += ("--%s\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"qr.png\"\r\nContent-Type: image/png\r\n\r\n" % boundary).encode()
|
||||||
|
body += png + ("\r\n--%s--\r\n" % boundary).encode()
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request("https://api.telegram.org/bot%s/sendPhoto" % token, data=body,
|
||||||
|
headers={"Content-Type": "multipart/form-data; boundary=%s" % boundary})
|
||||||
|
return urllib.request.urlopen(req, timeout=25).read()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def bot_send_new_client(chat, name):
|
||||||
|
ok, rep = create_client_cli(name)
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
|
if ok:
|
||||||
|
try:
|
||||||
|
conf, link = build_phone_config(name, "android")
|
||||||
|
if link:
|
||||||
|
tg_send_photo(chat, qr_png_bytes(link), "📱 Android (PhobosWG) — отсканируй QR в приложении")
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": "phobos:// ссылка (Android):\n<code>%s</code>" % link, "parse_mode": "HTML"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def telegram_bot():
|
||||||
offset = 0
|
offset = 0
|
||||||
|
import urllib.request
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
@@ -144,36 +236,85 @@ def telegram_bot():
|
|||||||
data = json.loads(urllib.request.urlopen(url, timeout=40).read())
|
data = json.loads(urllib.request.urlopen(url, timeout=40).read())
|
||||||
for upd in data.get("result", []):
|
for upd in data.get("result", []):
|
||||||
offset = upd["update_id"] + 1
|
offset = upd["update_id"] + 1
|
||||||
|
cq = upd.get("callback_query")
|
||||||
|
if cq:
|
||||||
|
if str(cq.get("message", {}).get("chat", {}).get("id", "")) != chat:
|
||||||
|
continue
|
||||||
|
cdata = cq.get("data", "")
|
||||||
|
if cdata.startswith("del:"):
|
||||||
|
_, rep = delete_client_cli(cdata[4:])
|
||||||
|
tg_api("answerCallbackQuery", {"callback_query_id": cq["id"], "text": "OK"})
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
|
continue
|
||||||
m = upd.get("message") or upd.get("channel_post")
|
m = upd.get("message") or upd.get("channel_post")
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
if str(m.get("chat", {}).get("id", "")) != chat:
|
if str(m.get("chat", {}).get("id", "")) != chat:
|
||||||
continue
|
continue
|
||||||
text = (m.get("text") or "").strip()
|
text = (m.get("text") or "").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
# detect reply-keyboard button by its emoji prefix (NOT a command/arg)
|
||||||
|
btn = None
|
||||||
if not text.startswith("/"):
|
if not text.startswith("/"):
|
||||||
continue
|
if text.startswith("➕"):
|
||||||
parts = text.split()
|
btn = "add"
|
||||||
cmd = parts[0].split("@")[0].lower()
|
elif text.startswith("\U0001F5D1"):
|
||||||
arg = parts[1] if len(parts) > 1 else ""
|
btn = "del"
|
||||||
if cmd in ("/add", "/new", "/create"):
|
elif text.startswith("\U0001F4CB"):
|
||||||
reply = "Использование: /add <имя>" if not arg else create_client_cli(arg)[1]
|
btn = "list"
|
||||||
elif cmd in ("/del", "/delete", "/remove", "/rm"):
|
elif text.startswith("\U0001F4CA"):
|
||||||
reply = "Использование: /del <имя>" if not arg else delete_client_cli(arg)[1]
|
btn = "status"
|
||||||
elif cmd in ("/list", "/ls"):
|
elif text.startswith("❓"):
|
||||||
cls = [c.get("client_id", "") for c in get_clients()]
|
btn = "help"
|
||||||
reply = ("Клиенты (%d):\n" % len(cls)) + "\n".join("\u2022 " + c for c in cls) if cls else "Клиентов нет."
|
# conversation: awaiting a client name to create
|
||||||
elif cmd in ("/help", "/start"):
|
if _bot_state.get(chat) == "add":
|
||||||
reply = ("Команды PCA Phobos:\n"
|
if btn is None and not text.startswith("/"):
|
||||||
"/add <имя> — создать клиента\n"
|
_bot_state.pop(chat, None)
|
||||||
"/del <имя> — удалить клиента\n"
|
bot_send_new_client(chat, text)
|
||||||
"/list — список клиентов")
|
continue
|
||||||
else:
|
_bot_state.pop(chat, None) # a button/command cancels the prompt
|
||||||
continue
|
# parse slash command + arg (buttons carry no arg)
|
||||||
tg_send(reply)
|
cmd = ""
|
||||||
|
arg = ""
|
||||||
|
if text.startswith("/"):
|
||||||
|
toks = text.split()
|
||||||
|
cmd = toks[0].split("@")[0].lower()
|
||||||
|
arg = toks[1] if len(toks) > 1 else ""
|
||||||
|
if btn == "add" or cmd in ("/add", "/new", "/create"):
|
||||||
|
if arg:
|
||||||
|
bot_send_new_client(chat, arg)
|
||||||
|
else:
|
||||||
|
_bot_state[chat] = "add"
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": "Введите имя нового клиента (латиница, цифры, _ и -):"})
|
||||||
|
elif btn == "del" or cmd in ("/del", "/delete", "/remove", "/rm"):
|
||||||
|
if arg:
|
||||||
|
_, rep = delete_client_cli(arg)
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
|
else:
|
||||||
|
cl = get_clients()
|
||||||
|
if not cl:
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": "Клиентов нет.", "reply_markup": MAIN_KB})
|
||||||
|
else:
|
||||||
|
kb = {"inline_keyboard": [[{"text": "\U0001F5D1 " + c.get("client_id", ""), "callback_data": "del:" + c.get("client_id", "")}] for c in cl]}
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": "Кого удалить? Нажми на клиента:", "reply_markup": kb})
|
||||||
|
elif btn == "list" or cmd in ("/list", "/ls"):
|
||||||
|
cl = [c.get("client_id", "") for c in get_clients()]
|
||||||
|
rep = ("Клиенты (%d):\n" % len(cl)) + "\n".join("• " + c for c in cl) if cl else "Клиентов нет."
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "reply_markup": MAIN_KB})
|
||||||
|
elif btn == "status" or cmd in ("/status", "/stat"):
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": _bot_status_text(), "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
|
elif btn == "help" or cmd in ("/help", "/start"):
|
||||||
|
rep = ("<b>PCA Phobos — бот</b>\n\n"
|
||||||
|
"Кнопки внизу или команды:\n"
|
||||||
|
"➕ /add <имя> — создать клиента\n"
|
||||||
|
"\U0001F5D1 /del <имя> — удалить\n"
|
||||||
|
"\U0001F4CB /list — список\n"
|
||||||
|
"\U0001F4CA /status — статус")
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
except Exception:
|
except Exception:
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
def get_wg_peers():
|
def get_wg_peers():
|
||||||
"""Parse `wg show wg0` to get active peers with transfer/handshake info."""
|
"""Parse `wg show wg0` to get active peers with transfer/handshake info."""
|
||||||
try:
|
try:
|
||||||
@@ -338,6 +479,7 @@ def kick_client_by_id(client_id):
|
|||||||
|
|
||||||
prev_session_keys = None
|
prev_session_keys = None
|
||||||
prev_server_status = {}
|
prev_server_status = {}
|
||||||
|
_down_streak = {}
|
||||||
_last_fanout = 0
|
_last_fanout = 0
|
||||||
server_stats_cache = {}
|
server_stats_cache = {}
|
||||||
server_handshakes_cache = {}
|
server_handshakes_cache = {}
|
||||||
@@ -362,12 +504,19 @@ def get_local_stats():
|
|||||||
|
|
||||||
|
|
||||||
def get_remote_stats(server):
|
def get_remote_stats(server):
|
||||||
|
import urllib.request
|
||||||
|
url = f"http://{server['ip']}:8444/api/health"
|
||||||
|
req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")})
|
||||||
|
data = None
|
||||||
|
for _attempt in range(2): # retry once: smooths transient timeouts (1-worker agent)
|
||||||
|
try:
|
||||||
|
data = json.loads(urllib.request.urlopen(req, timeout=6).read())
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
data = None
|
||||||
try:
|
try:
|
||||||
import urllib.request
|
if data is None:
|
||||||
url = f"http://{server['ip']}:8444/api/health"
|
raise Exception("no response")
|
||||||
req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")})
|
|
||||||
resp = urllib.request.urlopen(req, timeout=3)
|
|
||||||
data = json.loads(resp.read())
|
|
||||||
# cache handshakes so page renders never block on remote HTTP
|
# cache handshakes so page renders never block on remote HTTP
|
||||||
server_handshakes_cache[server.get("ip", "")] = data.get("handshakes", {}) or {}
|
server_handshakes_cache[server.get("ip", "")] = data.get("handshakes", {}) or {}
|
||||||
# Filter peers: only count known client public keys
|
# Filter peers: only count known client public keys
|
||||||
@@ -636,7 +785,7 @@ def push_config_to_router(client_id):
|
|||||||
|
|
||||||
|
|
||||||
def session_monitor():
|
def session_monitor():
|
||||||
global prev_session_keys, prev_server_status, server_stats_cache, _last_fanout
|
global prev_session_keys, prev_server_status, server_stats_cache, _last_fanout, _down_streak
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
@@ -673,19 +822,24 @@ def session_monitor():
|
|||||||
local_stats = get_local_stats()
|
local_stats = get_local_stats()
|
||||||
server_stats_cache[SERVER_IP] = local_stats
|
server_stats_cache[SERVER_IP] = local_stats
|
||||||
|
|
||||||
|
DOWN_CONFIRM = 3 # consecutive failed cycles before declaring a server DOWN
|
||||||
for srv in servers:
|
for srv in servers:
|
||||||
ip = srv["ip"]
|
ip = srv["ip"]
|
||||||
stats = get_remote_stats(srv)
|
stats = get_remote_stats(srv)
|
||||||
server_stats_cache[ip] = stats
|
raw = stats.get("status", "down")
|
||||||
|
streak = _down_streak.get(ip, 0)
|
||||||
|
streak = streak + 1 if raw == "down" else 0
|
||||||
|
_down_streak[ip] = streak
|
||||||
|
# update cache on OK, or only once a DOWN is confirmed (keep last-good on transient)
|
||||||
|
if raw == "ok" or streak >= DOWN_CONFIRM:
|
||||||
|
server_stats_cache[ip] = stats
|
||||||
|
eff = "down" if streak >= DOWN_CONFIRM else "ok"
|
||||||
was_up = prev_server_status.get(ip, "ok")
|
was_up = prev_server_status.get(ip, "ok")
|
||||||
now_status = stats["status"]
|
if was_up == "ok" and eff == "down":
|
||||||
|
|
||||||
if was_up == "ok" and now_status == "down":
|
|
||||||
tg_send(f"🔴 <b>Server {ip}</b> DOWN!")
|
tg_send(f"🔴 <b>Server {ip}</b> DOWN!")
|
||||||
elif was_up == "down" and now_status == "ok":
|
elif was_up == "down" and eff == "ok":
|
||||||
tg_send(f"🟢 <b>Server {ip}</b> back ONLINE")
|
tg_send(f"🟢 <b>Server {ip}</b> back ONLINE")
|
||||||
|
prev_server_status[ip] = eff
|
||||||
prev_server_status[ip] = now_status
|
|
||||||
|
|
||||||
# periodic config fan-out to secondaries (covers CLI-created clients
|
# periodic config fan-out to secondaries (covers CLI-created clients
|
||||||
# and config drift; gated ~5 min, skips down servers)
|
# and config drift; gated ~5 min, skips down servers)
|
||||||
@@ -767,7 +921,7 @@ input:focus{outline:none;border-color:#4f46e5}
|
|||||||
CONTENT
|
CONTENT
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<b style="color:#a5b4fc">PCA Phobos Panel</b> — панель управления © <a href="https://github.com/andrey271192/PCA_Phobos" target="_blank" rel="noopener">andrey271192</a><br>
|
<b style="color:#a5b4fc">PCA Phobos Panel</b> — панель управления © <a href="https://github.com/andrey271192/PCA_Phobos" target="_blank" rel="noopener">andrey271192</a><br>
|
||||||
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/c/2474115507/78722" target="_blank" rel="noopener">✉️ Telegram</a>
|
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/PCAdministration" target="_blank" rel="noopener">✉️ Telegram</a>
|
||||||
<div style="margin-top:8px;font-size:.88em;color:#475569;line-height:1.7;text-align:left;max-width:660px;margin:8px auto 0">
|
<div style="margin-top:8px;font-size:.88em;color:#475569;line-height:1.7;text-align:left;max-width:660px;margin:8px auto 0">
|
||||||
<b>Создано на основе открытых проектов:</b><br>
|
<b>Создано на основе открытых проектов:</b><br>
|
||||||
• <a href="https://github.com/Ground-Zerro/Phobos" target="_blank" rel="noopener">Phobos</a> — обфусцированный WireGuard VPN, автор <b>Ground_Zerro</b> · <a href="https://boosty.to/ground_zerro" target="_blank" rel="noopener">поддержать</a><br>
|
• <a href="https://github.com/Ground-Zerro/Phobos" target="_blank" rel="noopener">Phobos</a> — обфусцированный WireGuard VPN, автор <b>Ground_Zerro</b> · <a href="https://boosty.to/ground_zerro" target="_blank" rel="noopener">поддержать</a><br>
|
||||||
@@ -1230,9 +1384,12 @@ def labels_page():
|
|||||||
def settings_page():
|
def settings_page():
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
msg = ""
|
msg = ""
|
||||||
if request.method == "POST" and request.form.get("action") in ("update", "rollback"):
|
if request.method == "POST" and request.form.get("action") in ("update", "rollback", "check"):
|
||||||
if request.form.get("action") == "update":
|
_act = request.form.get("action")
|
||||||
|
if _act == "update":
|
||||||
_out = run_update(request.form.get("target", "").strip() or "main")
|
_out = run_update(request.form.get("target", "").strip() or "main")
|
||||||
|
elif _act == "check":
|
||||||
|
_out = run_update("--check")
|
||||||
else:
|
else:
|
||||||
_out = run_update("--rollback")
|
_out = run_update("--rollback")
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
@@ -1273,9 +1430,13 @@ def settings_page():
|
|||||||
<p>{tr("Установленная версия","Installed version")}: <b style="color:#a5b4fc">{panel_version()}</b></p>
|
<p>{tr("Установленная версия","Installed version")}: <b style="color:#a5b4fc">{panel_version()}</b></p>
|
||||||
<form method="post" class="form-row">
|
<form method="post" class="form-row">
|
||||||
<input type="hidden" name="action" value="update">
|
<input type="hidden" name="action" value="update">
|
||||||
<input type="text" name="target" placeholder="main / v1.1.0" style="width:150px">
|
<input type="text" name="target" placeholder="main / beta / v1.2.3" style="width:150px">
|
||||||
<button class="btn btn-primary" type="submit" onclick="return confirm('{tr("Обновить панель и скрипты?","Update panel and scripts?")}')">{tr("Обновить","Update")}</button>
|
<button class="btn btn-primary" type="submit" onclick="return confirm('{tr("Обновить панель и скрипты?","Update panel and scripts?")}')">{tr("Обновить","Update")}</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="check">
|
||||||
|
<button class="btn btn-sm" style="background:#0ea5e9" type="submit">{tr("Проверить обновления","Check for updates")}</button>
|
||||||
|
</form>
|
||||||
<form method="post" style="margin-top:8px">
|
<form method="post" style="margin-top:8px">
|
||||||
<input type="hidden" name="action" value="rollback">
|
<input type="hidden" name="action" value="rollback">
|
||||||
<button class="btn btn-danger btn-sm" type="submit" onclick="return confirm('{tr("Откатить на предыдущую версию?","Roll back to the previous version?")}')">{tr("Откатить на предыдущую","Rollback")}</button>
|
<button class="btn btn-danger btn-sm" type="submit" onclick="return confirm('{tr("Откатить на предыдущую версию?","Roll back to the previous version?")}')">{tr("Откатить на предыдущую","Rollback")}</button>
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ PANEL_PASS="${PANEL_PASS:-OcAdmin2026!}"
|
|||||||
TG_TOKEN="${TG_TOKEN:-}"
|
TG_TOKEN="${TG_TOKEN:-}"
|
||||||
TG_CHAT="${TG_CHAT:-}"
|
TG_CHAT="${TG_CHAT:-}"
|
||||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
||||||
PCA_BRANCH="${PCA_BRANCH:-main}"
|
# Канал: stable по умолчанию (ветка main). Бета — по желанию: CHANNEL=beta (или PCA_BRANCH=beta).
|
||||||
|
CHANNEL="${CHANNEL:-stable}"
|
||||||
|
if [ -z "${PCA_BRANCH:-}" ]; then
|
||||||
|
case "$CHANNEL" in beta) PCA_BRANCH="beta";; *) PCA_BRANCH="main";; esac
|
||||||
|
fi
|
||||||
GH_TOKEN="${GH_TOKEN:-}" # set for private-repo installs; empty for public
|
GH_TOKEN="${GH_TOKEN:-}" # set for private-repo installs; empty for public
|
||||||
PHOBOS_DIR="/opt/Phobos"
|
PHOBOS_DIR="/opt/Phobos"
|
||||||
PANEL_DIR="/opt/phobos-panel"
|
PANEL_DIR="/opt/phobos-panel"
|
||||||
|
|||||||
20
update.sh
20
update.sh
@@ -60,10 +60,19 @@ apply_ref() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref_exists() { curl -fsSL -m12 "$RAW/$1/app.py" -o /dev/null 2>/dev/null; }
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
--version|-v)
|
--version|-v|--check)
|
||||||
echo "Установлено: $CUR"
|
echo "Установлено: $CUR"
|
||||||
echo "Доступно: $(latest_version)"
|
echo "Стабильная: $(latest_version)"
|
||||||
|
beta=$(curl -fsSL -m8 "$RAW/beta/VERSION" 2>/dev/null | tr -d ' \r\n')
|
||||||
|
[ -n "$beta" ] && echo "Бета (beta): $beta"
|
||||||
|
;;
|
||||||
|
--versions|--tags)
|
||||||
|
echo "Доступные версии (git tags):"
|
||||||
|
curl -fsSL -m10 "https://api.github.com/repos/$REPO/tags?per_page=100" 2>/dev/null \
|
||||||
|
| grep -oE '"name": *"[^"]+"' | sed 's/.*"name": *"\(.*\)"/ \1/' || echo " (нет данных)"
|
||||||
;;
|
;;
|
||||||
--list)
|
--list)
|
||||||
echo "Бэкапы (для отката):"
|
echo "Бэкапы (для отката):"
|
||||||
@@ -83,6 +92,11 @@ case "${1:-}" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
ref="${1:-main}"; [ "$ref" = "latest" ] && ref="main"
|
ref="${1:-main}"; [ "$ref" = "latest" ] && ref="main"
|
||||||
|
if ! ref_exists "$ref"; then
|
||||||
|
echo "Версия/ветка '$ref' не найдена на GitHub. Обновление отменено (текущая $CUR не тронута)."
|
||||||
|
echo "Доступные: phobos-update --versions · стабильная: $(latest_version)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
target=$(curl -fsSL -m10 "$RAW/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
target=$(curl -fsSL -m10 "$RAW/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
||||||
echo "Текущая: $CUR -> целевая: $target ($ref)"
|
echo "Текущая: $CUR -> целевая: $target ($ref)"
|
||||||
b=$(do_backup); echo "Бэкап: $b"
|
b=$(do_backup); echo "Бэкап: $b"
|
||||||
|
|||||||
Reference in New Issue
Block a user