mbs-panel/landing/index.html

336 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MBS Panel</title>
<meta name="description" content="Самостоятельная VPN-панель на Xray-core: VLESS+Reality, Hysteria2, Telegram-бот, платежи и админка — без Docker и без чужого закрытого кода внутри.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2222%22 fill=%22%230a0b0f%22/><path d=%22M20 65 L50 25 L80 65%22 stroke=%22%237c6cf0%22 stroke-width=%228%22 fill=%22none%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22/></svg>">
<style>
:root {
--bg: #0a0b0f; --card: #131519; --border: #1e2128;
--text: #eceef2; --muted: #868c99; --accent: #7c6cf0;
--good: #5fd48a; --bad: #e8748a;
--mono: "SF Mono", "Cascadia Code", Consolas, monospace;
--ease: cubic-bezier(0.16, 1, 0.3, 1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0; background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; }
.wrap { max-width: 920px; margin: 0 auto; padding: 0 24px; }
header { display: flex; align-items: center; justify-content: space-between; padding: 26px 0; }
.logo { display: flex; align-items: center; gap: 9px; font-weight: 600; font-size: 15px; letter-spacing: -0.01em; }
.logo svg { width: 20px; height: 20px; }
nav { display: flex; align-items: center; gap: 26px; }
nav a { position: relative; text-decoration: none; color: var(--muted); font-size: 14px; transition: color .25s var(--ease); }
nav a::after {
content: ""; position: absolute; left: 0; bottom: -4px; width: 100%; height: 1px;
background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .3s var(--ease);
}
nav a:hover { color: var(--text); }
nav a:hover::after { transform: scaleX(1); }
.btn {
display: inline-flex; align-items: center; gap: 8px; padding: 13px 22px; border-radius: 10px;
background: var(--text); color: var(--bg); text-decoration: none; font-weight: 600; font-size: 14px;
border: none; cursor: pointer; transition: transform .15s var(--ease), opacity .15s var(--ease);
}
.btn:hover { opacity: .85; }
.btn:active { transform: scale(.97); }
.btn.ghost {
background: transparent; color: var(--text); border: 1px solid var(--border);
transition: transform .15s var(--ease), border-color .25s var(--ease), background .25s var(--ease);
}
.btn.ghost:hover { border-color: #333947; background: var(--card); opacity: 1; }
.btn.sm { padding: 9px 14px; font-size: 13px; }
.reveal { opacity: 0; transform: translateY(14px); filter: blur(6px); transition: opacity .7s var(--ease), transform .7s var(--ease), filter .7s var(--ease); }
.reveal.in { opacity: 1; transform: translateY(0); filter: blur(0); }
.hero { padding: 80px 0 56px; }
.badges { display: flex; gap: 8px; margin-bottom: 22px; flex-wrap: wrap; }
.badges img { height: 20px; display: block; }
.hero h1 { font-size: 44px; line-height: 1.14; margin: 0 0 18px; letter-spacing: -.03em; font-weight: 600; max-width: 680px; }
.hero .accent { color: var(--accent); }
.hero p { color: var(--muted); font-size: 17px; max-width: 560px; margin: 0 0 30px; line-height: 1.6; }
.hero-ctas { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 34px; }
.install {
background: var(--card); border: 1px solid var(--border); border-radius: 12px;
padding: 16px 18px; max-width: 620px; display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.install code { font-family: var(--mono); font-size: 13.5px; color: var(--text); overflow-x: auto; white-space: nowrap; }
.install code .dim { color: var(--muted); }
.copy-btn {
flex-shrink: 0; background: transparent; border: 1px solid var(--border); color: var(--muted);
border-radius: 7px; padding: 7px 11px; font-size: 12px; cursor: pointer; font-family: inherit;
transition: all .2s var(--ease);
}
.copy-btn:hover { color: var(--text); border-color: #333947; }
.copy-btn.copied { color: var(--good); border-color: var(--good); }
.divider { height: 1px; background: var(--border); margin: 0; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); margin: 0; }
.feature { background: var(--bg); padding: 32px 28px; }
.feature .idx { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; margin-bottom: 14px; }
.feature h3 { font-size: 16px; margin: 0 0 8px; font-weight: 600; }
.feature p { color: var(--muted); font-size: 14px; margin: 0; line-height: 1.55; }
section { padding: 72px 0; }
.section-label { font-size: 13px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 12px; }
h2 { font-size: 26px; margin: 0 0 12px; font-weight: 600; letter-spacing: -.01em; }
.section-sub { color: var(--muted); font-size: 15px; max-width: 560px; line-height: 1.6; margin: 0 0 36px; }
.cmp-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 12px; }
table.cmp { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 560px; }
table.cmp th, table.cmp td { padding: 13px 18px; text-align: left; border-bottom: 1px solid var(--border); }
table.cmp th { color: var(--muted); font-weight: 500; font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; background: var(--card); }
table.cmp th:not(:first-child), table.cmp td:not(:first-child) { text-align: center; }
table.cmp tr:last-child td { border-bottom: none; }
table.cmp td:first-child { color: var(--text); }
table.cmp .us { background: rgba(124,108,240,.06); font-weight: 600; }
.yes { color: var(--good); }
.no { color: var(--muted); }
.soon { color: var(--accent); font-size: 12.5px; }
.cmp-note { color: var(--muted); font-size: 13px; margin-top: 14px; line-height: 1.5; }
.arch-diagram {
background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 28px;
font-family: var(--mono); font-size: 13px; color: var(--muted); line-height: 2; overflow-x: auto; white-space: pre;
}
.arch-diagram .n { color: var(--text); } .arch-diagram .a { color: var(--accent); }
.how-steps { display: flex; flex-direction: column; gap: 18px; max-width: 640px; }
.how-step { display: flex; gap: 16px; color: var(--muted); font-size: 14.5px; line-height: 1.6; }
.how-step span {
flex-shrink: 0; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center; font-size: 12px; color: var(--accent);
font-variant-numeric: tabular-nums;
}
.how-step b { color: var(--text); font-weight: 600; }
code.inline { font-family: var(--mono); background: var(--card); border: 1px solid var(--border); border-radius: 5px; padding: 2px 6px; font-size: 13px; color: var(--text); }
.stack { display: flex; flex-wrap: wrap; gap: 10px; }
.stack span {
border: 1px solid var(--border); border-radius: 8px; padding: 7px 13px; font-size: 13px; color: var(--muted);
font-family: var(--mono);
}
.cta { text-align: center; padding: 20px 0 90px; }
.cta h2 { margin-bottom: 8px; }
.cta .section-sub { margin: 0 auto 28px; text-align: center; }
.cta-row { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; }
footer { border-top: 1px solid var(--border); padding: 28px 0; color: var(--muted); font-size: 13px; }
footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
footer a { text-decoration: underline; text-underline-offset: 2px; }
@media (max-width: 640px) {
.hero h1 { font-size: 32px; }
.grid { grid-template-columns: 1fr; }
.install { flex-direction: column; align-items: stretch; }
.install code { white-space: normal; word-break: break-all; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
.reveal { opacity: 1 !important; transform: none !important; filter: none !important; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="logo">
<svg viewBox="0 0 100 100" fill="none"><path d="M20 65 L50 25 L80 65" stroke="#7c6cf0" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/></svg>
MBS Panel
</div>
<nav>
<a href="#features">Возможности</a>
<a href="#comparison">Сравнение</a>
<a href="#install">Установка</a>
<a href="https://github.com/savsisbtw/mbs-panel" target="_blank">GitHub</a>
</nav>
</header>
<section class="hero" style="padding-bottom:0">
<div class="badges reveal">
<img src="https://img.shields.io/github/actions/workflow/status/savsisbtw/mbs-panel/ci.yml?label=CI&style=flat-square&color=7c6cf0" alt="CI">
<img src="https://img.shields.io/github/license/savsisbtw/mbs-panel?style=flat-square&color=7c6cf0" alt="License">
<img src="https://img.shields.io/github/stars/savsisbtw/mbs-panel?style=flat-square&color=7c6cf0" alt="Stars">
</div>
<h1 class="reveal">VPN-панель, которую<br>можно <span class="accent">понять за вечер</span></h1>
<p class="reveal">Xray-core, Reality, Hysteria2, Telegram-бот и платежи — в одном небольшом репозитории на Python. Без Docker, без чужой закрытой панели под капотом, без разбора чужого фреймворка перед первым коммитом.</p>
<div class="hero-ctas reveal">
<a class="btn" href="https://github.com/savsisbtw/mbs-panel" target="_blank">Смотреть на GitHub</a>
<a class="btn ghost" href="#install">Установка</a>
</div>
<div class="install reveal">
<code><span class="dim">$</span> bash &lt;(curl -Ls https://mbs.savsis.xyz/install.sh)</code>
<button class="copy-btn" onclick="copyInstall(this)">Копировать</button>
</div>
</section>
</div>
<div class="divider" style="margin-top:56px"></div>
<div class="wrap">
<div class="grid" id="features">
<div class="feature reveal">
<div class="idx">01</div>
<h3>Телеграм-бот как основа</h3>
<p>Не уведомления сбоку, а весь флоу покупки и управления — тарифы, гифт-коды, авто-отключение по истечении раз в 90 секунд, а не раз в полчаса.</p>
</div>
<div class="feature reveal">
<div class="idx">02</div>
<h3>Админка без фреймворков</h3>
<p>Чистый HTML/CSS/JS. Дашборд, карточка юзера с историей и устройствами, ноды с полным редактированием, трафик по Stats API самого Xray.</p>
</div>
<div class="feature reveal">
<div class="idx">03</div>
<h3>Мультинодовость в одну команду</h3>
<p>Добавил ноду в панели — получил одну bash-команду. Вставил на чистый сервер — сам ставит Xray, генерит ключи, регистрируется.</p>
</div>
<div class="feature reveal">
<div class="idx">04</div>
<h3>Протоколы на выбор</h3>
<p>VLESS Reality (TCP / gRPC / XHTTP), VLESS WS+TLS с реальным Let's Encrypt сертификатом, Hysteria2 отдельным процессом.</p>
</div>
<div class="feature reveal">
<div class="idx">05</div>
<h3>HWID-лимит устройств</h3>
<p>Ограничение числа устройств на подписку через заголовок клиента — опционально, как у Remnawave, но необязательно.</p>
</div>
<div class="feature reveal">
<div class="idx">06</div>
<h3>Платежи из коробки</h3>
<p>ЮKassa и Platega — опционально. Без них бот просто выдаёт подписку по кнопке, для своих или для теста.</p>
</div>
</div>
</div>
<div class="wrap">
<section id="comparison">
<div class="section-label reveal">Сравнение</div>
<h2 class="reveal">Чем MBS Panel отличается</h2>
<p class="section-sub reveal">Честно, без "мы лучше всех" — Remnawave и Marzban старше, крупнее и во многом функциональнее. Вот где реальная разница, а не маркетинг.</p>
<div class="cmp-wrap reveal">
<table class="cmp">
<thead><tr><th>Функция</th><th class="us">MBS Panel</th><th>Remnawave</th><th>Marzban</th></tr></thead>
<tbody>
<tr><td>Telegram-бот — основной UX покупки</td><td class="us yes">✓</td><td class="no">только уведомления</td><td class="no">уведомления + команды</td></tr>
<tr><td>HWID-лимит устройств</td><td class="us yes">✓</td><td class="yes">✓</td><td class="no">—</td></tr>
<tr><td>База данных</td><td class="us">SQLite</td><td>PostgreSQL</td><td>на выбор</td></tr>
<tr><td>Установка</td><td class="us">1 bash-команда</td><td>Docker Compose</td><td>bash-скрипт / Docker</td></tr>
<tr><td>Backup & Restore в самой панели</td><td class="us yes">✓</td><td class="no">community tools</td><td class="no">community tools</td></tr>
<tr><td>Xray config pre-flight проверка</td><td class="us yes">✓</td><td class="yes">full-featured</td><td class="no">только JSON-синтаксис</td></tr>
<tr><td>Мультиадминство (раздельные логины)</td><td class="us yes">✓</td><td class="no">—</td><td class="no">в разработке</td></tr>
<tr><td>2FA на вход в админку</td><td class="us yes">✓ TOTP</td><td>есть (passkeys/OAuth)</td><td class="no">—</td></tr>
<tr><td>Rate-limit на вход/2FA-код</td><td class="us yes">✓</td><td class="no">не документировано</td><td class="no">не документировано</td></tr>
<tr><td>Свой путь входа в админку</td><td class="us yes">✓</td><td class="yes">заявлено</td><td class="no">—</td></tr>
<tr><td>Цепочки серверов (клиент → A → B → интернет)</td><td class="us yes">✓ мышкой, с замером задержки</td><td>руками в конфиге Xray</td><td>руками в конфиге Xray</td></tr>
<tr><td>Сортировка нод мышкой</td><td class="us yes">✓</td><td class="yes">Web UI</td><td class="no">только через конфиг Xray</td></tr>
<tr><td>Пауза подписки без потери оплаченных дней</td><td class="us yes">✓</td><td class="no">—</td><td class="yes">есть</td></tr>
<tr><td>Исходящие вебхуки (пользователи + ноды)</td><td class="us yes">✓</td><td class="yes">✓</td><td class="no">только пользователи</td></tr>
<tr><td>Лицензия</td><td class="us">MIT</td><td>AGPL-3.0</td><td>AGPL-3.0</td></tr>
</tbody>
</table>
</div>
<p class="cmp-note reveal">Данные по Remnawave/Marzban — из их собственной документации на момент публикации (<a href="https://docs.rw/overview/comparison-of-functions" target="_blank" style="text-decoration:underline">docs.rw</a>), проверяй актуальность сам, проекты активно развиваются.</p>
</section>
</div>
<div class="divider"></div>
<div class="wrap">
<section>
<div class="section-label reveal">Как это работает</div>
<h2 class="reveal">Что происходит под капотом</h2>
<p class="section-sub reveal">Панель и первая VPN-нода живут на одном сервере. Дополнительные ноды подключаются по SSH — без агента, без открытых портов управления наружу.</p>
<div class="arch-diagram reveal">Telegram / браузер
│
▼
<span class="n">bot.py</span> (aiogram) ──┐
<span class="n">api.py</span> (FastAPI) ──┼──▶ <span class="n">SQLite</span>
│ │
▼ ▼
локальный <span class="a">Xray</span> SSH ──▶ <span class="a">Xray</span> на удалённой ноде
(management-ключ, генерится сам)</div>
<div class="how-steps reveal">
<div class="how-step"><span>1</span>Устанавливаешь на чистый сервер — скрипт сам ставит Xray, nginx, certbot, генерит Reality-ключи, поднимает бота и API systemd-юнитами.</div>
<div class="how-step"><span>2</span>Бот — точка входа для юзеров: тарифы, гифт-коды, оплата (если включена). Подписка выдаётся сразу и добавляется в клиент по одной ссылке.</div>
<div class="how-step"><span>3</span>Админка — точка входа для тебя: ноды, юзеры, устройства, трафик по Stats API самого Xray. Всё через браузер, ничего руками на сервере.</div>
<div class="how-step"><span>4</span>Новую ноду добавляешь в панели — получаешь одну bash-команду. Вставляешь на чистый сервер, она сама ставит Xray и регистрируется — SSH-ключ панель добавляет туда сама, пароль не спрашивает ни разу.</div>
</div>
<div class="stack reveal" style="margin-top:28px">
<span>Python 3.10+</span><span>FastAPI</span><span>aiogram 3</span><span>SQLite (WAL)</span><span>paramiko</span><span>Xray-core</span>
</div>
</section>
</div>
<div class="divider"></div>
<div class="wrap">
<section id="install">
<div class="section-label reveal">Как скачать и установить</div>
<h2 class="reveal">Чистый сервер → готовая панель за одну команду</h2>
<p class="section-sub reveal">Код открыт, MIT — клонируешь и ставишь на свой сервер, ничего не покупаешь и никуда не регистрируешься.</p>
<div class="how-steps reveal">
<div class="how-step"><span>1</span>Подними чистый сервер — <b>Ubuntu 22.04/24.04</b> или <b>Debian 11/12</b>, root-доступ — и три DNS A-записи на его IP: под панель, под подписку и под первую VPN-ноду.</div>
<div class="how-step"><span>2</span>Запусти установочный скрипт от root — одна команда ниже, сама подтянет исходники.</div>
<div class="how-step"><span>3</span>Скрипт спросит домены и пароль админки, сам поставит Xray/nginx/certbot, сгенерит ключи и выпустит сертификаты.</div>
<div class="how-step"><span>4</span>Готово: бот отвечает в Telegram, сайт с подпиской и админка — на своих доменах. Обновления потом — командой <code class="inline">mbs update</code>.</div>
</div>
<div class="install reveal" style="max-width:none;margin-top:8px">
<code><span class="dim">$</span> bash &lt;(curl -Ls https://mbs.savsis.xyz/install.sh)</code>
<button class="copy-btn" onclick="copyInstall(this)">Копировать</button>
</div>
</section>
<div class="cta reveal">
<h2>Открытый исходник, MIT</h2>
<p class="section-sub">Изначально писалось под конкретный проект — получилось достаточно универсально, чтобы выложить как есть. Issues и PR приветствуются.</p>
<div class="cta-row">
<a class="btn" href="https://github.com/savsisbtw/mbs-panel" target="_blank">github.com/savsisbtw/mbs-panel</a>
<a class="btn ghost" href="https://github.com/savsisbtw/mbs-panel#readme" target="_blank">Читать README</a>
</div>
</div>
</div>
<footer>
<div class="wrap">
<div>MBS Panel — made by <a href="https://github.com/savsisbtw" target="_blank">savsis</a></div>
<div><a href="https://github.com/savsisbtw/mbs-panel/blob/main/LICENSE" target="_blank">MIT License</a></div>
</div>
</footer>
<script>
const io = new IntersectionObserver((entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting) {
e.target.style.transitionDelay = (i % 4) * 0.07 + "s";
e.target.classList.add("in");
io.unobserve(e.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll(".reveal").forEach((el) => io.observe(el));
function copyInstall(btn) {
const text = "bash <(curl -Ls https://mbs.savsis.xyz/install.sh)";
navigator.clipboard.writeText(text).then(() => {
const orig = btn.textContent;
btn.textContent = "Скопировано";
btn.classList.add("copied");
setTimeout(() => { btn.textContent = orig; btn.classList.remove("copied"); }, 1600);
});
}
</script>
</body>
</html>