Files
vpn/app/templates/admin/xui_detail.html
T

305 lines
12 KiB
HTML

{% extends "admin/layout.html" %}
{% block admin_title %}{{ panel.name }}{% endblock %}
{% block admin_content %}
<div class="admin-top">
<div>
<h1>{{ panel.name }}</h1>
<div class="muted"><a href="{{ panel.base_url }}" target="_blank" rel="noopener">{{ panel.base_url }}</a></div>
</div>
<div class="actions">
<a class="btn btn-sm btn-ghost" href="/admin/xui">Назад</a>
<button class="btn btn-sm btn-ghost" type="button" id="btn-reload-inbounds">Загрузить inbound’ы</button>
<form method="post" action="/admin/xui/{{ panel.id }}/check" class="inline-form">
<button class="btn btn-sm btn-accent" type="submit">Обновить API</button>
</form>
</div>
</div>
{% if flash %}
{% if flash.startswith('error:') %}
<div class="flash flash-error">{{ flash[6:] }}</div>
{% elif flash == 'client_created' %}
<div class="flash">Клиент создан в 3x-ui</div>
{% elif flash == 'ok' %}
<div class="flash">Данные обновлены</div>
{% endif %}
{% endif %}
{% if error %}
<div class="flash flash-error">{{ error }}</div>
{% endif %}
{% if created %}
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head"><strong>Новый клиент ({{ created.protocol }})</strong></div>
<div class="panel-body stack">
<p style="margin:0"><strong>{{ created.email }}</strong> → inbound #{{ created.inbound_id }}</p>
{% if created.protocol == 'vless' %}
{% if created.uuid %}<p class="muted" style="margin:0">UUID: <code>{{ created.uuid }}</code>{% if created.flow %} · flow: {{ created.flow }}{% endif %}</p>{% endif %}
{% if created.expiry_days %}
<p class="muted" style="margin:0">
Срок: {{ created.expiry_days }} дн.
{% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %}
</p>
{% endif %}
{% if created.links %}
{% for link in created.links %}
<pre class="mono">{{ link }}</pre>
{% endfor %}
{% else %}
<p class="muted">Ссылка vless:// появится в 3x-ui (Clients → Copy URL).</p>
{% endif %}
{% elif created.protocol == 'wireguard' %}
{% if created.expiry_days %}
<p class="muted" style="margin:0">
Срок: {{ created.expiry_days }} дн.
{% if created.start_after_first_use %}· старт после первого использования{% else %}· с момента создания{% endif %}
</p>
{% endif %}
<p class="muted" style="margin:0">Ключи клиента (конфиг соберите в 3x-ui или ниже):</p>
<pre class="mono">PrivateKey = {{ created.privateKey }}
PublicKey = {{ created.publicKey }}
{% if created.inbound %}Endpoint port = {{ created.inbound.port }}{% endif %}</pre>
{% endif %}
</div>
</div>
{% endif %}
{% if data %}
<div class="stats">
<div class="stat">
<div class="label">Все inbound’ы</div>
<div class="value">{{ data.inbounds|length }}</div>
</div>
<div class="stat">
<div class="label">VLESS</div>
<div class="value">{{ data.vless_inbounds|length }}</div>
</div>
<div class="stat">
<div class="label">WireGuard</div>
<div class="value">{{ data.wireguard_inbounds|length }}</div>
</div>
</div>
<div class="detail-grid" style="margin-bottom:1rem">
<div class="panel">
<div class="panel-head"><strong>Создать VLESS клиента</strong></div>
<div class="panel-body">
<form method="post" action="/admin/xui/{{ panel.id }}/clients" class="stack">
<input type="hidden" name="protocol" value="vless" />
<label>Email / имя
<input name="email" placeholder="user-vless" required />
</label>
<label>Inbound VLESS
<select name="inbound_id" id="select-vless" required>
{% for ib in data.vless_inbounds %}
<option value="{{ ib.id }}">#{{ ib.id }} {{ ib.remark or 'vless' }} :{{ ib.port }}{% if ib.enable is defined and not ib.enable %} (off){% endif %}</option>
{% else %}
<option value="" disabled selected>Нет VLESS inbound’ов — нажмите «Загрузить»</option>
{% endfor %}
</select>
</label>
<label>Flow (пусто = авто vision для Reality/TLS)
<input name="flow" placeholder="xtls-rprx-vision" />
</label>
<div class="form-grid-2">
<label>Срок действия (дни, 0 = безлимит)
<input type="number" name="expiry_days" value="30" min="0" />
</label>
<label>Лимит GB
<input type="number" name="total_gb" value="0" min="0" />
</label>
<label>Limit IP
<input type="number" name="limit_ip" value="0" min="0" />
</label>
</div>
<label class="check">
<input type="checkbox" name="start_after_first_use" value="1" checked />
Старт после первого использования
</label>
<label>Комментарий
<input name="comment" />
</label>
<button class="btn btn-accent" type="submit" {% if not data.vless_inbounds %}disabled{% endif %}>Создать VLESS</button>
</form>
</div>
</div>
<div class="panel">
<div class="panel-head"><strong>Создать WireGuard клиента</strong></div>
<div class="panel-body">
<form method="post" action="/admin/xui/{{ panel.id }}/clients" class="stack">
<input type="hidden" name="protocol" value="wireguard" />
<label>Email / имя
<input name="email" placeholder="user-wg" required />
</label>
<label>Inbound WireGuard
<select name="inbound_id" id="select-wg" required>
{% for ib in data.wireguard_inbounds %}
<option value="{{ ib.id }}">#{{ ib.id }} {{ ib.remark or 'wireguard' }} :{{ ib.port }}{% if ib.enable is defined and not ib.enable %} (off){% endif %}</option>
{% else %}
<option value="" disabled selected>Нет WireGuard inbound’ов — нажмите «Загрузить»</option>
{% endfor %}
</select>
</label>
<div class="form-grid-2">
<label>Срок действия (дни, 0 = безлимит)
<input type="number" name="expiry_days" value="30" min="0" />
</label>
<label>Лимит GB
<input type="number" name="total_gb" value="0" min="0" />
</label>
<label>Limit IP
<input type="number" name="limit_ip" value="0" min="0" />
</label>
</div>
<label class="check">
<input type="checkbox" name="start_after_first_use" value="1" checked />
Старт после первого использования
</label>
<label>Комментарий
<input name="comment" />
</label>
<button class="btn btn-accent" type="submit" {% if not data.wireguard_inbounds %}disabled{% endif %}>Создать WireGuard</button>
</form>
</div>
</div>
</div>
<div class="panel" style="margin-bottom:1rem">
<div class="panel-head">
<strong>Доступные inbound’ы (VLESS / WireGuard)</strong>
<span class="muted" id="inbounds-status"></span>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Remark</th>
<th>Protocol</th>
<th>Port</th>
<th>Enable</th>
</tr>
</thead>
<tbody id="inbounds-table-body">
{% for ib in data.available_inbounds %}
<tr>
<td>{{ ib.id }}</td>
<td>{{ ib.remark or '—' }}</td>
<td><span class="badge badge-proto">{{ ib.protocol }}</span></td>
<td>{{ ib.port }}</td>
<td>{% if ib.enable is not defined or ib.enable %}<span class="badge badge-ok">on</span>{% else %}<span class="badge badge-off">off</span>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">Нет VLESS/WireGuard inbound’ов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel">
<div class="panel-head"><strong>Clients</strong></div>
<table>
<thead>
<tr>
<th>Email</th>
<th>Enable</th>
<th>Inbounds</th>
<th>Traffic</th>
</tr>
</thead>
<tbody>
{% for c in data.clients %}
<tr>
<td>{{ c.email }}</td>
<td>{% if c.enable %}<span class="badge badge-ok">on</span>{% else %}<span class="badge badge-off">off</span>{% endif %}</td>
<td>{{ c.inboundIds or c.inbound_ids or '—' }}</td>
<td class="muted">
{% if c.traffic %}
↑ {{ c.traffic.up or 0 }} / ↓ {{ c.traffic.down or 0 }}
{% else %}—{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="4" class="muted">Нет клиентов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function () {
const panelId = {{ panel.id }};
const btn = document.getElementById('btn-reload-inbounds');
const statusEl = document.getElementById('inbounds-status');
const tbody = document.getElementById('inbounds-table-body');
const selectVless = document.getElementById('select-vless');
const selectWg = document.getElementById('select-wg');
function fillSelect(select, items, emptyLabel) {
if (!select) return;
select.innerHTML = '';
if (!items.length) {
const opt = document.createElement('option');
opt.disabled = true;
opt.selected = true;
opt.value = '';
opt.textContent = emptyLabel;
select.appendChild(opt);
return;
}
items.forEach((ib) => {
const opt = document.createElement('option');
opt.value = ib.id;
const off = (ib.enable === false) ? ' (off)' : '';
opt.textContent = `#${ib.id} ${ib.remark || ib.protocol} :${ib.port}${off}`;
select.appendChild(opt);
});
}
async function reloadInbounds() {
statusEl.textContent = 'загрузка…';
btn.disabled = true;
try {
const res = await fetch(`/admin/xui/${panelId}/inbounds`);
const data = await res.json();
if (!data.success) throw new Error(data.error || 'fail');
const items = data.obj || [];
const vless = items.filter((i) => String(i.protocol).toLowerCase() === 'vless');
const wg = items.filter((i) => String(i.protocol).toLowerCase() === 'wireguard');
fillSelect(selectVless, vless, 'Нет VLESS inbound’ов');
fillSelect(selectWg, wg, 'Нет WireGuard inbound’ов');
const vlessBtn = selectVless && selectVless.closest('form').querySelector('button[type=submit]');
const wgBtn = selectWg && selectWg.closest('form').querySelector('button[type=submit]');
if (vlessBtn) vlessBtn.disabled = vless.length === 0;
if (wgBtn) wgBtn.disabled = wg.length === 0;
tbody.innerHTML = '';
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">Нет VLESS/WireGuard inbound’ов</td></tr>';
} else {
items.forEach((ib) => {
const tr = document.createElement('tr');
const on = ib.enable === false
? '<span class="badge badge-off">off</span>'
: '<span class="badge badge-ok">on</span>';
tr.innerHTML = `<td>${ib.id}</td><td>${ib.remark || '—'}</td><td><span class="badge badge-proto">${ib.protocol}</span></td><td>${ib.port}</td><td>${on}</td>`;
tbody.appendChild(tr);
});
}
statusEl.textContent = `загружено: ${items.length} (VLESS ${vless.length}, WG ${wg.length})`;
} catch (e) {
statusEl.textContent = 'ошибка: ' + e.message;
} finally {
btn.disabled = false;
}
}
btn.addEventListener('click', reloadInbounds);
})();
</script>
{% endif %}
{% endblock %}