Files
panel-vpn/web/templates/node_view.html
T

224 lines
10 KiB
HTML
Raw 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>{{.Title}} · VPN Panel</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">
{{if eq (printf "%s" .Node.Status) "installing"}}
<meta http-equiv="refresh" content="4">
{{end}}
</head>
<body class="page-admin">
<aside class="sidebar">
<a class="brand" href="/admin">VPN Panel</a>
<nav>
<a class="{{if eq .Active "dashboard"}}active{{end}}" href="/admin">Обзор</a>
<a class="{{if eq .Active "nodes"}}active{{end}}" href="/admin/nodes">Ноды</a>
<a class="{{if eq .Active "profiles"}}active{{end}}" href="/admin/profiles">Профили</a>
<a class="{{if eq .Active "protocols"}}active{{end}}" href="/admin/protocols">Протоколы</a>
</nav>
<div class="sidebar-foot">
<span class="user-chip">{{.UserName}}</span>
<form method="POST" action="/logout"><button type="submit" class="link-btn">Выйти</button></form>
</div>
</aside>
<main class="admin-main">
<header class="admin-header row-head">
<div>
<h1>{{.Node.Name}}</h1>
<p class="muted"><code>{{.Node.Host}}</code> · API :{{.Node.NodePort}} · {{.Node.InstallMode}}</p>
</div>
<span class="badge {{statusClass (printf "%s" .Node.Status)}}">{{.Node.StatusLabel}}</span>
</header>
{{if .Flash}}<div class="alert ok">{{.Flash}}</div>{{end}}
{{if .Error}}<div class="alert">{{.Error}}</div>{{end}}
{{if .Node.LastError}}<div class="alert">{{.Node.LastError}}</div>{{end}}
<div class="cta-row" style="margin-bottom:1.25rem">
<form method="POST" action="/admin/nodes/{{.Node.ID}}/install">
<button class="btn btn-primary" type="submit">Установить / переустановить по SSH</button>
</form>
<form method="POST" action="/admin/nodes/{{.Node.ID}}/check">
<button class="btn btn-ghost" type="submit">Проверить online</button>
</form>
<form method="POST" action="/admin/nodes/{{.Node.ID}}/sync">
<button class="btn btn-ghost" type="submit">Синхронизировать протоколы</button>
</form>
<form method="POST" action="/admin/nodes/{{.Node.ID}}/logs">
<button class="btn btn-ghost" type="submit">Обновить логи</button>
</form>
<form method="POST" action="/admin/nodes/{{.Node.ID}}/delete" onsubmit="return confirm('Удалить ноду из панели?');">
<button class="btn btn-ghost" type="submit">Удалить</button>
</form>
<a class="btn btn-ghost" href="/admin/nodes">К списку</a>
</div>
<section class="panel-section">
<h2>Config Profile</h2>
<form class="form-card" method="POST" action="/admin/nodes/{{.Node.ID}}/profile">
<label>Профиль для этой ноды
<select name="profile_id">
<option value="none">— без профиля —</option>
{{range .Profiles}}
<option value="{{.ID}}" {{if eq $.Node.ProfileIDString (printf "%s" .ID)}}selected{{end}}>{{.Name}} ({{.InboundCount}} inbound)</option>
{{end}}
</select>
</label>
<p class="hint">При назначении все inbound профиля включаются на ноде. Потом можно выключить отдельные.</p>
<button class="btn btn-primary" type="submit">Назначить профиль</button>
</form>
{{if .Node.ProfileName}}
<p class="muted" style="margin-top:0.75rem">Текущий: <strong>{{.Node.ProfileName}}</strong> · <a href="/admin/profiles/{{.Node.ProfileIDString}}">открыть профиль</a></p>
{{end}}
</section>
{{if .NodeInbounds}}
<section class="panel-section">
<h2>Инбаунды на ноде</h2>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Tag</th>
<th>Протокол</th>
<th>Порт</th>
<th>Network / Security</th>
<th>На ноде</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .NodeInbounds}}
<tr>
<td><code>{{.Tag}}</code></td>
<td>{{.ProtocolName}}</td>
<td>{{.Port}}</td>
<td>{{.Network}} / {{.Security}}</td>
<td><span class="badge {{if .NodeEnabled}}on{{else}}off{{end}}">{{if .NodeEnabled}}ON{{else}}OFF{{end}}</span></td>
<td class="actions">
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/inbounds/{{.ID}}">
<input type="hidden" name="action" value="{{if .NodeEnabled}}disable{{else}}enable{{end}}">
<button class="btn btn-sm {{if .NodeEnabled}}btn-ghost{{else}}btn-primary{{end}}" type="submit">
{{if .NodeEnabled}}Выключить{{else}}Включить{{end}}
</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
{{end}}
<section class="panel-section">
<h2>Протоколы на этом сервере</h2>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Протокол</th>
<th>Порт</th>
<th>Установлен</th>
<th>Включён</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .NodeProtocols}}
<tr>
<td>
<strong>{{.ProtocolName}}</strong>
<div><code>{{.ProtocolCode}}</code></div>
</td>
<td>{{.Port}}</td>
<td><span class="badge {{if .Installed}}on{{else}}off{{end}}">{{if .Installed}}Да{{else}}Нет{{end}}</span></td>
<td><span class="badge {{if .Enabled}}on{{else}}off{{end}}">{{if .Enabled}}ON{{else}}OFF{{end}}</span></td>
<td class="actions actions-multi">
{{if not .Installed}}
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
<input type="hidden" name="action" value="install">
<button class="btn btn-sm btn-primary" type="submit">Установить</button>
</form>
{{else}}
{{if .Enabled}}
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
<input type="hidden" name="action" value="disable">
<button class="btn btn-sm btn-ghost" type="submit">Выключить</button>
</form>
{{else}}
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
<input type="hidden" name="action" value="enable">
<button class="btn btn-sm btn-primary" type="submit">Включить</button>
</form>
{{end}}
<form method="POST" action="/admin/nodes/{{$.Node.ID}}/protocols/{{.ProtocolID}}">
<input type="hidden" name="action" value="uninstall">
<button class="btn btn-sm btn-ghost" type="submit">Снять</button>
</form>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
<section class="panel-section">
<h2>Важно</h2>
<p class="muted">Откройте порт <strong>{{.Node.NodePort}}</strong> на ноде <em>только</em> для IP панели (как NODE_PORT в Remnawave).</p>
</section>
<section class="panel-section">
<h2>docker-compose.yml</h2>
<pre class="code-block">{{.Compose}}</pre>
</section>
<section class="panel-section">
<h2>Ручная установка ( Remnawave-style )</h2>
<ol class="steps">
<li>На сервере ноды: <code>sudo curl -fsSL https://get.docker.com | sh</code></li>
<li><code>mkdir -p /opt/vpn-panel-node && cd /opt/vpn-panel-node</code></li>
<li>Скопируйте бинарник <code>vpn-node</code> (linux amd64) из образа панели <code>/app/bin/vpn-node</code></li>
<li>Вставьте compose ниже / выше и выполните <code>docker compose up -d</code></li>
</ol>
<pre class="code-block">{{.ManualScript}}</pre>
</section>
{{if .Node.InstallLog}}
<section class="panel-section">
<h2>Лог установки</h2>
<pre class="code-block log">{{.Node.InstallLog}}</pre>
</section>
{{end}}
<section class="panel-section" id="logs">
<div class="section-head">
<h2>Логи сервера</h2>
<form method="POST" action="/admin/nodes/{{.Node.ID}}/logs">
<button class="btn btn-sm btn-ghost" type="submit">Обновить</button>
</form>
</div>
{{if .Node.RuntimeLog}}
<pre class="code-block log">{{.Node.RuntimeLog}}</pre>
{{else}}
<p class="muted">Логов ещё нет. Нажмите «Обновить логи» — панель заберёт agent API и docker logs по SSH.</p>
{{end}}
</section>
<section class="panel-section meta-grid">
<div><span class="stat-label">Агент</span><div>{{if .Node.AgentVersion}}{{.Node.AgentVersion}}{{else}}—{{end}}</div></div>
<div><span class="stat-label">Last seen</span><div>{{if .Node.LastSeenAt}}{{.Node.LastSeenAt}}{{else}}—{{end}}</div></div>
<div><span class="stat-label">Создана</span><div>{{.Node.CreatedAt}}</div></div>
</section>
</main>
</body>
</html>