Template
Load inbounds from 3x-ui and use panel share links only.
Restore inbound selection from the panel API and send a minimal addClient payload so locally invented fields do not break configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3905,12 +3905,14 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
|||||||
config = await xui_get_config(data.get('settings', {}), req.client_id, panel_id=panel_id)
|
config = await xui_get_config(data.get('settings', {}), req.client_id, panel_id=panel_id)
|
||||||
result = {'client_id': req.client_id, 'config': config, 'subscription_url': config if str(config).startswith('http') else ''}
|
result = {'client_id': req.client_id, 'config': config, 'subscription_url': config if str(config).startswith('http') else ''}
|
||||||
else:
|
else:
|
||||||
|
inbound = req.xui_inbound_id or panel.get('default_inbound_id') or None
|
||||||
|
if not inbound:
|
||||||
|
return JSONResponse({'error': 'Select a VLESS inbound from 3x-ui'}, status_code=400)
|
||||||
from managers.xui_api import xui_create_vless_config
|
from managers.xui_api import xui_create_vless_config
|
||||||
created = await xui_create_vless_config(
|
created = await xui_create_vless_config(
|
||||||
data.get('settings', {}),
|
data.get('settings', {}),
|
||||||
name=req.name or user.get('username') or 'user',
|
name=req.name or user.get('username') or 'user',
|
||||||
# Optional hint only — panel assigns inbound + share link
|
inbound_id=inbound,
|
||||||
inbound_id=req.xui_inbound_id or panel.get('default_inbound_id') or None,
|
|
||||||
panel_id=panel_id,
|
panel_id=panel_id,
|
||||||
)
|
)
|
||||||
result = {
|
result = {
|
||||||
@@ -4498,8 +4500,9 @@ async def _create_config_for_protocol(
|
|||||||
if not panel:
|
if not panel:
|
||||||
raise RuntimeError('Add a 3x-ui server in Settings first')
|
raise RuntimeError('Add a 3x-ui server in Settings first')
|
||||||
panel_id = panel.get('id') or ''
|
panel_id = panel.get('id') or ''
|
||||||
# Prefer invite/default inbound; otherwise 3x-ui picks first VLESS on that panel
|
inbound = int(xui_inbound_id or 0) or int(panel.get('default_inbound_id') or 0) or None
|
||||||
inbound = xui_inbound_id or panel.get('default_inbound_id') or None
|
if not inbound:
|
||||||
|
raise RuntimeError('Select a VLESS inbound from 3x-ui')
|
||||||
expiry_ms = _expiry_ms_from_duration_days(duration_days)
|
expiry_ms = _expiry_ms_from_duration_days(duration_days)
|
||||||
created = await xui_create_vless_config(
|
created = await xui_create_vless_config(
|
||||||
data.get('settings', {}),
|
data.get('settings', {}),
|
||||||
@@ -4611,9 +4614,10 @@ async def api_create_invite(request: Request, req: InviteCreateRequest):
|
|||||||
if not panel:
|
if not panel:
|
||||||
return JSONResponse({'error': 'Add a 3x-ui server in Settings first'}, status_code=400)
|
return JSONResponse({'error': 'Add a 3x-ui server in Settings first'}, status_code=400)
|
||||||
panel_id = panel.get('id') or ''
|
panel_id = panel.get('id') or ''
|
||||||
# Inbound is optional — 3x-ui panel assigns it (or uses server default) on redeem
|
|
||||||
if not inbound_id:
|
if not inbound_id:
|
||||||
inbound_id = int(panel.get('default_inbound_id') or 0)
|
inbound_id = int(panel.get('default_inbound_id') or 0)
|
||||||
|
if not inbound_id:
|
||||||
|
return JSONResponse({'error': 'Select a VLESS inbound from 3x-ui'}, status_code=400)
|
||||||
link = {
|
link = {
|
||||||
'id': str(uuid.uuid4()),
|
'id': str(uuid.uuid4()),
|
||||||
'name': (req.name or 'Invite').strip() or 'Invite',
|
'name': (req.name or 'Invite').strip() or 'Invite',
|
||||||
@@ -4686,6 +4690,10 @@ async def api_update_invite(request: Request, invite_id: str, req: InviteUpdateR
|
|||||||
return JSONResponse({'error': 'Add a 3x-ui server in Settings first'}, status_code=400)
|
return JSONResponse({'error': 'Add a 3x-ui server in Settings first'}, status_code=400)
|
||||||
if not link.get('xui_panel_id'):
|
if not link.get('xui_panel_id'):
|
||||||
link['xui_panel_id'] = panel.get('id') or ''
|
link['xui_panel_id'] = panel.get('id') or ''
|
||||||
|
if not int(link.get('xui_inbound_id') or 0):
|
||||||
|
link['xui_inbound_id'] = int(panel.get('default_inbound_id') or 0)
|
||||||
|
if not int(link.get('xui_inbound_id') or 0):
|
||||||
|
return JSONResponse({'error': 'Select a VLESS inbound from 3x-ui'}, status_code=400)
|
||||||
save_data(data)
|
save_data(data)
|
||||||
return {'status': 'success', 'invite': _invite_public_view(link)}
|
return {'status': 'success', 'invite': _invite_public_view(link)}
|
||||||
|
|
||||||
|
|||||||
+108
-79
@@ -158,79 +158,113 @@ class XuiApi:
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def get_inbound(self, inbound_id: int) -> dict:
|
||||||
|
inbound_id = int(inbound_id)
|
||||||
|
for inbound in await self.list_inbounds():
|
||||||
|
if isinstance(inbound, dict) and int(inbound.get('id') or 0) == inbound_id:
|
||||||
|
return inbound
|
||||||
|
raise XuiApiError(f'Inbound #{inbound_id} not found on 3x-ui')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_inbound_settings(inbound: dict) -> dict:
|
||||||
|
settings_raw = inbound.get('settings') or '{}'
|
||||||
|
if isinstance(settings_raw, str):
|
||||||
|
try:
|
||||||
|
settings_obj = json.loads(settings_raw)
|
||||||
|
except Exception:
|
||||||
|
settings_obj = {}
|
||||||
|
else:
|
||||||
|
settings_obj = settings_raw if isinstance(settings_raw, dict) else {}
|
||||||
|
return settings_obj if isinstance(settings_obj, dict) else {}
|
||||||
|
|
||||||
|
def _client_template_from_inbound(self, inbound: dict) -> dict:
|
||||||
|
"""Copy only fields 3x-ui already uses on this inbound — do not invent extras."""
|
||||||
|
settings_obj = self._parse_inbound_settings(inbound)
|
||||||
|
clients = settings_obj.get('clients') or []
|
||||||
|
template = next((c for c in clients if isinstance(c, dict)), None) or {}
|
||||||
|
flow = template.get('flow')
|
||||||
|
if not isinstance(flow, str):
|
||||||
|
flow = ''
|
||||||
|
# If no existing clients, detect Reality → vision (required for those inbounds)
|
||||||
|
if not flow:
|
||||||
|
stream_raw = inbound.get('streamSettings') or '{}'
|
||||||
|
if isinstance(stream_raw, str):
|
||||||
|
try:
|
||||||
|
stream = json.loads(stream_raw)
|
||||||
|
except Exception:
|
||||||
|
stream = {}
|
||||||
|
else:
|
||||||
|
stream = stream_raw if isinstance(stream_raw, dict) else {}
|
||||||
|
security = (stream.get('security') or '').lower()
|
||||||
|
network = (stream.get('network') or '').lower()
|
||||||
|
if security == 'reality' and network in ('tcp', 'raw', ''):
|
||||||
|
flow = 'xtls-rprx-vision'
|
||||||
|
return {
|
||||||
|
'flow': flow,
|
||||||
|
'limitIp': int(template.get('limitIp') or 0),
|
||||||
|
'totalGB': 0,
|
||||||
|
'tgId': '' if template.get('tgId') in (None, 0) else str(template.get('tgId') or ''),
|
||||||
|
}
|
||||||
|
|
||||||
async def add_vless_client(
|
async def add_vless_client(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
email: str,
|
email: str,
|
||||||
inbound_id: int,
|
inbound_id: int,
|
||||||
comment: str = '',
|
|
||||||
enable: bool = True,
|
enable: bool = True,
|
||||||
total_gb: int = 0,
|
|
||||||
expiry_time: int = 0,
|
expiry_time: int = 0,
|
||||||
limit_ip: int = 0,
|
|
||||||
flow: str = '',
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
"""Add client via 3x-ui addClient only. Minimal payload; flow taken from inbound."""
|
||||||
email = (email or '').strip()
|
email = (email or '').strip()
|
||||||
if not email:
|
if not email:
|
||||||
raise XuiApiError('Client email is required')
|
raise XuiApiError('Client email is required')
|
||||||
if not inbound_id:
|
if not inbound_id:
|
||||||
raise XuiApiError('VLESS inbound id is required')
|
raise XuiApiError('VLESS inbound id is required')
|
||||||
|
|
||||||
|
inbound = await self.get_inbound(int(inbound_id))
|
||||||
|
if (inbound.get('protocol') or '').lower() != 'vless':
|
||||||
|
raise XuiApiError(f'Inbound #{inbound_id} is not VLESS')
|
||||||
|
|
||||||
|
tmpl = self._client_template_from_inbound(inbound)
|
||||||
client_uuid = str(uuid.uuid4())
|
client_uuid = str(uuid.uuid4())
|
||||||
sub_id = _new_sub_id()
|
sub_id = _new_sub_id()
|
||||||
|
|
||||||
|
# Minimal client object — nothing beyond what 3x-ui expects for this inbound
|
||||||
client = {
|
client = {
|
||||||
'id': client_uuid,
|
'id': client_uuid,
|
||||||
'email': email,
|
'email': email,
|
||||||
'enable': enable,
|
'enable': bool(enable),
|
||||||
'flow': flow or '',
|
'flow': tmpl['flow'],
|
||||||
'limitIp': limit_ip,
|
'limitIp': tmpl['limitIp'],
|
||||||
'totalGB': total_gb,
|
'totalGB': 0,
|
||||||
'expiryTime': expiry_time,
|
'expiryTime': int(expiry_time or 0),
|
||||||
'tgId': 0,
|
'tgId': tmpl['tgId'],
|
||||||
'subId': sub_id,
|
'subId': sub_id,
|
||||||
'comment': comment or '',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Modern API
|
# Official path: addClient with settings as JSON string (object form breaks some builds)
|
||||||
try:
|
await self._request(
|
||||||
await self._request(
|
'POST',
|
||||||
'POST',
|
'/panel/api/inbounds/addClient',
|
||||||
'/panel/api/clients/add',
|
json={
|
||||||
json={'client': client, 'inboundIds': [int(inbound_id)]},
|
'id': int(inbound_id),
|
||||||
)
|
'settings': json.dumps({'clients': [client]}),
|
||||||
except XuiApiError as modern_err:
|
},
|
||||||
logger.info('Modern clients/add failed (%s), trying legacy addClient', modern_err)
|
)
|
||||||
# Legacy: settings must be a JSON-encoded string on many builds
|
|
||||||
settings_obj = {'clients': [client]}
|
# Share links ONLY from the panel — never build vless:// here
|
||||||
try:
|
links = await self.get_client_links(email)
|
||||||
await self._request(
|
if not links:
|
||||||
'POST',
|
links = await self.get_sub_links(sub_id)
|
||||||
'/panel/api/inbounds/addClient',
|
|
||||||
json={
|
|
||||||
'id': int(inbound_id),
|
|
||||||
'settings': json.dumps(settings_obj),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except XuiApiError:
|
|
||||||
# Some builds accept nested object
|
|
||||||
await self._request(
|
|
||||||
'POST',
|
|
||||||
'/panel/api/inbounds/addClient',
|
|
||||||
json={
|
|
||||||
'id': int(inbound_id),
|
|
||||||
'settings': settings_obj,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Share URL is resolved by the caller via resolve_share_link (panel assigns link)
|
|
||||||
return {
|
return {
|
||||||
'client_id': email,
|
'client_id': email,
|
||||||
'uuid': client_uuid,
|
'uuid': client_uuid,
|
||||||
'sub_id': sub_id,
|
'sub_id': sub_id,
|
||||||
'email': email,
|
'email': email,
|
||||||
'config': '',
|
'config': '',
|
||||||
'links': [],
|
'links': links,
|
||||||
'expiry_time': expiry_time,
|
'expiry_time': int(expiry_time or 0),
|
||||||
'inbound_id': int(inbound_id),
|
'inbound_id': int(inbound_id),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,33 +335,38 @@ class XuiApi:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
async def resolve_share_link(self, *, email: str, sub_id: str = '', configured_sub_base: str = '') -> dict:
|
async def resolve_share_link(self, *, email: str, sub_id: str = '', configured_sub_base: str = '') -> dict:
|
||||||
"""Ask the panel for the shareable subscription / protocol link (do not invent config)."""
|
"""Use only links returned by 3x-ui. Do not synthesize protocol configs."""
|
||||||
email = (email or '').strip()
|
email = (email or '').strip()
|
||||||
sub_id = (sub_id or '').strip()
|
sub_id = (sub_id or '').strip()
|
||||||
|
|
||||||
|
links: list = []
|
||||||
|
if email:
|
||||||
|
links = await self.get_client_links(email)
|
||||||
|
if not links and sub_id:
|
||||||
|
links = await self.get_sub_links(sub_id)
|
||||||
|
|
||||||
|
# Prefer protocol share from panel; then HTTP(S) subscription URL from panel
|
||||||
|
panel_proto = next(
|
||||||
|
(u for u in links if isinstance(u, str) and u.startswith(('vless://', 'vmess://', 'trojan://', 'ss://'))),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
panel_http = next(
|
||||||
|
(u for u in links if isinstance(u, str) and u.startswith(('http://', 'https://'))),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Subscription URL: panel HTTP link first; optional configured base only as last resort
|
||||||
base = (configured_sub_base or '').strip().rstrip('/')
|
base = (configured_sub_base or '').strip().rstrip('/')
|
||||||
if not base:
|
if not panel_http and not base:
|
||||||
try:
|
try:
|
||||||
base = (await self.get_panel_sub_base() or '').rstrip('/')
|
base = (await self.get_panel_sub_base() or '').rstrip('/')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('get_panel_sub_base failed: %s', e)
|
logger.warning('get_panel_sub_base failed: %s', e)
|
||||||
base = ''
|
base = ''
|
||||||
|
|
||||||
links = []
|
|
||||||
if sub_id:
|
|
||||||
links = await self.get_sub_links(sub_id)
|
|
||||||
if not links and email:
|
|
||||||
links = await self.get_client_links(email)
|
|
||||||
|
|
||||||
# Prefer HTTP(S) link returned by 3x-ui; else build from panel/settings sub base + subId
|
|
||||||
panel_http = next(
|
|
||||||
(u for u in links if isinstance(u, str) and u.startswith(('http://', 'https://'))),
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
subscription_url = panel_http or (f'{base}/{sub_id}' if base and sub_id else '')
|
subscription_url = panel_http or (f'{base}/{sub_id}' if base and sub_id else '')
|
||||||
share = subscription_url or next(
|
|
||||||
(u for u in links if isinstance(u, str) and u.startswith(('vless://', 'vmess://', 'trojan://', 'ss://'))),
|
# Prefer panel-generated protocol link (real config); else subscription URL
|
||||||
'',
|
share = panel_proto or subscription_url or (links[0] if links else '')
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
'subscription_url': subscription_url,
|
'subscription_url': subscription_url,
|
||||||
'links': links,
|
'links': links,
|
||||||
@@ -335,6 +374,7 @@ class XuiApi:
|
|||||||
'sub_base': base,
|
'sub_base': base,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def delete_client(self, email: str) -> None:
|
async def delete_client(self, email: str) -> None:
|
||||||
email = (email or '').strip()
|
email = (email or '').strip()
|
||||||
if not email:
|
if not email:
|
||||||
@@ -408,7 +448,7 @@ async def xui_create_vless_config(
|
|||||||
expiry_time: int = 0,
|
expiry_time: int = 0,
|
||||||
panel_id: Optional[str] = None,
|
panel_id: Optional[str] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Create a client via 3x-ui API; inbound + share link come from the panel response."""
|
"""Create client on selected 3x-ui inbound; return only links from the panel."""
|
||||||
scoped = _resolve_settings(settings, panel_id)
|
scoped = _resolve_settings(settings, panel_id)
|
||||||
creds = _settings_creds(scoped)
|
creds = _settings_creds(scoped)
|
||||||
inbound = inbound_id if inbound_id not in (None, 0, '0') else creds.get('inbound_id')
|
inbound = inbound_id if inbound_id not in (None, 0, '0') else creds.get('inbound_id')
|
||||||
@@ -416,15 +456,10 @@ async def xui_create_vless_config(
|
|||||||
inbound = int(inbound or 0)
|
inbound = int(inbound or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
inbound = 0
|
inbound = 0
|
||||||
|
if not inbound:
|
||||||
|
raise XuiApiError('Select a VLESS inbound from 3x-ui')
|
||||||
|
|
||||||
async with XuiApi.from_panel_settings(scoped) as api:
|
async with XuiApi.from_panel_settings(scoped) as api:
|
||||||
# Panel assigns inbound: use configured default, else first VLESS on that panel
|
|
||||||
if not inbound:
|
|
||||||
vless_inbounds = await api.list_vless_inbounds()
|
|
||||||
if not vless_inbounds:
|
|
||||||
raise XuiApiError('No VLESS inbound found on 3x-ui — create one in the panel first')
|
|
||||||
inbound = int(vless_inbounds[0]['id'])
|
|
||||||
|
|
||||||
base = ''.join(ch if ch.isalnum() or ch in '._-+@' else '_' for ch in (name or 'user').strip())
|
base = ''.join(ch if ch.isalnum() or ch in '._-+@' else '_' for ch in (name or 'user').strip())
|
||||||
base = base[:48] or f'user_{secrets.token_hex(4)}'
|
base = base[:48] or f'user_{secrets.token_hex(4)}'
|
||||||
email = base
|
email = base
|
||||||
@@ -434,7 +469,6 @@ async def xui_create_vless_config(
|
|||||||
created = await api.add_vless_client(
|
created = await api.add_vless_client(
|
||||||
email=email,
|
email=email,
|
||||||
inbound_id=inbound,
|
inbound_id=inbound,
|
||||||
comment=name or email,
|
|
||||||
expiry_time=int(expiry_time or 0),
|
expiry_time=int(expiry_time or 0),
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@@ -447,24 +481,19 @@ async def xui_create_vless_config(
|
|||||||
created = await api.add_vless_client(
|
created = await api.add_vless_client(
|
||||||
email=email,
|
email=email,
|
||||||
inbound_id=inbound,
|
inbound_id=inbound,
|
||||||
comment=name or email,
|
|
||||||
expiry_time=int(expiry_time or 0),
|
expiry_time=int(expiry_time or 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ask the panel for the subscription / protocol link (do not invent config here)
|
|
||||||
share = await api.resolve_share_link(
|
share = await api.resolve_share_link(
|
||||||
email=created.get('email') or email,
|
email=created.get('email') or email,
|
||||||
sub_id=created.get('sub_id') or '',
|
sub_id=created.get('sub_id') or '',
|
||||||
configured_sub_base=creds.get('sub_url') or '',
|
configured_sub_base=creds.get('sub_url') or '',
|
||||||
)
|
)
|
||||||
subscription_url = share.get('subscription_url') or ''
|
|
||||||
panel_share = share.get('share') or ''
|
|
||||||
panel_links = share.get('links') or created.get('links') or []
|
panel_links = share.get('links') or created.get('links') or []
|
||||||
|
|
||||||
created['subscription_url'] = subscription_url
|
|
||||||
created['links'] = panel_links
|
created['links'] = panel_links
|
||||||
# What we show the user: panel subscription URL, else panel protocol link
|
created['subscription_url'] = share.get('subscription_url') or ''
|
||||||
created['config'] = subscription_url or panel_share or created.get('config') or ''
|
# Prefer real vless:// (etc.) from 3x-ui; subscription URL only as fallback
|
||||||
|
created['config'] = share.get('share') or ''
|
||||||
if not created['config'] and panel_links:
|
if not created['config'] and panel_links:
|
||||||
created['config'] = panel_links[0]
|
created['config'] = panel_links[0]
|
||||||
created['inbound_id'] = int(created.get('inbound_id') or inbound)
|
created['inbound_id'] = int(created.get('inbound_id') or inbound)
|
||||||
|
|||||||
+45
-5
@@ -44,6 +44,7 @@
|
|||||||
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:2px;">
|
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:2px;">
|
||||||
{% if inv.protocol == 'xui' %}
|
{% if inv.protocol == 'xui' %}
|
||||||
3x-ui · {% for s in xui_servers if s.id == inv.xui_panel_id %}{{ s.name }}{% else %}{{ _('xui_server_select_label') }}{% endfor %}
|
3x-ui · {% for s in xui_servers if s.id == inv.xui_panel_id %}{{ s.name }}{% else %}{{ _('xui_server_select_label') }}{% endfor %}
|
||||||
|
· inbound #{{ inv.xui_inbound_id or '—' }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ inv.protocol }}
|
{{ inv.protocol }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -141,12 +142,17 @@
|
|||||||
|
|
||||||
<div class="form-group" id="inviteInboundGroup">
|
<div class="form-group" id="inviteInboundGroup">
|
||||||
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||||
<select class="form-select" id="inviteXuiPanel">
|
<select class="form-select" id="inviteXuiPanel" onchange="loadInviteInbounds()">
|
||||||
{% for s in xui_servers %}
|
{% for s in xui_servers %}
|
||||||
<option value="{{ s.id }}">{{ s.name }}</option>
|
<option value="{{ s.id }}">{{ s.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
||||||
|
<label class="form-label" style="margin-top:var(--space-sm);">{{ _('xui_inbound_label') }}</label>
|
||||||
|
<select class="form-select" id="inviteInboundId">
|
||||||
|
<option value="">{{ _('invite_inbound_loading') }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-hint">{{ _('invite_inbound_hint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -181,6 +187,7 @@
|
|||||||
<script>
|
<script>
|
||||||
const invitesData = {{ invites | tojson }};
|
const invitesData = {{ invites | tojson }};
|
||||||
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
|
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
|
||||||
|
const xuiDefaultInbound = {{ xui_default_inbound | int }};
|
||||||
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
|
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
|
||||||
|
|
||||||
function inviteUrl(token) {
|
function inviteUrl(token) {
|
||||||
@@ -195,6 +202,38 @@
|
|||||||
function updateInviteProtoFields() {
|
function updateInviteProtoFields() {
|
||||||
const v = document.getElementById('inviteProtocol').value;
|
const v = document.getElementById('inviteProtocol').value;
|
||||||
document.getElementById('inviteInboundGroup').style.display = (v === 'xui') ? '' : 'none';
|
document.getElementById('inviteInboundGroup').style.display = (v === 'xui') ? '' : 'none';
|
||||||
|
if (v === 'xui') loadInviteInbounds(document.getElementById('inviteInboundId').value || xuiDefaultInbound);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInviteInbounds(selected) {
|
||||||
|
const select = document.getElementById('inviteInboundId');
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_loading')}</option>`;
|
||||||
|
if (!xuiConfigured) {
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_need_xui')}</option>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const panelId = document.getElementById('inviteXuiPanel')?.value || xuiDefaultPanelId || '';
|
||||||
|
const q = panelId ? `?panel_id=${encodeURIComponent(panelId)}` : '';
|
||||||
|
const data = await apiCall('/api/settings/xui/inbounds' + q);
|
||||||
|
const list = data.inbounds || [];
|
||||||
|
if (!list.length) {
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_empty')}</option>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.innerHTML = '';
|
||||||
|
list.forEach((ib, idx) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = ib.id;
|
||||||
|
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||||
|
const prefer = selected || xuiDefaultInbound;
|
||||||
|
if (prefer && String(ib.id) === String(prefer)) opt.selected = true;
|
||||||
|
else if (!prefer && idx === 0) opt.selected = true;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
select.innerHTML = `<option value="">${_('error')}: ${e.message}</option>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseProtocolValue() {
|
function parseProtocolValue() {
|
||||||
@@ -234,6 +273,7 @@
|
|||||||
const p = document.getElementById('inviteXuiPanel');
|
const p = document.getElementById('inviteXuiPanel');
|
||||||
if (p && [...p.options].some(o => o.value === xuiDefaultPanelId)) p.value = xuiDefaultPanelId;
|
if (p && [...p.options].some(o => o.value === xuiDefaultPanelId)) p.value = xuiDefaultPanelId;
|
||||||
}
|
}
|
||||||
|
loadInviteInbounds(xuiDefaultInbound);
|
||||||
openModal('inviteModal');
|
openModal('inviteModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +297,7 @@
|
|||||||
if (panelSel && inv.xui_panel_id && [...panelSel.options].some(o => o.value === inv.xui_panel_id)) {
|
if (panelSel && inv.xui_panel_id && [...panelSel.options].some(o => o.value === inv.xui_panel_id)) {
|
||||||
panelSel.value = inv.xui_panel_id;
|
panelSel.value = inv.xui_panel_id;
|
||||||
}
|
}
|
||||||
|
loadInviteInbounds(inv.xui_inbound_id || xuiDefaultInbound);
|
||||||
openModal('inviteModal');
|
openModal('inviteModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +309,8 @@
|
|||||||
try {
|
try {
|
||||||
const editId = document.getElementById('inviteEditId').value;
|
const editId = document.getElementById('inviteEditId').value;
|
||||||
const proto = parseProtocolValue();
|
const proto = parseProtocolValue();
|
||||||
if (proto.protocol === 'xui' && !document.getElementById('inviteXuiPanel')?.value) {
|
const inbound = parseInt(document.getElementById('inviteInboundId').value || '0');
|
||||||
throw new Error(_('invite_inbound_need_xui'));
|
if (proto.protocol === 'xui' && !inbound) throw new Error(_('invite_inbound_required'));
|
||||||
}
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
name: document.getElementById('inviteName').value || 'Invite',
|
name: document.getElementById('inviteName').value || 'Invite',
|
||||||
@@ -279,7 +319,7 @@
|
|||||||
user_id: document.getElementById('inviteUserId').value || '',
|
user_id: document.getElementById('inviteUserId').value || '',
|
||||||
protocol: proto.protocol,
|
protocol: proto.protocol,
|
||||||
server_id: proto.server_id,
|
server_id: proto.server_id,
|
||||||
xui_inbound_id: 0,
|
xui_inbound_id: inbound,
|
||||||
xui_panel_id: document.getElementById('inviteXuiPanel')?.value || '',
|
xui_panel_id: document.getElementById('inviteXuiPanel')?.value || '',
|
||||||
note: document.getElementById('inviteNote').value || '',
|
note: document.getElementById('inviteNote').value || '',
|
||||||
};
|
};
|
||||||
|
|||||||
+48
-6
@@ -127,13 +127,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="guestInboundGroup" style="{% if settings.guest.create_protocol != 'xui' %}display:none;{% endif %}">
|
<div class="form-group" id="guestInboundGroup" style="{% if settings.guest.create_protocol != 'xui' %}display:none;{% endif %}">
|
||||||
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||||
<select class="form-select" id="guest_create_xui_panel">
|
<select class="form-select" id="guest_create_xui_panel" onchange="loadGuestInbounds()">
|
||||||
{% for s in xui_servers %}
|
{% for s in xui_servers %}
|
||||||
<option value="{{ s.id }}" {% if settings.guest.create_xui_panel_id == s.id %}selected{% endif %}>{{ s.name }}</option>
|
<option value="{{ s.id }}" {% if settings.guest.create_xui_panel_id == s.id %}selected{% endif %}>{{ s.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
<label class="form-label" style="margin-top:var(--space-sm);">{{ _('xui_inbound_label') }}</label>
|
||||||
<input type="hidden" id="guest_create_inbound_id" value="0">
|
<select class="form-select" id="guest_create_inbound_id">
|
||||||
|
<option value="0">{{ _('invite_inbound_loading') }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-hint">{{ _('xui_inbound_hint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -850,9 +853,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inbound + share link are assigned by the selected 3x-ui panel on create
|
// Load VLESS inbounds from the selected 3x-ui panel
|
||||||
async function loadXuiInbounds() {}
|
async function loadXuiInbounds() {
|
||||||
async function loadGuestInbounds() {}
|
await loadGuestInbounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGuestInbounds() {
|
||||||
|
const select = document.getElementById('guest_create_inbound_id');
|
||||||
|
if (!select) return;
|
||||||
|
const preferred = {{ settings.guest.create_inbound_id | default(0) | int }};
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_loading')}</option>`;
|
||||||
|
try {
|
||||||
|
const panelSel = document.getElementById('guest_create_xui_panel');
|
||||||
|
const panelId = panelSel ? panelSel.value : '';
|
||||||
|
const q = panelId ? `?panel_id=${encodeURIComponent(panelId)}` : '';
|
||||||
|
const res = await fetch('/api/settings/xui/inbounds' + q);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Failed to load inbounds');
|
||||||
|
const list = data.inbounds || [];
|
||||||
|
if (!list.length) {
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_empty')}</option>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.innerHTML = '';
|
||||||
|
list.forEach((ib, idx) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = ib.id;
|
||||||
|
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||||
|
if (preferred && String(ib.id) === String(preferred)) opt.selected = true;
|
||||||
|
else if (!preferred && idx === 0) opt.selected = true;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('loadGuestInbounds:', err);
|
||||||
|
select.innerHTML = `<option value="">${_('error')}: ${err.message}</option>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('syncCreateConns').addEventListener('change', (e) => {
|
document.getElementById('syncCreateConns').addEventListener('change', (e) => {
|
||||||
document.getElementById('autoConnFields').style.display = e.target.checked ? 'block' : 'none';
|
document.getElementById('autoConnFields').style.display = e.target.checked ? 'block' : 'none';
|
||||||
@@ -1286,6 +1322,12 @@
|
|||||||
const v = document.getElementById('guest_create_protocol')?.value;
|
const v = document.getElementById('guest_create_protocol')?.value;
|
||||||
const group = document.getElementById('guestInboundGroup');
|
const group = document.getElementById('guestInboundGroup');
|
||||||
if (group) group.style.display = (v === 'xui') ? 'block' : 'none';
|
if (group) group.style.display = (v === 'xui') ? 'block' : 'none';
|
||||||
|
if (v === 'xui') loadGuestInbounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefill guest inbound list when page opens on 3x-ui
|
||||||
|
if (document.getElementById('guest_create_protocol')?.value === 'xui') {
|
||||||
|
loadGuestInbounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyGuestLink() {
|
function copyGuestLink() {
|
||||||
|
|||||||
+39
-4
@@ -293,12 +293,16 @@
|
|||||||
|
|
||||||
<div class="form-group" id="ucXuiInboundGroup" style="display:none;">
|
<div class="form-group" id="ucXuiInboundGroup" style="display:none;">
|
||||||
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||||
<select class="form-select" id="ucXuiPanel">
|
<select class="form-select" id="ucXuiPanel" onchange="loadXuiInbounds()">
|
||||||
{% for s in xui_servers %}
|
{% for s in xui_servers %}
|
||||||
<option value="{{ s.id }}" {% if s.id == xui_default_panel_id %}selected{% endif %}>{{ s.name }}</option>
|
<option value="{{ s.id }}" {% if s.id == xui_default_panel_id %}selected{% endif %}>{{ s.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
<label class="form-label" style="margin-top:var(--space-sm);">{{ _('xui_inbound_label') }}</label>
|
||||||
|
<select class="form-select" id="ucXuiInbound">
|
||||||
|
<option value="">{{ _('invite_inbound_loading') }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-hint">{{ _('xui_inbound_hint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="ucNameGroup">
|
<div class="form-group" id="ucNameGroup">
|
||||||
@@ -639,14 +643,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadXuiInbounds() {
|
async function loadXuiInbounds() {
|
||||||
// Inbound is assigned by the selected 3x-ui panel — nothing to load here.
|
const select = document.getElementById('ucXuiInbound');
|
||||||
|
if (!select) return;
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_loading')}</option>`;
|
||||||
|
try {
|
||||||
|
const panelId = document.getElementById('ucXuiPanel')?.value || '';
|
||||||
|
const q = panelId ? `?panel_id=${encodeURIComponent(panelId)}` : '';
|
||||||
|
const data = await apiCall('/api/settings/xui/inbounds' + q);
|
||||||
|
const list = data.inbounds || [];
|
||||||
|
if (!list.length) {
|
||||||
|
select.innerHTML = `<option value="">${_('invite_inbound_empty')}</option>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.innerHTML = '';
|
||||||
|
list.forEach((ib, idx) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = ib.id;
|
||||||
|
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||||
|
if (xuiDefaultInboundId && String(ib.id) === String(xuiDefaultInboundId)) opt.selected = true;
|
||||||
|
else if (!xuiDefaultInboundId && idx === 0) opt.selected = true;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
select.innerHTML = `<option value="">${_('error')}: ${err.message}</option>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateXuiInboundVisibility() {
|
function updateXuiInboundVisibility() {
|
||||||
const proto = document.getElementById('ucProtocol')?.value;
|
const proto = document.getElementById('ucProtocol')?.value;
|
||||||
const group = document.getElementById('ucXuiInboundGroup');
|
const group = document.getElementById('ucXuiInboundGroup');
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
group.style.display = (proto === 'xui') ? 'block' : 'none';
|
if (proto === 'xui') {
|
||||||
|
group.style.display = 'block';
|
||||||
|
loadXuiInbounds();
|
||||||
|
} else {
|
||||||
|
group.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide connection fields
|
// Show/hide connection fields
|
||||||
@@ -847,6 +879,9 @@
|
|||||||
if (!clientId) throw new Error(_('select_connection'));
|
if (!clientId) throw new Error(_('select_connection'));
|
||||||
body.client_id = clientId;
|
body.client_id = clientId;
|
||||||
} else if (body.protocol === 'xui') {
|
} else if (body.protocol === 'xui') {
|
||||||
|
const inbound = parseInt(document.getElementById('ucXuiInbound').value || '0');
|
||||||
|
if (!inbound) throw new Error(_('invite_inbound_required'));
|
||||||
|
body.xui_inbound_id = inbound;
|
||||||
body.xui_panel_id = document.getElementById('ucXuiPanel')?.value || '';
|
body.xui_panel_id = document.getElementById('ucXuiPanel')?.value || '';
|
||||||
} else if (body.protocol === 'telemt') {
|
} else if (body.protocol === 'telemt') {
|
||||||
body.telemt_quota = document.getElementById('ucTelemtQuota').value || null;
|
body.telemt_quota = document.getElementById('ucTelemtQuota').value || null;
|
||||||
|
|||||||
@@ -220,11 +220,11 @@
|
|||||||
"invite_duration_short": "Lifetime",
|
"invite_duration_short": "Lifetime",
|
||||||
"invite_duration_starts_hint": "After you get a config it will work for {} day(s)",
|
"invite_duration_starts_hint": "After you get a config it will work for {} day(s)",
|
||||||
"invite_config_expires_at": "Config valid until: {}",
|
"invite_config_expires_at": "Config valid until: {}",
|
||||||
"invite_inbound_hint": "Choose the VLESS inbound once — all configs from this link use it",
|
"invite_inbound_hint": "Inbounds are loaded from the selected 3x-ui panel. Only 3x-ui returns the config/share link.",
|
||||||
"invite_inbound_loading": "Loading inbounds…",
|
"invite_inbound_loading": "Loading inbounds from 3x-ui…",
|
||||||
"invite_inbound_empty": "No VLESS inbounds found",
|
"invite_inbound_empty": "No VLESS inbounds on this 3x-ui panel",
|
||||||
"invite_inbound_need_xui": "Configure 3x-ui in Settings first",
|
"invite_inbound_need_xui": "Configure 3x-ui in Settings first",
|
||||||
"invite_inbound_required": "Select a VLESS inbound",
|
"invite_inbound_required": "Select a VLESS inbound from 3x-ui",
|
||||||
"invite_sub_url_missing_title": "Subscription URL is not set",
|
"invite_sub_url_missing_title": "Subscription URL is not set",
|
||||||
"invite_sub_url_missing_hint": "In Settings → 3x-ui set Subscription URL base so invite links issue /sub/… configs.",
|
"invite_sub_url_missing_hint": "In Settings → 3x-ui set Subscription URL base so invite links issue /sub/… configs.",
|
||||||
"invite_sub_tab": "Subscription",
|
"invite_sub_tab": "Subscription",
|
||||||
@@ -301,16 +301,16 @@
|
|||||||
"xui_sync_hint": "Clients from 3x-ui inbounds will be created, disabled, and deleted as panel users (matched by email)",
|
"xui_sync_hint": "Clients from 3x-ui inbounds will be created, disabled, and deleted as panel users (matched by email)",
|
||||||
"xui_inbound_label": "VLESS inbound",
|
"xui_inbound_label": "VLESS inbound",
|
||||||
"xui_inbound_auto": "Auto (first VLESS inbound)",
|
"xui_inbound_auto": "Auto (first VLESS inbound)",
|
||||||
"xui_inbound_hint": "The 3x-ui panel assigns the inbound when creating a client. This site only sends the API request.",
|
"xui_inbound_hint": "Inbounds are loaded from 3x-ui. This site only creates the client and takes the ready share link — no local config generation.",
|
||||||
"xui_servers_title": "3x-ui servers",
|
"xui_servers_title": "3x-ui servers",
|
||||||
"xui_servers_hint": "Add 3x-ui panels. When creating invites/configs, pick a server — the panel assigns the inbound and returns the share link.",
|
"xui_servers_hint": "Add 3x-ui panels. When creating, pick a server and an inbound from the 3x-ui list — the panel returns the share link.",
|
||||||
"xui_servers_empty": "No 3x-ui servers yet. Click Add server.",
|
"xui_servers_empty": "No 3x-ui servers yet. Click Add server.",
|
||||||
"xui_servers_manage_hint": "Panel URLs and subscription bases are managed in the “3x-ui servers” section below.",
|
"xui_servers_manage_hint": "Panel URLs and subscription bases are managed in the “3x-ui servers” section below.",
|
||||||
"xui_server_add": "Add server",
|
"xui_server_add": "Add server",
|
||||||
"xui_server_name_label": "Name / description",
|
"xui_server_name_label": "Name / description",
|
||||||
"xui_server_name_hint": "Shown in invites and user forms (e.g. US, NL, Home).",
|
"xui_server_name_hint": "Shown in invites and user forms (e.g. US, NL, Home).",
|
||||||
"xui_server_select_label": "3x-ui server",
|
"xui_server_select_label": "3x-ui server",
|
||||||
"xui_server_select_hint": "Pick a 3x-ui panel — it assigns the inbound and returns the subscription link.",
|
"xui_server_select_hint": "Pick a 3x-ui panel, then an inbound from the list that panel returns via API.",
|
||||||
"xui_server_delete_confirm": "Delete this 3x-ui server from the panel?",
|
"xui_server_delete_confirm": "Delete this 3x-ui server from the panel?",
|
||||||
"sync_now_btn": "🔄 Sync now",
|
"sync_now_btn": "🔄 Sync now",
|
||||||
"delete_sync_btn": "🗑 Delete synchronization",
|
"delete_sync_btn": "🗑 Delete synchronization",
|
||||||
|
|||||||
@@ -220,11 +220,11 @@
|
|||||||
"invite_duration_short": "Срок",
|
"invite_duration_short": "Срок",
|
||||||
"invite_duration_starts_hint": "После получения конфиг будет действовать {} дн.",
|
"invite_duration_starts_hint": "После получения конфиг будет действовать {} дн.",
|
||||||
"invite_config_expires_at": "Конфиг действует до: {}",
|
"invite_config_expires_at": "Конфиг действует до: {}",
|
||||||
"invite_inbound_hint": "Укажите нужный VLESS inbound один раз — все конфиги с этой ссылки пойдут в него",
|
"invite_inbound_hint": "Список inbound загружается из выбранной панели 3x-ui. Конфиг и ссылку отдаёт только 3x-ui.",
|
||||||
"invite_inbound_loading": "Загрузка inbound…",
|
"invite_inbound_loading": "Загрузка inbound из 3x-ui…",
|
||||||
"invite_inbound_empty": "VLESS inbound не найдены",
|
"invite_inbound_empty": "VLESS inbound не найдены на этой панели 3x-ui",
|
||||||
"invite_inbound_need_xui": "Сначала настройте 3x-ui в Настройках",
|
"invite_inbound_need_xui": "Сначала настройте 3x-ui в Настройках",
|
||||||
"invite_inbound_required": "Выберите VLESS inbound",
|
"invite_inbound_required": "Выберите VLESS inbound из 3x-ui",
|
||||||
"invite_sub_url_missing_title": "Не задан URL подписки",
|
"invite_sub_url_missing_title": "Не задан URL подписки",
|
||||||
"invite_sub_url_missing_hint": "В Настройки → 3x-ui укажите «Базовый URL подписки», чтобы ссылки выдавали /sub/… конфиги.",
|
"invite_sub_url_missing_hint": "В Настройки → 3x-ui укажите «Базовый URL подписки», чтобы ссылки выдавали /sub/… конфиги.",
|
||||||
"invite_sub_tab": "Подписка",
|
"invite_sub_tab": "Подписка",
|
||||||
@@ -301,16 +301,16 @@
|
|||||||
"xui_sync_hint": "Клиенты из inbounds 3x-ui будут создаваться, отключаться и удаляться как пользователи панели (связь по email)",
|
"xui_sync_hint": "Клиенты из inbounds 3x-ui будут создаваться, отключаться и удаляться как пользователи панели (связь по email)",
|
||||||
"xui_inbound_label": "VLESS inbound",
|
"xui_inbound_label": "VLESS inbound",
|
||||||
"xui_inbound_auto": "Авто (первый VLESS inbound)",
|
"xui_inbound_auto": "Авто (первый VLESS inbound)",
|
||||||
"xui_inbound_hint": "Inbound выбирает панель 3x-ui при создании клиента. Здесь сайт только отправляет запрос.",
|
"xui_inbound_hint": "Inbound подгружается из 3x-ui. Сайт только создаёт клиента и забирает готовую ссылку — без своей генерации конфига.",
|
||||||
"xui_servers_title": "Серверы 3x-ui",
|
"xui_servers_title": "Серверы 3x-ui",
|
||||||
"xui_servers_hint": "Добавьте панели 3x-ui. При создании инвайта/конфига выберите сервер — панель сама назначит inbound и вернёт ссылку.",
|
"xui_servers_hint": "Добавьте панели 3x-ui. При создании выберите сервер и inbound из списка 3x-ui — ссылку вернёт панель.",
|
||||||
"xui_servers_empty": "Серверов 3x-ui пока нет. Нажмите «Добавить сервер».",
|
"xui_servers_empty": "Серверов 3x-ui пока нет. Нажмите «Добавить сервер».",
|
||||||
"xui_servers_manage_hint": "URL панелей и базовые URL подписок настраиваются в разделе «Серверы 3x-ui» ниже.",
|
"xui_servers_manage_hint": "URL панелей и базовые URL подписок настраиваются в разделе «Серверы 3x-ui» ниже.",
|
||||||
"xui_server_add": "Добавить сервер",
|
"xui_server_add": "Добавить сервер",
|
||||||
"xui_server_name_label": "Название / описание",
|
"xui_server_name_label": "Название / описание",
|
||||||
"xui_server_name_hint": "Так сервер будет отображаться в инвайтах и у пользователей (например: US, NL, Home).",
|
"xui_server_name_hint": "Так сервер будет отображаться в инвайтах и у пользователей (например: US, NL, Home).",
|
||||||
"xui_server_select_label": "Сервер 3x-ui",
|
"xui_server_select_label": "Сервер 3x-ui",
|
||||||
"xui_server_select_hint": "Выберите панель 3x-ui — она сама назначит inbound и вернёт ссылку подписки.",
|
"xui_server_select_hint": "Выберите панель 3x-ui, затем inbound из списка, который она отдаёт по API.",
|
||||||
"xui_server_delete_confirm": "Удалить этот сервер 3x-ui из панели?",
|
"xui_server_delete_confirm": "Удалить этот сервер 3x-ui из панели?",
|
||||||
"sync_now_btn": "🔄 Синхронизировать сейчас",
|
"sync_now_btn": "🔄 Синхронизировать сейчас",
|
||||||
"delete_sync_btn": "🗑 Удалить синхронизацию",
|
"delete_sync_btn": "🗑 Удалить синхронизацию",
|
||||||
|
|||||||
Reference in New Issue
Block a user