diff --git a/app.py b/app.py index 3a648ce..a080753 100644 --- a/app.py +++ b/app.py @@ -1422,10 +1422,14 @@ async def _xui_fetch_clients(client: httpx.AsyncClient, base_url: str, headers: return [] -async def sync_users_with_xui(data: dict): - """Import / update panel users from an existing 3x-ui instance.""" +async def sync_users_with_xui(data: dict, *, force: bool = False): + """Import / update panel users from an existing 3x-ui instance. + + force=True: used by the manual "Sync now" button (ignores xui_sync_users flag). + Background jobs keep force=False and only run when xui_sync_users is enabled. + """ settings = data.get('settings', {}).get('sync', {}) - if not settings.get('xui_sync_users'): + if not force and not settings.get('xui_sync_users'): return 0, "3x-ui synchronization is disabled in settings" url = (settings.get('xui_url') or '').strip() @@ -4042,8 +4046,34 @@ async def api_xui_sync_now(request: Request): if not _check_admin(request): return JSONResponse({'error': 'Forbidden'}, status_code=403) data = load_data() - count, msg = await sync_users_with_xui(data) - return {'status': 'success', 'count': count, 'message': msg} + + # Allow syncing with credentials from the request body (form may not be saved yet) + try: + body = await request.json() + except Exception: + body = {} + if isinstance(body, dict) and body: + sync_cfg = data.setdefault('settings', {}).setdefault('sync', {}) + for key in ( + 'xui_url', 'xui_username', 'xui_password', 'xui_api_token', + 'xui_create_conns', 'xui_server_id', 'xui_protocol', 'xui_sync_users', 'xui_sync', + ): + if key in body and body[key] is not None: + sync_cfg[key] = body[key] + # Persist so background sync / next page load keep the values + async with DATA_LOCK: + current = load_data() + current.setdefault('settings', {}).setdefault('sync', {}).update(sync_cfg) + save_data(current) + data = current + + count, msg = await sync_users_with_xui(data, force=True) + ok = count > 0 or (isinstance(msg, str) and msg.startswith('Successfully')) + return { + 'status': 'success' if ok else 'error', + 'count': count, + 'message': msg, + } @app.post('/api/settings/xui_sync_delete', tags=["Settings"]) diff --git a/templates/settings.html b/templates/settings.html index 597267f..cb6fe73 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -881,23 +881,35 @@ const btn = document.getElementById('xuiSyncNowBtn'); const text = document.getElementById('xuiSyncNowBtnText'); const spinner = document.getElementById('xuiSyncNowSpinner'); + const syncForm = document.getElementById('syncForm'); btn.disabled = true; text.textContent = _('sync_running'); spinner.classList.remove('hidden'); try { - const res = await fetch('/api/settings/xui_sync_now', { method: 'POST' }); + const payload = { + xui_sync: true, + xui_sync_users: true, + xui_url: syncForm.xui_url.value.trim(), + xui_username: syncForm.xui_username.value.trim(), + xui_password: syncForm.xui_password.value, + xui_api_token: syncForm.xui_api_token.value.trim(), + xui_create_conns: syncForm.xui_create_conns.checked, + xui_server_id: parseInt(syncForm.xui_server_id.value || '0'), + xui_protocol: syncForm.xui_protocol.value + }; + const res = await fetch('/api/settings/xui_sync_now', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); const data = await res.json(); if (data.status === 'success') { showToast(_('sync_success').replace('{}', data.count), 'success'); - if (data.message && data.count === 0) { - showToast(data.message, 'error'); - } else { - setTimeout(() => window.location.reload(), 1500); - } + setTimeout(() => window.location.reload(), 1500); } else { - throw new Error(data.message || 'Error occurred'); + throw new Error(data.message || data.error || 'Error occurred'); } } catch (err) { showToast(`${_('error')}: ` + err.message, 'error');