mt5-manager/static/manager.js
Yuriy Bykov Yuriy Bykov 779efef59f Improve terminal settings and operation notifications
Separate configuration saving from connection testing, preserve terminal process state after tests, and support deleting stopped instances.

Simplify account fields and optional servers.dat uploads. Show operation progress per terminal and dismiss results after five seconds with a circular countdown.

Update documentation and tests for the revised behavior.
2026-11-01 19:40:01 +03:00
122 líneas
5,6 KiB
JavaScript
/** @file manager.js @version 0.3.1 */
const managerNoticeTimers = new WeakMap();
function managerIcon(name) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('icon');
svg.setAttribute('aria-hidden', 'true');
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#icon-' + name);
svg.append(use);
return svg;
}
async function managerRequest(url, options = {}) {
const response = await fetch(url, options);
let data;
try { data = await response.json(); }
catch { throw new Error('Сервер вернул неожиданный ответ. Попробуйте обновить страницу.'); }
if (!response.ok) {
throw new Error(typeof data.detail === 'string' ? data.detail : 'Проверьте введённые значения.');
}
return data;
}
function managerNotice(element, text, type = 'info', timeout = type === 'info' ? 0 : 5000) {
clearTimeout(managerNoticeTimers.get(element));
const hide = () => {
clearTimeout(managerNoticeTimers.get(element));
managerNoticeTimers.delete(element);
element.classList.add('is-hidden');
};
if (timeout > 0) managerNoticeTimers.set(element, setTimeout(hide, timeout));
if (timeout === 0 && element.dataset.message === text && element.dataset.type === type && !element.classList.contains('is-hidden')) return;
element.dataset.message = text;
element.dataset.type = type;
element.classList.remove('is-hidden', 'notice-info', 'notice-success', 'notice-warning', 'notice-danger');
element.classList.add('notice', 'notice-' + type);
const indicator = type === 'info' ? document.createElement('span') : managerIcon(type === 'success' ? 'check' : 'terminal');
if (type === 'info') { indicator.className = 'spinner'; indicator.setAttribute('aria-hidden', 'true'); }
const copy = document.createElement('span');
copy.className = 'notice-copy';
copy.textContent = text;
const close = document.createElement('button');
close.type = 'button';
close.className = 'icon-button';
close.setAttribute('aria-label', 'Скрыть сообщение');
close.append(managerIcon('close'));
if (timeout > 0) {
close.classList.add('notice-timed-close');
close.title = 'Скрыть сейчас; автоматически через ' + timeout / 1000 + ' с';
const ring = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
ring.classList.add('notice-countdown');
ring.setAttribute('viewBox', '0 0 32 32');
ring.setAttribute('aria-hidden', 'true');
ring.style.setProperty('--notice-timeout', timeout + 'ms');
for (const name of ['track', 'progress']) {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.classList.add('notice-countdown-' + name);
circle.setAttribute('cx', '16');
circle.setAttribute('cy', '16');
circle.setAttribute('r', '13');
circle.setAttribute('pathLength', '100');
ring.append(circle);
}
close.append(ring);
}
close.addEventListener('click', hide);
element.replaceChildren(indicator, copy, close);
}
function managerMessage(text, type = 'info', timeout = type === 'info' ? 0 : 5000) {
managerNotice(document.getElementById('operation-message'), text, type, timeout);
}
function managerFormError(form, text = '') {
const element = form.querySelector('.form-feedback');
if (!element) return;
element.textContent = text;
element.classList.toggle('is-hidden', !text);
}
async function managerJob(job, report = null) {
while (true) {
const warnings = (job.warnings || []).join(' ');
if (job.status === 'error') {
if (report) report(job);
throw new Error(job.error);
}
if (job.status === 'done') {
if (report) report(job);
else managerMessage(job.folder + ': готово.' + (warnings ? ' ' + warnings : ''), warnings ? 'warning' : 'success');
return job;
}
if (report) report(job);
else managerMessage(job.folder + ': ' + job.stage + (warnings ? '. ' + warnings : ''), warnings ? 'warning' : 'info', 0);
await new Promise(resolve => setTimeout(resolve, 1000));
job = await managerRequest('/api/jobs/' + encodeURIComponent(job.id));
}
}
function managerJson(method, data) {
return {method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)};
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-dialog-open]').forEach(button => {
button.addEventListener('click', () => {
const dialog = document.getElementById(button.dataset.dialogOpen);
managerFormError(dialog);
dialog.showModal();
});
});
document.querySelectorAll('[data-dialog-close]').forEach(button => {
button.addEventListener('click', () => button.closest('dialog').close());
});
document.querySelectorAll('dialog').forEach(dialog => {
dialog.addEventListener('click', event => { if (event.target === dialog) {
const rect = dialog.getBoundingClientRect();
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
} });
dialog.addEventListener('close', () => dialog.querySelectorAll('input[type=password]').forEach(input => { input.value = ''; }));
});
});