import { renderHeader } from '../components/header.js'; import { saveEntrySettings, state } from '../state.js'; import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '../services/server-health-service.js'; export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' }; const SERVER_FIELDS = [ { key: 'solanaServer', label: 'Адрес Solana сервера' }, { key: 'shineServerLogin', label: 'Логин сервера Сияние' }, { key: 'arweaveServer', label: 'Адрес сервера Arweave' }, ]; export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; const draft = { language: state.entrySettings.language, solanaServer: state.entrySettings.solanaServer, shineServerLogin: state.entrySettings.shineServerLogin, shineServerHttp: state.entrySettings.shineServerHttp, arweaveServer: state.entrySettings.arweaveServer, callPreflightTimeoutMs: Number(state.entrySettings.callPreflightTimeoutMs || 6000), statuses: { ...state.entrySettings.statuses }, }; const timers = new Map(); const body = document.createElement('div'); body.className = 'card stack'; SERVER_FIELDS.forEach((field) => { const block = document.createElement('div'); block.className = 'stack'; const title = document.createElement('label'); title.className = 'field-label'; title.textContent = field.label; const input = document.createElement('input'); input.className = 'input'; input.type = 'text'; input.value = draft[field.key]; const controls = document.createElement('div'); controls.className = 'row wrap-row'; const checkButton = document.createElement('button'); checkButton.className = 'ghost-btn server-check-btn'; checkButton.type = 'button'; checkButton.textContent = 'Проверить'; const status = document.createElement('span'); status.className = 'status-line'; const applyStatus = (value, exactAddress = '') => { draft.statuses[field.key] = value; checkButton.classList.remove('is-available', 'is-unavailable'); status.classList.remove('is-available', 'is-unavailable'); if (value === 'available') { status.textContent = field.key === 'shineServerLogin' && exactAddress ? `Доступен: ${exactAddress}` : 'Доступен'; checkButton.classList.add('is-available'); status.classList.add('is-available'); } else if (value === 'unavailable') { status.textContent = 'Недоступен'; checkButton.classList.add('is-unavailable'); status.classList.add('is-unavailable'); } else { status.textContent = field.key === 'shineServerLogin' && draft.shineServerHttp ? `Текущий адрес: ${draft.shineServerHttp}` : 'Статус не проверен'; } }; const runCheck = async () => { draft[field.key] = input.value.trim(); checkButton.disabled = true; checkButton.textContent = 'Проверка...'; try { if (field.key === 'shineServerLogin') { const resolved = await resolveAndCheckShineServerLogin(input.value, draft.solanaServer); draft.shineServerHttp = resolved.httpBase; draft.shineServer = resolved.wsUrl; applyStatus(resolved.status, resolved.httpBase); } else { const next = await checkServerAvailabilityByKey(field.key, input.value); applyStatus(next); } } finally { checkButton.disabled = false; checkButton.textContent = 'Проверить'; } }; applyStatus(draft.statuses[field.key]); checkButton.addEventListener('click', runCheck); input.addEventListener('input', () => { draft[field.key] = input.value; applyStatus('idle'); window.clearTimeout(timers.get(field.key)); timers.set(field.key, window.setTimeout(() => { void runCheck(); }, 3000)); }); input.addEventListener('blur', () => { void runCheck(); }); input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); void runCheck(); } }); controls.append(checkButton, status); block.append(title, input, controls); body.append(block); }); const callSettings = document.createElement('div'); callSettings.className = 'stack'; const callTimeoutLabel = document.createElement('label'); callTimeoutLabel.className = 'field-label'; callTimeoutLabel.textContent = 'Таймаут пред-подключения перед звонком (мс)'; const callTimeoutInput = document.createElement('input'); callTimeoutInput.className = 'input'; callTimeoutInput.type = 'number'; callTimeoutInput.min = '1000'; callTimeoutInput.max = '20000'; callTimeoutInput.step = '500'; callTimeoutInput.value = String(Math.max(1000, Math.min(20000, Number(draft.callPreflightTimeoutMs) || 6000))); callTimeoutInput.addEventListener('input', () => { const n = Number(callTimeoutInput.value); if (!Number.isFinite(n)) return; draft.callPreflightTimeoutMs = Math.max(1000, Math.min(20000, Math.round(n))); }); const callTimeoutHint = document.createElement('p'); callTimeoutHint.className = 'meta-muted'; callTimeoutHint.textContent = 'Перед исходящим звонком клиент проверяет и восстанавливает WS-сессию. Это время ожидания такой проверки перед ошибкой «Сервер временно недоступен».'; callSettings.append(callTimeoutLabel, callTimeoutInput, callTimeoutHint); body.append(callSettings); const actions = document.createElement('div'); actions.className = 'auth-footer-actions'; const cancelButton = document.createElement('button'); cancelButton.className = 'ghost-btn'; cancelButton.type = 'button'; cancelButton.textContent = 'Отмена'; cancelButton.addEventListener('click', () => navigate('settings-view')); const saveButton = document.createElement('button'); saveButton.className = 'primary-btn'; saveButton.type = 'button'; saveButton.textContent = 'Сохранить'; saveButton.addEventListener('click', async () => { try { await saveEntrySettings(draft); navigate('settings-view'); } catch (error) { window.alert(error?.message || 'Не удалось сохранить настройки серверов.'); } }); actions.append(cancelButton, saveButton); const help = document.createElement('button'); help.className = 'help-fab'; help.type = 'button'; help.textContent = '?'; help.addEventListener('click', () => { window.alert( 'Текст для разработчиков: для SHiNE вводится логин серверного аккаунта. Клиент читает его PDA, берёт server_address, показывает точный https-адрес и проверяет доступность WS-канала автоматически.', ); }); screen.append( renderHeader({ title: 'Настройки серверов', leftAction: { label: '←', onClick: () => navigate('settings-view') }, }), body, actions, help, ); screen.cleanup = () => { timers.forEach((timerId) => window.clearTimeout(timerId)); }; return screen; }