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 <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-15 03:57:36 +03:00
parent 8d6bf1a45c
commit bb007ad644
7 changed files with 186 additions and 5 deletions

View File

@@ -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();

View File

@@ -7,9 +7,20 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=1.2.19">
<link rel="stylesheet" href="/styles.css?v=1.2.20">
</head>
<body class="page">
<aside id="panel-promo-strip" class="panel-promo-strip hidden" role="complementary" aria-label="Поддержка проекта">
<div class="panel-promo-strip-inner">
<p id="panel-promo-subtitle" class="panel-promo-subtitle muted"></p>
<div class="panel-promo-links">
<a id="panel-strip-donate" class="panel-promo-a hidden" target="_blank" rel="noopener noreferrer"></a>
<span id="panel-strip-sep" class="panel-promo-sep hidden" aria-hidden="true">·</span>
<a id="panel-strip-telegram" class="panel-promo-a hidden" target="_blank" rel="noopener noreferrer"></a>
</div>
</div>
</aside>
<div id="login-gate" class="gate hidden" aria-hidden="true">
<div class="gate-card">
<p class="eyebrow">Панель сервера</p>
@@ -222,9 +233,13 @@
<strong class="support-title">amnezia_web</strong>
<span class="support-sep" aria-hidden="true">·</span>
<a href="https://github.com/andrey271192/amnezia_web" target="_blank" rel="noopener noreferrer">GitHub</a>
<span id="footer-promo-gap1" class="support-sep support-footer-promo-gap hidden" aria-hidden="true">·</span>
<a id="footer-promo-donate" class="support-promo-link hidden" target="_blank" rel="noopener noreferrer"></a>
<span id="footer-promo-sep-mid" class="support-sep hidden" aria-hidden="true">·</span>
<a id="footer-promo-telegram" class="support-promo-link hidden" target="_blank" rel="noopener noreferrer"></a>
</div>
<p class="support-blurb">
Базовая панель: AWGклиенты, статусы и MTProtoпрокси (Docker) без сторонней интеграции Telegramботов.
Базовая панель: AWGклиенты, статусы и MTProtoпрокси (Docker). Ссылки поддержки автора только в этом интерфейсе панели, не на отдельных пользовательских лендингах.
</p>
</footer>
@@ -262,6 +277,6 @@
</div>
</dialog>
<script src="/app.js?v=1.2.19" type="module"></script>
<script src="/app.js?v=1.2.20" type="module"></script>
</body>
</html>

View File

@@ -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%);