201 lines
7.4 KiB
JavaScript
201 lines
7.4 KiB
JavaScript
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;
|
||
}
|