mt5-manager/static/script.js
184 lines
8.7 KiB
JavaScript
/** @file script.js @version 0.5.0 */
class TerminalCard {
constructor(element) {
this.element = element;
this.folder = element.dataset.folder;
this.data = {};
this.busy = false;
this.loaded = false;
}
Yuriy Bykov
2025-10-10 10:16:59 +03:00
text(selector, value) { this.element.querySelector(selector).textContent = value; }
Yuriy Bykov
2025-10-30 22:41:27 +03:00
amount(value, signed = false) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '—';
return new Intl.NumberFormat('ru-RU', {
minimumFractionDigits: 2, maximumFractionDigits: 2,
signDisplay: signed ? 'always' : 'auto'
}).format(value);
Yuriy Bykov
2025-10-30 22:41:27 +03:00
}
updateData(data) {
this.data = data;
this.loaded = true;
this.element.classList.remove('loading-card');
const account = data.account;
const login = account?.login || data.login;
const server = account?.server || data.server;
this.text('.account', login ? 'Счёт ···' + String(login).slice(-4) : 'Счёт не задан');
this.text('.account-server', server || 'Без подключения');
this.element.querySelector('.account').title = login ? String(login) : '';
const broker = this.element.querySelector('.server');
broker.className = 'server' + (server ? ' server-' + server.replace(/[^a-zA-Z0-9_-]/g, '_') : '');
if (account) {
this.text('.balance', this.amount(account.balance));
this.text('.equity', this.amount(account.equity));
this.text('.profit', this.amount(account.profit, true));
this.text('.margin', this.amount(account.margin));
this.text('.currency', account.currency ? '· ' + account.currency : '');
this.element.querySelector('.profit').classList.toggle('neg', account.profit < 0);
const percent = this.element.querySelector('.profit-pct');
percent.classList.toggle('is-hidden', !account.balance);
percent.classList.toggle('neg', account.profit < 0);
percent.textContent = new Intl.NumberFormat('ru-RU', {
style: 'percent', maximumFractionDigits: 2, minimumFractionDigits: 2, signDisplay: 'always'
}).format(account.balance ? account.profit / account.balance : 0);
}
if (data.last_update) {
const date = new Date(data.last_update * 1000);
const today = date.toDateString() === new Date().toDateString();
this.text('.time', (today ? '' : date.toLocaleDateString('ru-RU', {day:'2-digit', month:'2-digit'}) + ' · ') +
date.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit', second:'2-digit'}));
this.element.querySelector('.update-time').title = 'Последнее обновление: ' + date.toLocaleString('ru-RU');
} else if (!data.pid) {
this.text('.time', 'Ожидает запуска');
} else {
this.text('.time', 'Ожидаем данные');
}
this.renderState();
}
renderState() {
const data = this.data;
let state = 'stopped', text = 'Остановлен', tone = 'neutral';
if (this.busy) { state = 'starting'; text = 'Выполняется операция'; tone = 'warning'; }
else if (data.pid && data.status === 'running' && data.last_error?.code === 1) {
state = 'running'; text = 'Работает'; tone = 'success';
} else if (data.pid && data.status === 'running') {
state = 'disconnected'; text = 'Нет подключения'; tone = 'danger';
} else if (data.pid) { state = 'starting'; text = 'Запускается'; tone = 'warning'; }
this.element.dataset.state = state;
const badge = this.element.querySelector('.status-badge');
badge.dataset.tone = tone;
badge.title = data.last_error?.description || '';
this.text('.description', text);
this.element.querySelector('.btn-start').disabled = this.busy || Boolean(data.pid);
this.element.querySelector('.btn-stop').disabled = this.busy || !data.pid;
}
}
const cards = Object.create(null);
let updateInterval;
let selectedFilter = 'all';
function updateTerminalList() {
const values = Object.values(cards);
const active = values.filter(card => card.data.pid).length;
const ready = values.every(card => card.loaded);
document.getElementById('count-active').textContent = ready ? active : '…';
document.getElementById('count-stopped').textContent = ready ? values.length - active : '…';
const query = document.getElementById('terminal-search').value.trim().toLocaleLowerCase();
let visible = 0;
for (const card of values) {
const searchText = [card.folder, card.element.querySelector('h2').textContent, card.data.server,
card.data.login, card.data.account?.server, card.data.account?.login].join(' ').toLocaleLowerCase();
const stateMatch = selectedFilter === 'all' || (selectedFilter === 'active' ? Boolean(card.data.pid) : !card.data.pid);
const show = stateMatch && searchText.includes(query);
card.element.hidden = !show;
if (show) visible++;
}
document.getElementById('terminal-empty').classList.toggle('is-hidden', visible > 0);
}
Yuriy Bykov
2025-10-30 10:00:52 +03:00
async function fetchTerminalInfo(folder) {
const card = cards[folder];
try {
const data = await managerRequest('/instances/' + encodeURIComponent(folder), {method:'POST'});
card.updateData(data);
updateTerminalList();
} catch (error) {
const badge = card.element.querySelector('.status-badge');
badge.dataset.tone = 'danger';
badge.title = error.message;
card.text('.description', 'Нет ответа');
}
}
async function terminalOperation(folder, action) {
const card = cards[folder];
if (card.busy) return;
card.busy = true;
card.renderState();
try {
await managerJob(await managerRequest('/' + action + '/' + encodeURIComponent(folder), {method:'POST'}));
} catch (error) {
managerMessage(error.message, 'danger');
} finally {
card.busy = false;
card.renderState();
await fetchTerminalInfo(folder);
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.terminal-card').forEach(element => {
cards[element.dataset.folder] = new TerminalCard(element);
});
document.querySelectorAll('[data-action]').forEach(button => {
button.addEventListener('click', () => terminalOperation(button.dataset.folder, button.dataset.action));
});
document.querySelectorAll('[data-filter]').forEach(button => {
button.addEventListener('click', () => {
selectedFilter = button.dataset.filter;
document.querySelectorAll('[data-filter]').forEach(item => {
item.classList.toggle('active', item === button);
item.setAttribute('aria-pressed', String(item === button));
});
updateTerminalList();
});
});
document.getElementById('terminal-search').addEventListener('input', updateTerminalList);
managerRequest('/api/settings').then(data => {
document.getElementById('new-folder').value = data.suggested_name;
}).catch(error => managerMessage(error.message, 'danger'));
document.getElementById('create-terminal').addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget;
const button = form.querySelector('[type=submit]');
if (button.disabled) return;
managerFormError(form);
const values = Object.fromEntries(new FormData(form));
button.disabled = true;
try {
const job = await managerRequest('/api/terminals', managerJson('POST', values));
document.getElementById('create-dialog').close();
const finished = await managerJob(job);
sessionStorage.setItem('manager-result', JSON.stringify(finished));
location.reload();
} catch (error) {
managerFormError(form, error.message);
managerMessage(error.message, 'danger');
} finally { button.disabled = false; }
});
const previous = sessionStorage.getItem('manager-result');
if (previous) {
sessionStorage.removeItem('manager-result');
const job = JSON.parse(previous);
managerMessage(job.folder + ': создан. ' + job.warnings.join(' '), job.warnings.length ? 'warning' : 'success');
}
terminals.forEach(fetchTerminalInfo);
updateInterval = setInterval(() => terminals.forEach(fetchTerminalInfo), 10000);
});
window.addEventListener('beforeunload', () => clearInterval(updateInterval));