From bb007ad644b051fe1c749c112541a506e15b0c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Fri, 15 May 2026 03:57:36 +0300 Subject: [PATCH] feat(panel): donor strip + Telegram sponsor in footer Public GET /api/panel/footer with env-backed URLs (defaults: Boosty + t.me sponsor). install.sh forwards PANEL_FOOTER_*. Promo only in admin SPA, not nginx landing. Co-authored-by: Cursor --- README.md | 5 ++++ package.json | 2 +- public/app.js | 57 +++++++++++++++++++++++++++++++++++++++++++++ public/index.html | 21 ++++++++++++++--- public/styles.css | 58 ++++++++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 4 +++- server.js | 44 +++++++++++++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0b34d45..8a9b391 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `AWG_PROFILES` | _(нет)_ | Несколько инстансов; см. примеры в репозитории PRO | | `COMMUNITY_UPGRADE_URL` | Страница подписки PRO на Boosty (зашита в код по умолчанию) | Переопределите, если смените уровень подписки | | `COMMUNITY_UPGRADE_PITCH` | _(текст по умолчанию в коде)_ | Текст под заголовком базовой версии | +| `PANEL_FOOTER_DONATE_URL` | Как **`COMMUNITY_UPGRADE_URL`** | Ссылка «донат» в **шапке и подвале** веб‑панели (**не** добавляется на nginx‑лендинг пользователей). | +| `PANEL_FOOTER_TELEGRAM_URL` | `https://t.me/lot_andrey` | Ссылка Telegram‑спонсора / канала (там же: только панель). | +| `PANEL_FOOTER_DONATE_LABEL` | `Поддержать проект` | Текст якоря доната | +| `PANEL_FOOTER_TELEGRAM_LABEL` | `Telegram‑канал спонсора` | Текст якоря Telegram | +| `PANEL_FOOTER_PROMO_SUBTITLE` | _(строка по умолчанию в коде)_ | Поясняющий текст в верхней полосе панели | | `SKIP_LANDING` | `0` | `1` — без лендинга на порту 80 | | `ALLOW_COMMUNITY_GITHUB_ACTIVATION` | `0` | `1` — в редакции community показывать ввод GitHub-токена и запускать приватный `install.sh` (доступ к репо с Contents Read / repo; **высокая чувствительность**, см. раздел безопасности) | | `COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL` | `https://raw.githubusercontent.com/andrey271192/amnezia_web-pro/main/scripts/install.sh` | Сырой URL `scripts/install.sh` в **вашем** приватном репозитории PRO | diff --git a/package.json b/package.json index 97df75b..3e1859a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.2.19", + "version": "1.2.20", "private": true, "description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).", "license": "MIT", diff --git a/public/app.js b/public/app.js index 71985ff..b4378dc 100644 --- a/public/app.js +++ b/public/app.js @@ -147,6 +147,62 @@ function fmtBytesRu(n) { return `${Math.round(x / (1024 * 102.4)) / 10} MiB`; } +/** Ссылки доната и Telegram‑спонсора: шапка + подвал панели (`GET /api/panel/footer`). */ +function wirePanelPromoAnchor(el, url, label) { + const u = typeof url === "string" ? url.trim() : ""; + const lb = typeof label === "string" ? label.trim() : ""; + if (!el || !u || !lb) { + if (el) { + el.classList.add("hidden"); + el.removeAttribute("href"); + el.textContent = ""; + } + return false; + } + el.href = u; + el.textContent = lb; + el.classList.remove("hidden"); + return true; +} + +async function hydratePanelPromoFooter() { + const strip = document.getElementById("panel-promo-strip"); + const sub = document.getElementById("panel-promo-subtitle"); + const aDonTop = document.getElementById("panel-strip-donate"); + const sepTop = document.getElementById("panel-strip-sep"); + const aTgTop = document.getElementById("panel-strip-telegram"); + const gap1 = document.getElementById("footer-promo-gap1"); + const aDonF = document.getElementById("footer-promo-donate"); + const sepMid = document.getElementById("footer-promo-sep-mid"); + const aTgF = document.getElementById("footer-promo-telegram"); + + try { + const r = await fetch("/api/panel/footer", { credentials: "same-origin" }); + if (!r.ok) return; + const j = await r.json().catch(() => null); + if (!j || typeof j !== "object") return; + + const subtitle = typeof j.promoSubtitle === "string" ? j.promoSubtitle.trim() : ""; + if (sub) sub.textContent = subtitle; + + const okD = wirePanelPromoAnchor(aDonTop, j.donateUrl, j.donateLabel); + const okT = wirePanelPromoAnchor(aTgTop, j.telegramUrl, j.telegramLabel); + if (sepTop) sepTop.classList.toggle("hidden", !(okD && okT)); + + wirePanelPromoAnchor(aDonF, j.donateUrl, j.donateLabel); + wirePanelPromoAnchor(aTgF, j.telegramUrl, j.telegramLabel); + + const footerD = aDonF && !aDonF.classList.contains("hidden"); + const footerT = aTgF && !aTgF.classList.contains("hidden"); + if (gap1) gap1.classList.toggle("hidden", !(footerD || footerT)); + if (sepMid) sepMid.classList.toggle("hidden", !(footerD && footerT)); + + if (strip && (okD || okT)) strip.classList.remove("hidden"); + } catch { + /* офлайн или сбой — блок остаётся скрытым */ + } +} + function ensureEditionBannerInstallLogPanel() { if (!editionBanner) return null; let wrap = editionBanner.querySelector(".edition-banner-install-log"); @@ -1603,4 +1659,5 @@ async function boot() { } } +void hydratePanelPromoFooter(); boot(); diff --git a/public/index.html b/public/index.html index 292d14a..b3c6557 100644 --- a/public/index.html +++ b/public/index.html @@ -7,9 +7,20 @@ - + + + - + diff --git a/public/styles.css b/public/styles.css index 8f506ce..12889de 100644 --- a/public/styles.css +++ b/public/styles.css @@ -43,6 +43,64 @@ body.page { flex-direction: column; } +.panel-promo-strip { + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 40; + border-bottom: 1px solid rgba(180, 83, 9, 0.28); + background: linear-gradient(90deg, rgba(254, 243, 199, 0.95), rgba(255, 237, 213, 0.97)); + backdrop-filter: blur(6px); +} + +.panel-promo-strip-inner { + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 1.25rem; + max-width: min(1120px, 100%); + margin: 0 auto; + padding: 0.5rem clamp(1rem, 3vw, 1.5rem); +} + +.panel-promo-subtitle { + margin: 0; + flex: 1 1 16rem; + font-size: 0.84rem; + line-height: 1.5; + color: var(--text); +} + +.panel-promo-links { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem 0.65rem; +} + +.panel-promo-a { + font-weight: 650; + font-size: 0.88rem; + color: #9a3412; + text-decoration: none; + white-space: nowrap; +} + +.panel-promo-a:hover { + text-decoration: underline; +} + +.panel-promo-sep { + color: var(--muted); + font-weight: 700; + user-select: none; +} + +.support-line .support-promo-link { + white-space: nowrap; +} + .shell { flex: 1 0 auto; max-width: min(1120px, 100%); diff --git a/scripts/install.sh b/scripts/install.sh index 4e5edc7..9b89ef1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -265,7 +265,9 @@ elif [[ -f "${INSTALL_DIR}/.amnezia-panel-edition" ]]; then echo "→ AMNEZIA_EDITION из ${INSTALL_DIR}/.amnezia-panel-edition: ${__PE}" fi fi -for __ce_var in COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH ALLOW_COMMUNITY_GITHUB_ACTIVATION COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL COMMUNITY_INSTALL_FETCH_MS PRIVATE_INSTALL_SCRIPT_MAX_BYTES COMMUNITY_DISABLE_DOCKER_CLI_HELPER COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO COMMUNITY_PRO_INSTALL_HELPER_IMAGE FREE_PANEL_CONTAINER_FOR_PRO_INSTALL FREE_LANDING_CONTAINER_FOR_PRO_INSTALL STALE_PRO_PANEL_CONTAINER_FOR_PRO_INSTALL COMMUNITY_INSTALL_LOG_TAIL_BYTES COMMUNITY_INSTALL_LOG_API_CHUNK_MAX COMMUNITY_HELPER_SKIP_PREPARE_TOOLS; do +for __ce_var in COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH PANEL_FOOTER_DONATE_URL PANEL_FOOTER_TELEGRAM_URL \ + PANEL_FOOTER_DONATE_LABEL PANEL_FOOTER_TELEGRAM_LABEL PANEL_FOOTER_PROMO_SUBTITLE \ + ALLOW_COMMUNITY_GITHUB_ACTIVATION COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL COMMUNITY_INSTALL_FETCH_MS PRIVATE_INSTALL_SCRIPT_MAX_BYTES COMMUNITY_DISABLE_DOCKER_CLI_HELPER COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO COMMUNITY_PRO_INSTALL_HELPER_IMAGE FREE_PANEL_CONTAINER_FOR_PRO_INSTALL FREE_LANDING_CONTAINER_FOR_PRO_INSTALL STALE_PRO_PANEL_CONTAINER_FOR_PRO_INSTALL COMMUNITY_INSTALL_LOG_TAIL_BYTES COMMUNITY_INSTALL_LOG_API_CHUNK_MAX COMMUNITY_HELPER_SKIP_PREPARE_TOOLS; do if [[ -n "${!__ce_var:-}" ]]; then RUN_ENV+=( -e "${__ce_var}=${!__ce_var}" ) fi diff --git a/server.js b/server.js index 070ce71..1fc6be8 100644 --- a/server.js +++ b/server.js @@ -69,6 +69,45 @@ const COMMUNITY_UPGRADE_PITCH = process.env.COMMUNITY_UPGRADE_PITCH?.trim() || "В PRO: вкл/выкл клиентов, даты и расписание отключений, переименование, экспорт .conf, каскад, Cloudflare WARP, синхронизация времени хоста. В FREE: удаление клиента с сервера + отдельный раздел установки Telegram MTProto-прокси (Docker-контейнер). Полная сборка AWG-панели — приватный репозиторий amnezia_web-PRO; доступ по подписке Boosty."; +const PANEL_FOOTER_TELEGRAM_DEFAULT = "https://t.me/lot_andrey"; + +function sanitizeHttpsHttpUrl(raw) { + const s = String(raw || "").trim(); + if (!s) return ""; + try { + const u = new URL(s); + if (u.protocol === "http:" || u.protocol === "https:") return u.href; + } catch { + /* ignore */ + } + return ""; +} + +/** Донат + Telegram‑спонсор: только в админ‑панели (не пользовательские лендинги). Переопределяется через env контейнера. */ +function panelPromoFooterPayload() { + const donateResolved = + sanitizeHttpsHttpUrl( + process.env.PANEL_FOOTER_DONATE_URL?.trim() || COMMUNITY_UPGRADE_URL.trim(), + ) || ""; + const telegramResolved = + sanitizeHttpsHttpUrl( + process.env.PANEL_FOOTER_TELEGRAM_URL?.trim() || PANEL_FOOTER_TELEGRAM_DEFAULT, + ) || ""; + + return { + donateUrl: donateResolved, + telegramUrl: telegramResolved, + donateLabel: + process.env.PANEL_FOOTER_DONATE_LABEL?.trim() || "Поддержать проект", + telegramLabel: + process.env.PANEL_FOOTER_TELEGRAM_LABEL?.trim() || + "Telegram‑канал спонсора", + promoSubtitle: + process.env.PANEL_FOOTER_PROMO_SUBTITLE?.trim() || + "Поддержите разработку и канал спонсора — ссылки есть вверху и внизу панели. На пользовательские сайты‑лендинги это не добавляется.", + }; +} + function editionPayload() { return { tier: IS_COMMUNITY ? "community" : "pro", @@ -2115,6 +2154,11 @@ app.get("/health", (_req, res) => { res.json({ ok: true, version: PANEL_VERSION }); }); +/** Публичные ссылки доната / Telegram‑спонсора для интерфейса панели (без авторизации). */ +app.get("/api/panel/footer", (_req, res) => { + res.json(panelPromoFooterPayload()); +}); + app.get("/api/session", (req, res) => { if (!readSession(getSessionToken(req))) { res.status(401).json({ ok: false });