Show subscription inbounds as Remnawave-style host cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-29 04:56:45 +03:00
co-authored by Cursor
parent c0e78ce949
commit c41bae9852
8 changed files with 493 additions and 72 deletions
+99
View File
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · Subscription</title>
<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:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/app.css">
</head>
<body class="page-sub">
<div class="home-bg" aria-hidden="true"></div>
<main class="sub-wrap">
<header class="sub-header">
<p class="brand-mark sub-brand">VPN Panel</p>
<h1>{{.Info.Client.Username}}</h1>
<p class="muted">Подписка · инбаунды / хосты</p>
{{if .Expired}}
<div class="alert">Подписка неактивна или истекла</div>
{{else}}
<span class="badge {{statusClass (printf "%s" .Info.Client.Status)}}">{{.Info.Client.StatusLabel}}</span>
{{end}}
</header>
<section class="sub-meta">
<div class="stat">
<span class="stat-label">Трафик</span>
<span class="stat-value" style="font-size:1.2rem">
{{if gt .Info.Client.TrafficLimitBytes 0}}
использовано / лимит задан
{{else}}
∞ без лимита
{{end}}
</span>
</div>
<div class="stat">
<span class="stat-label">Истекает</span>
<span class="stat-value" style="font-size:1.2rem">
{{if .Info.Client.ExpireAt}}{{.Info.Client.ExpireAt.Format "02.01.2006 15:04"}}{{else}}бессрочно{{end}}
</span>
</div>
<div class="stat">
<span class="stat-label">Хостов</span>
<span class="stat-value accent" style="font-size:1.2rem">{{len .Info.Hosts}}</span>
</div>
</section>
<section class="panel-section">
<h2>Subscription URL</h2>
<pre class="code-block">{{.SubURL}}</pre>
<p class="hint">В клиенте добавьте эту ссылку как подписку. В браузере открывается эта страница с инбаундами.</p>
<div class="cta-row">
<a class="btn btn-ghost btn-sm" href="{{.SubURL}}?format=base64">Raw base64</a>
<a class="btn btn-ghost btn-sm" href="{{.SubURL}}?format=plain">Plain links</a>
<a class="btn btn-ghost btn-sm" href="{{.SubURL}}/info">JSON info</a>
</div>
</section>
<section class="panel-section">
<h2>Инбаунды / Hosts</h2>
{{if .Info.Hosts}}
<div class="proto-grid">
{{range .Info.Hosts}}
<article class="proto-card sub-host {{if .Available}}is-on{{else}}is-off{{end}}">
<div class="proto-top">
<h3>{{.Title}}</h3>
<span class="badge {{if .Available}}on{{else}}off{{end}}">{{if .Available}}ON{{else}}OFF{{end}}</span>
</div>
<p>
<code>{{.Tag}}</code> · {{.ProtocolName}}<br>
{{.Network}} / {{.Security}} · порт {{.Port}}
</p>
<div class="proto-meta">
{{if .Address}}
<span>{{.NodeName}}</span>
<code>{{.Address}}:{{.Port}}</code>
{{else}}
<span class="muted">Нет online-ноды с этим inbound</span>
{{end}}
</div>
{{if .Link}}
<details class="sub-link">
<summary>Показать ссылку</summary>
<pre class="code-block log">{{.Link}}</pre>
</details>
{{end}}
</article>
{{end}}
</div>
{{else}}
<div class="empty-state">
<p>Инбаунды не назначены. Админ должен выбрать inbound у пользователя.</p>
</div>
{{end}}
</section>
</main>
</body>
</html>