const MODE_LABELS = { auto: 'Автономный', manual: 'Ручной', assist: 'Ассистированный', standby: 'Ожидание', }; const STATUS_LABELS = { online: 'ONLINE', idle: 'IDLE', offline: 'OFFLINE', emergency: 'E-STOP', }; let currentUser = null; let ROBOTS = []; let selectedRobotId = null; let saveTimer = null; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); async function init() { try { currentUser = await API.me(); } catch { window.location.href = '/login'; return; } $('#header-user').textContent = currentUser.email; initNavigation(); initLogout(); await loadRobots(); initControls(); initProfile(); initFooterClock(); addLog('Система инициализирована', 'success'); } async function loadRobots() { const data = await API.getRobots(); ROBOTS = data.robots; selectedRobotId = data.selected_robot_id || ROBOTS[0]?.id; ROBOTS.forEach(applySettingsToRobot); initRobotList(); selectRobot(selectedRobotId, { skipSave: true }); $('#robot-count').textContent = ROBOTS.length; } function applySettingsToRobot(robot) { if (robot.emergency) { robot.speed = 0; robot.status = 'emergency'; robot.temperature = Math.round((robot.baseTemp || 30) + 5); robot.sensorAccuracy = 0; return; } if (robot.mode === 'standby') { robot.speed = 0; robot.status = 'idle'; robot.temperature = Math.round(robot.baseTemp || 30); robot.sensorAccuracy = Math.round(robot.sensitivity * 0.5); return; } const maxSpeed = robot.maxSpeed || 2; robot.speed = +(maxSpeed * robot.speedSetting / 100).toFixed(1); robot.sensorAccuracy = robot.sensitivity; robot.temperature = Math.round( (robot.baseTemp || 30) + robot.speedSetting * 0.12 + robot.sensitivity * 0.06 ); if (robot.battery < 15) { robot.status = 'offline'; robot.speed = 0; } else { robot.status = 'online'; } } function readControlsToRobot(robot) { robot.mode = $('#ctrl-mode').value; robot.speedSetting = parseInt($('#ctrl-speed').value, 10); robot.sensitivity = parseInt($('#ctrl-sensitivity').value, 10); robot.emergency = $('#ctrl-emergency').checked; robot.nightMode = $('#ctrl-night').checked; } async function persistRobots() { await API.saveRobots({ robots: ROBOTS, selected_robot_id: selectedRobotId, }); } function scheduleSave() { clearTimeout(saveTimer); saveTimer = setTimeout(async () => { try { await persistRobots(); } catch (err) { showToast(err.message); } }, 500); } async function saveCurrentRobotSettings({ silent = false } = {}) { const robot = ROBOTS.find((r) => r.id === selectedRobotId); if (!robot) return; readControlsToRobot(robot); applySettingsToRobot(robot); refreshRobotListItem(robot); syncRobotUI(robot); try { await persistRobots(); if (!silent) { addLog(`Настройки применены: ${robot.name}`, 'success'); showToast('Настройки сохранены'); } } catch (err) { showToast(err.message); } } function initNavigation() { $$('.nav__btn').forEach((btn) => { btn.addEventListener('click', async () => { const page = btn.dataset.page; const activePage = $('.page--active')?.id; if (activePage === 'page-dashboard') { await saveCurrentRobotSettings({ silent: true }); } if (activePage === 'page-profile') { await loadProfile(); } $$('.nav__btn').forEach((b) => b.classList.remove('nav__btn--active')); btn.classList.add('nav__btn--active'); $$('.page').forEach((p) => p.classList.remove('page--active')); $(`#page-${page}`).classList.add('page--active'); if (page === 'profile') await loadProfile(); if (page === 'dashboard') { const robot = ROBOTS.find((r) => r.id === selectedRobotId); if (robot) syncRobotUI(robot); } }); }); } function initLogout() { $('#btn-logout').addEventListener('click', async () => { await API.logout(); window.location.href = '/login'; }); } function initRobotList() { const list = $('#robot-list'); list.innerHTML = ROBOTS.map((r) => renderRobotListItem(r)).join(''); list.querySelectorAll('.robot-item').forEach((item) => { item.addEventListener('click', async () => { if (item.dataset.id !== selectedRobotId) { await saveCurrentRobotSettings({ silent: true }); } selectRobot(item.dataset.id, { skipSave: true }); }); }); } function renderRobotListItem(robot) { const statusClass = robot.status === 'emergency' ? 'emergency' : robot.status; return `
${String(robot.number).padStart(3, '0')}
${robot.name}
${robot.type}
${MODE_LABELS[robot.mode]} · ${robot.speed} м/с
`; } function refreshRobotListItem(robot) { const item = $(`.robot-item[data-id="${robot.id}"]`); if (!item) return; const wrapper = document.createElement('div'); wrapper.innerHTML = renderRobotListItem(robot); const newItem = wrapper.firstElementChild; item.replaceWith(newItem); newItem.addEventListener('click', async () => { if (newItem.dataset.id !== selectedRobotId) { await saveCurrentRobotSettings({ silent: true }); } selectRobot(newItem.dataset.id, { skipSave: true }); }); } function selectRobot(id, { skipSave = false } = {}) { selectedRobotId = id; $$('.robot-item').forEach((item) => { item.classList.toggle('robot-item--active', item.dataset.id === id); }); const robot = ROBOTS.find((r) => r.id === id); if (!robot) return; loadControls(robot); syncRobotUI(robot); if (!skipSave) scheduleSave(); addLog(`Выбран робот ${robot.name}`, 'info'); } function syncRobotUI(robot) { $('#selected-robot-name').textContent = robot.name; $('#robot-visual-id').textContent = `№ ${String(robot.number).padStart(3, '0')}`; $('#robot-mode-label').textContent = MODE_LABELS[robot.mode]; const badge = $('#robot-status-badge'); badge.textContent = STATUS_LABELS[robot.status] || robot.status.toUpperCase(); badge.className = 'badge badge--status'; if (['offline', 'emergency', 'idle'].includes(robot.status)) { badge.classList.add(robot.status); } const frame = $('#robot-visual-frame'); frame.className = 'robot-visual'; if (robot.nightMode) frame.classList.add('robot-visual--night'); if (robot.emergency) frame.classList.add('robot-visual--emergency'); if (robot.mode === 'standby' && !robot.emergency) frame.classList.add('robot-visual--standby'); renderStats(robot); } function renderStats(robot) { const maxSpeed = robot.maxSpeed || 2; const stats = [ { label: 'АКБ', value: `${robot.battery}%`, percent: robot.battery }, { label: 'Скорость', value: `${robot.speed} м/с`, percent: robot.speed / maxSpeed * 100 }, { label: 'Нагрузка', value: `${robot.payload} кг`, percent: robot.payload / 60 * 100 }, { label: 'Температура', value: `${robot.temperature}°C`, percent: robot.temperature / 80 * 100 }, { label: 'Датчики', value: `${robot.sensorAccuracy ?? robot.sensitivity}%`, percent: robot.sensorAccuracy ?? robot.sensitivity }, { label: 'Режим', value: MODE_LABELS[robot.mode], percent: robot.speedSetting }, ]; $('#stats-grid').innerHTML = stats.map((s) => `
${s.label}
${s.value}
`).join(''); } function loadControls(robot) { $('#ctrl-mode').value = robot.mode; $('#ctrl-speed').value = robot.speedSetting; $('#speed-value').textContent = robot.speedSetting; $('#ctrl-sensitivity').value = robot.sensitivity; $('#sensitivity-value').textContent = robot.sensitivity; $('#ctrl-emergency').checked = robot.emergency; $('#ctrl-night').checked = robot.nightMode; } function initControls() { const onChange = () => { const robot = ROBOTS.find((r) => r.id === selectedRobotId); if (!robot) return; readControlsToRobot(robot); applySettingsToRobot(robot); syncRobotUI(robot); refreshRobotListItem(robot); scheduleSave(); }; $('#ctrl-mode').addEventListener('change', onChange); $('#ctrl-emergency').addEventListener('change', onChange); $('#ctrl-night').addEventListener('change', onChange); $('#ctrl-speed').addEventListener('input', (e) => { $('#speed-value').textContent = e.target.value; onChange(); }); $('#ctrl-sensitivity').addEventListener('input', (e) => { $('#sensitivity-value').textContent = e.target.value; onChange(); }); $('#btn-apply').addEventListener('click', () => saveCurrentRobotSettings()); $('#btn-reset').addEventListener('click', async () => { await loadRobots(); showToast('Настройки сброшены'); }); } async function loadProfile() { currentUser = await API.getProfile(); $('#field-email').value = currentUser.email; $('#field-phone').value = currentUser.phone || ''; $('#field-firstname').value = currentUser.firstname || ''; $('#field-lastname').value = currentUser.lastname || ''; $('#field-patronymic').value = currentUser.patronymic || ''; $('#field-age').value = currentUser.age || ''; $('#profile-email').textContent = currentUser.email; $('#profile-rating').textContent = currentUser.rating.toFixed(1); $('#profile-points').textContent = currentUser.points; updateProfileDisplay(); const { sessions } = await API.getSessions(); $('#sessions-list').innerHTML = sessions.length ? sessions.map((s) => `
Сессия #${s.id}
Создана: ${formatDate(s.created_at)} · Истекает: ${formatDate(s.expires_at)}
IP: ${s.ip_address || '—'} · ${s.user_agent?.slice(0, 60) || '—'}
`).join('') : '

Нет активных сессий

'; } function initProfile() { $('#profile-form').addEventListener('submit', async (e) => { e.preventDefault(); try { currentUser = await API.updateProfile({ firstname: $('#field-firstname').value, lastname: $('#field-lastname').value, patronymic: $('#field-patronymic').value, age: parseInt($('#field-age').value, 10) || null, phone: $('#field-phone').value, }); updateProfileDisplay(); $('#profile-rating').textContent = currentUser.rating.toFixed(1); $('#profile-points').textContent = currentUser.points; addLog('Профиль обновлён', 'success'); showToast('Профиль сохранён'); } catch (err) { showToast(err.message); } }); $('#btn-cancel-profile').addEventListener('click', () => loadProfile()); } function updateProfileDisplay() { const parts = [currentUser.lastname, currentUser.firstname, currentUser.patronymic].filter(Boolean); $('#profile-fullname').textContent = parts.length ? parts.join(' ') : currentUser.email; } function formatDate(iso) { return new Date(iso).toLocaleString('ru-RU'); } function addLog(message, type = 'info') { const log = $('#event-log'); const time = new Date().toLocaleTimeString('ru-RU'); const entry = document.createElement('div'); entry.className = `log__entry log__entry--${type}`; entry.innerHTML = `
${time}
${message}
`; log.prepend(entry); while (log.children.length > 50) log.removeChild(log.lastChild); } function showToast(message) { const toast = $('#toast'); toast.textContent = message; toast.classList.add('toast--visible'); clearTimeout(showToast._t); showToast._t = setTimeout(() => toast.classList.remove('toast--visible'), 3000); } function initFooterClock() { const tick = () => { $('#footer-time').textContent = new Date().toLocaleString('ru-RU'); }; tick(); setInterval(tick, 1000); } document.addEventListener('DOMContentLoaded', init);