Files
rmm-openwrt/web/index.html
T

941 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#071019">
<meta name="robots" content="noindex,nofollow">
<title>Вход — OpenWrt RMM</title>
<link rel="icon" href="/favicon.svg?v=1" type="image/svg+xml">
<link rel="icon" href="/favicon-32x32.png?v=1" sizes="32x32" type="image/png">
<link rel="shortcut icon" href="/favicon.ico?v=1">
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=1">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/styles.css?v=25">
</head>
<body class="account-page">
<main id="loginView" class="landing-view">
<header class="landing-header">
<a class="landing-brand" href="/" aria-label="OpenWrt RMM — главная">
<img src="/favicon.svg?v=1" alt="" width="42" height="42">
<span><strong>OpenWrt RMM</strong><small>Remote network management</small></span>
</a>
<nav aria-label="Разделы страницы">
<a href="/#capabilities">Возможности</a>
<a href="/#security">Безопасность</a>
<a class="landing-login-link" href="#login">Войти</a>
</nav>
</header>
<section id="top" class="landing-hero">
<div class="landing-copy">
<span class="landing-kicker"><i></i> Роутеры под контролем из любой точки</span>
<h1>Единый центр управления <em>OpenWrt</em></h1>
<p>Следите за состоянием сети, клиентами и обновлениями агента. Открывайте LuCI через временный защищённый доступ без публикации панели роутера в интернет.</p>
<div class="landing-actions">
<a class="landing-primary" href="#login">Открыть кабинет</a>
<a class="landing-secondary" href="#capabilities">Как это работает</a>
</div>
<div class="landing-trust">
<span><b>●</b> Heartbeat в реальном времени</span>
<span><b>✓</b> Изоляция аккаунтов</span>
<span><b>↗</b> Временные LuCI-сессии</span>
</div>
</div>
<div class="landing-console" aria-label="Пример панели мониторинга">
<div class="console-top"><span></span><span></span><span></span><small>fleet / overview</small></div>
<div class="console-summary">
<div><small>На связи</small><strong>12</strong></div>
<div><small>Требуют внимания</small><strong class="warn">2</strong></div>
<div><small>Средний uptime</small><strong>18 дн.</strong></div>
</div>
<div class="console-router online"><i></i><span><strong>Домашняя сеть</strong><small>OpenWrt 25.12 · 8 клиентов</small></span><b>На связи</b></div>
<div class="console-router online"><i></i><span><strong>Офис</strong><small>OpenWrt 24.10 · 24 клиента</small></span><b>На связи</b></div>
<div class="console-router warning"><i></i><span><strong>Загородный дом</strong><small>Высокая загрузка процессора</small></span><b>Проверить</b></div>
</div>
</section>
<section id="capabilities" class="landing-features">
<article><span>01</span><h2>Состояние сети</h2><p>WAN, нагрузка, память, интерфейсы и активные клиенты в одном понятном обзоре.</p></article>
<article><span>02</span><h2>Безопасный LuCI</h2><p>Доступ создаётся по запросу, ограничен по времени и привязан к конкретному роутеру.</p></article>
<article><span>03</span><h2>Свои устройства</h2><p>Каждый пользователь видит только добавленные им роутеры и собственные DNS-имена.</p></article>
</section>
<section class="landing-partners" aria-labelledby="partnersTitle">
<div class="landing-partners-copy">
<span class="eyebrow">Экосистема</span>
<h2 id="partnersTitle">Наши партнёры</h2>
<p>Компании, с которыми мы развиваем инфраструктуру и сетевые сервисы.</p>
</div>
<div class="partner-logos" aria-label="Компании-партнёры">
<div class="partner-logo"><img src="/assets/partners/ks.png" alt="KS" width="512" height="512" loading="lazy"></div>
<div class="partner-logo"><img src="/assets/partners/vz.png" alt="VZ" width="512" height="512" loading="lazy"></div>
<div class="partner-logo"><img src="/assets/partners/partner-blue.png" alt="Партнёр OpenWrt RMM" width="240" height="240" loading="lazy"></div>
</div>
</section>
<section id="security" class="landing-security">
<div><span class="eyebrow">Безопасность по умолчанию</span><h2>Панель управления не должна становиться новой точкой входа</h2></div>
<ul><li>Пароли защищены Argon2id</li><li>Сессии в HttpOnly cookie</li><li>Все важные действия попадают в аудит</li></ul>
</section>
<section id="login" class="landing-login-section">
<div><span class="eyebrow">Личный кабинет</span><h2>Продолжить работу</h2><p>Используйте данные своего аккаунта. Администратор может создать отдельный доступ для каждого пользователя.</p></div>
<form id="loginForm" class="login-form">
<label>Логин<input id="loginUsername" type="text" autocomplete="username" placeholder="Введите логин" required></label>
<label>Пароль<input id="loginPassword" type="password" autocomplete="current-password" required></label>
<button class="primary" type="submit">Войти в RMM</button>
<button id="forgotPasswordBtn" class="text-button" type="button">Забыли пароль?</button>
<p id="loginError" class="login-error" role="alert"></p>
</form>
</section>
<footer class="landing-footer">
<span>OpenWrt RMM</span>
<span class="landing-footer-links"><small>Безопасное управление вашей сетью</small><a href="/legal.html">Лицензия и исходный код</a></span>
</footer>
</main>
<div id="appShell" class="app-shell is-hidden">
<aside class="sidebar">
<div class="brand">
<img class="brand-mark" src="/favicon.svg?v=1" alt="" width="42" height="42">
<div>
<h1>OpenWrt RMM</h1>
<p id="apiState">Проверка сервера</p>
</div>
</div>
<nav class="main-nav" aria-label="Основная навигация">
<button id="fleetNavBtn" class="nav-item is-active" type="button">
<span class="nav-icon">◫</span>
Объекты
</button>
<button id="problemsNavBtn" class="nav-item" type="button">
<span class="nav-icon">!</span>
Проблемы
<span id="navAlertCount" class="nav-count">0</span>
</button>
<button id="operationsNavBtn" class="nav-item" type="button">
<span class="nav-icon">⌁</span>
Операции
</button>
</nav>
<div class="sidebar-spacer"></div>
<a class="sidebar-legal-link" href="/legal.html">Лицензия · Исходный код</a>
<div class="operator-row">
<button id="profileBtn" class="operator-profile" type="button" aria-label="Открыть личный кабинет">
<span class="operator-avatar">О</span>
<span class="operator-copy">
<small>Личный кабинет</small>
<strong id="operatorName">operator</strong>
</span>
</button>
<button id="logoutBtn" class="icon-button" type="button" title="Выйти" aria-label="Выйти">↪</button>
</div>
<div class="bulk-command-grid is-hidden" aria-hidden="true">
<select id="bulkCommandType">
<option value="ping">ping</option>
<option value="traceroute">traceroute</option>
<option value="pkg_list_installed">pkg_list_installed</option>
</select>
<input id="bulkCommandTarget" type="text" placeholder="цель">
<button id="sendBulkCommandBtn" type="button">Запустить для выбранных</button>
</div>
</aside>
<main class="workspace">
<header class="topbar">
<div>
<p class="eyebrow">Управление</p>
<h2 id="pageTitle">Объекты</h2>
</div>
<div class="topbar-actions">
<span id="liveState" class="live-state is-connecting" title="Состояние автоматического обновления"><i></i><span>Подключение</span></span>
<span id="statusLine" class="status-line">Готово</span>
<button id="addRouterBtn" class="primary" type="button">Добавить роутер</button>
<button id="addUserBtn" class="is-hidden" type="button">Новый пользователь</button>
<button id="refreshBtn" class="icon-button" type="button" title="Обновить данные" aria-label="Обновить данные">↻</button>
</div>
</header>
<section id="fleetView" class="fleet-view">
<div class="fleet-summary">
<div>
<span>Всего</span>
<strong id="fleetTotalCount">0</strong>
</div>
<div class="summary-good">
<span>На связи</span>
<strong id="fleetOnlineCount">0</strong>
</div>
<div class="summary-bad">
<span>Не на связи</span>
<strong id="fleetOfflineCount">0</strong>
</div>
<div class="summary-warn">
<span>Требуют внимания</span>
<strong id="fleetAlertCount">0</strong>
</div>
</div>
<div class="fleet-toolbar">
<input id="fleetSearch" type="search" placeholder="Поиск по имени, модели, ID или тегу">
<button id="fleetFilterToggle" class="filter-toggle" type="button" aria-expanded="false">Фильтры</button>
<div class="filters">
<button class="filter is-active" data-filter="all" type="button">Все</button>
<button class="filter" data-filter="online" type="button">На связи</button>
<button class="filter" data-filter="offline" type="button">Не на связи</button>
<button class="filter" data-filter="alerts" type="button">Проблемы</button>
</div>
<div id="fleetAdvancedFilters" class="fleet-advanced-filters">
<input id="fleetGroupFilter" type="text" placeholder="Группа">
<input id="fleetTagFilter" type="text" placeholder="Тег">
</div>
</div>
<div class="fleet-table-shell">
<div class="fleet-table-head">
<span>Статус</span>
<span>Объект</span>
<span>Модель и группа</span>
<span>WAN / интернет</span>
<span>Онлайн / всего</span>
<span>Проблемы</span>
<span>Последний контакт</span>
<span></span>
</div>
<div id="deviceList" class="device-list"></div>
</div>
<section id="emptyState" class="empty-state is-hidden">
<h2>Объекты не найдены</h2>
<p>Измените фильтры или дождитесь подключения агента.</p>
</section>
</section>
<section id="deviceView" class="device-view is-hidden">
<div class="device-header">
<div>
<button id="backToFleetBtn" class="back-button" type="button">← Объекты</button>
<p class="eyebrow">Объект</p>
<h2 id="deviceName"></h2>
<p id="deviceMeta"></p>
</div>
<div class="device-header-actions">
<span id="deviceBadge" class="badge"></span>
<button id="quickDiagnosticBtn" type="button">Диагностика</button>
<button id="openLuciBtn" class="primary" type="button">Открыть LuCI</button>
</div>
</div>
<nav class="device-tabs" aria-label="Разделы объекта" role="tablist">
<button class="device-tab is-active" data-device-tab-target="overview" type="button" role="tab" aria-selected="true">Обзор</button>
<button class="device-tab" data-device-tab-target="clients" type="button" role="tab" aria-selected="false">Клиенты</button>
<button class="device-tab" data-device-tab-target="network" type="button" role="tab" aria-selected="false">Сеть</button>
<button class="device-tab" data-device-tab-target="operations" type="button" role="tab" aria-selected="false">Операции</button>
<button class="device-tab" data-device-tab-target="information" type="button" role="tab" aria-selected="false">Сведения</button>
<button class="device-tab" data-device-tab-target="expert" type="button" role="tab" aria-selected="false">Эксперт</button>
</nav>
<section class="panel overview-status-panel" data-device-tab="overview">
<div class="section-title">
<h2>Состояние объекта</h2>
<span class="summary-pill">обновляется автоматически</span>
</div>
<div id="deviceStatusHero" class="device-status-hero"></div>
<div id="healthSummary" class="health-summary"></div>
<div id="agentHealthSummary" class="agent-health-summary"></div>
</section>
<section class="panel device-information-panel" data-device-tab="information">
<div class="section-title">
<div>
<h2>Сведения об устройстве</h2>
<p class="section-subtitle">Основная информация, полученная от роутера</p>
</div>
<span id="informationStatus" class="badge">-</span>
</div>
<div class="information-grid">
<div class="information-item">
<span>Имя устройства</span>
<strong id="infoHostname">-</strong>
</div>
<div class="information-item">
<span>Модель</span>
<strong id="infoModel">-</strong>
</div>
<div class="information-item">
<span>Версия OpenWrt</span>
<strong id="infoOpenWrt">-</strong>
</div>
<div class="information-item">
<span>Платформа</span>
<strong id="infoTarget">-</strong>
</div>
<div class="information-item">
<span>Система</span>
<strong id="infoSystem">-</strong>
</div>
<div class="information-item">
<span>Версия ядра</span>
<strong id="infoKernel">-</strong>
</div>
<div class="information-item">
<span>Версия агента</span>
<strong id="infoAgentVersion">-</strong>
</div>
<div class="information-item">
<span>Зарегистрирован</span>
<strong id="infoCreatedAt">-</strong>
</div>
<div class="information-item">
<span>Последний контакт</span>
<strong id="infoLastSeenAt">-</strong>
</div>
</div>
</section>
<section class="panel" data-device-tab="information">
<div class="section-title">
<div>
<h2>Организация</h2>
<p class="section-subtitle">Группа и теги помогают находить и фильтровать объекты</p>
</div>
<button id="saveFleetBtn" class="primary" type="button">Сохранить</button>
</div>
<div class="fleet-form">
<label>
Группа
<input id="fleetGroup" type="text" placeholder="Офис, дом, филиал">
</label>
<label>
Теги через запятую
<input id="fleetTags" type="text" placeholder="VPN, тестовый, резервный">
</label>
</div>
</section>
<section class="panel" data-device-tab="information">
<div class="section-title">
<div>
<h2>Технические реквизиты</h2>
<p class="section-subtitle">Используются при диагностике и обращении в поддержку</p>
</div>
</div>
<div class="technical-details">
<div>
<span>ID устройства</span>
<code id="infoDeviceId">-</code>
<button class="copy-info-btn" data-copy-info="infoDeviceId" type="button">Копировать</button>
</div>
<div>
<span>Board name</span>
<code id="infoBoardName">-</code>
<button class="copy-info-btn" data-copy-info="infoBoardName" type="button">Копировать</button>
</div>
<div>
<span>Root filesystem</span>
<code id="infoRootfs">-</code>
</div>
</div>
</section>
<div class="metrics-grid" data-device-tab="overview">
<div class="metric">
<span>Последний контакт</span>
<strong id="lastSeen">-</strong>
</div>
<div class="metric">
<span>Нагрузка</span>
<strong id="loadAvg">-</strong>
</div>
<div class="metric">
<span>Время работы</span>
<strong id="uptime">-</strong>
</div>
<div class="metric secondary-metric">
<span>Маршрут по умолчанию</span>
<strong id="defaultRoute">-</strong>
</div>
<div class="metric">
<span>WAN-адрес</span>
<strong id="wanIp">-</strong>
</div>
<div class="metric">
<span>Память</span>
<strong id="memoryUsage">-</strong>
</div>
<div class="metric secondary-metric">
<span>Хранилище</span>
<strong id="diskUsage">-</strong>
</div>
<div class="metric">
<span>Клиенты</span>
<strong id="clientCounts">-</strong>
</div>
<div class="metric secondary-metric">
<span>Проверки связи</span>
<strong id="connectivityStatus">-</strong>
</div>
</div>
<section class="expert-notice" data-device-tab="expert">
<div>
<strong>Экспертный режим</strong>
<span>Здесь находятся низкоуровневые команды и настройки OpenWrt. Ошибочное изменение может нарушить связь с роутером.</span>
</div>
</section>
<div class="two-column" data-device-tab="expert">
<section class="panel">
<div class="section-title">
<h2>Inventory</h2>
</div>
<pre id="inventoryJson" class="code-block"></pre>
</section>
<section class="panel">
<div class="section-title">
<h2>Команда вручную</h2>
</div>
<div class="command-form">
<select id="commandType">
<option value="ping">ping</option>
<option value="traceroute">traceroute</option>
<option value="route_show">show routes</option>
<option value="interfaces_show">show interfaces</option>
<option value="pkg_list_installed">pkg_list_installed</option>
<option value="reboot">reboot</option>
</select>
<input id="commandTarget" type="text" placeholder="1.1.1.1">
<button id="sendCommandBtn" type="button">Запустить</button>
</div>
</section>
</div>
<section class="panel" data-device-tab="operations">
<div class="section-title">
<div>
<h2>Диагностика</h2>
<p class="section-subtitle">Проверки выполняются агентом без подключения по SSH</p>
</div>
<button id="runFullDiagnosticBtn" class="primary" type="button">Запустить полную диагностику</button>
</div>
<div id="diagnosticStatus" class="operation-status">
<span class="operation-icon">✓</span>
<div>
<strong>Готово к проверке</strong>
<small>Будут проверены сервер, интернет, маршруты и интерфейсы</small>
</div>
</div>
<h3 class="operation-subheading">Выборочная проверка</h3>
<div class="diagnostic-grid">
<button class="diagnostic-btn" data-diagnostic="ping_server" type="button">Проверить связь с сервером</button>
<button class="diagnostic-btn" data-diagnostic="ping_internet" type="button">Проверить интернет</button>
<button class="diagnostic-btn" data-diagnostic="traceroute_internet" type="button">Маршрут до интернета</button>
<button class="diagnostic-btn" data-diagnostic="show_routes" type="button">Показать маршруты</button>
<button class="diagnostic-btn" data-diagnostic="show_interfaces" type="button">Показать интерфейсы</button>
</div>
</section>
<section class="panel" data-device-tab="clients">
<div class="section-title">
<div>
<h2>Клиенты сети</h2>
<p class="section-subtitle">Онлайн подтверждается Wi-Fi или таблицей соседей; DHCP-аренда сама по себе не означает подключение</p>
</div>
<span id="clientSummary" class="summary-pill">0 клиентов</span>
</div>
<div class="client-toolbar">
<input id="clientSearch" type="search" placeholder="Поиск по имени, IP или MAC">
<div class="filters client-filters">
<button class="client-filter is-active" data-client-filter="all" type="button">Все</button>
<button class="client-filter" data-client-filter="online" type="button">Сейчас в сети</button>
<button class="client-filter" data-client-filter="wifi" type="button">Wi-Fi</button>
<button class="client-filter" data-client-filter="wired" type="button">Проводные</button>
<button class="client-filter" data-client-filter="unconfirmed" type="button">Не подтверждены</button>
</div>
</div>
<div class="client-table-head">
<span>Устройство</span>
<span>IP-адрес</span>
<span>MAC-адрес</span>
<span>Подключение</span>
<span>Сигнал / скорость</span>
<span>Состояние</span>
</div>
<div id="clientList" class="client-list"></div>
</section>
<section class="panel" data-device-tab="network">
<div class="section-title">
<div>
<h2>Сеть и интерфейсы</h2>
<p class="section-subtitle">Адреса и трафик интерфейсов роутера</p>
</div>
<span id="networkSummary" class="summary-pill">0 интерфейсов</span>
</div>
<div id="networkHealth" class="network-health"></div>
<div class="network-table-head">
<span>Интерфейс</span>
<span>Адреса</span>
<span>Получено</span>
<span>Передано</span>
<span>Ошибки</span>
</div>
<div id="interfaceCounters" class="network-list"></div>
</section>
<section class="panel" data-device-tab="overview">
<div class="section-title">
<h2>Проблемы</h2>
<div class="section-actions">
<select id="alertStatusFilter" aria-label="Фильтр алертов">
<option value="open">Открытые</option>
<option value="all">Все</option>
<option value="resolved">Resolved</option>
</select>
<span id="alertSummary" class="summary-pill">0 active</span>
</div>
</div>
<div id="alertList" class="table-list"></div>
</section>
<section class="panel" data-device-tab="overview">
<div class="section-title">
<div>
<h2>История мониторинга</h2>
<p class="section-subtitle">Последние показатели с временной шкалой</p>
</div>
<span id="metricsHistorySummary" class="summary-pill">12 замеров</span>
</div>
<div id="metricsHistory" class="metrics-chart-grid"></div>
</section>
<section class="panel" data-device-tab="expert">
<div class="section-title">
<div>
<h2>Передача роутера</h2>
<p class="section-subtitle">Новый владелец получит управление устройством. Текущие LuCI-сессии будут закрыты.</p>
</div>
</div>
<form id="deviceTransferForm" class="device-transfer-form">
<label>Логин нового владельца<input id="transferUsername" type="text" minlength="3" maxlength="64" autocomplete="off" required></label>
<label>Ваш текущий пароль<input id="transferPassword" type="password" autocomplete="current-password" required></label>
<button class="danger" type="submit">Передать роутер</button>
<p id="transferMessage" class="form-message" role="status"></p>
</form>
</section>
<section class="panel" data-device-tab="expert">
<div class="section-title">
<h2>UCI configuration</h2>
</div>
<div class="uci-grid">
<label>
Package
<select id="uciConfig">
<option value="network">network</option>
<option value="wireless">wireless</option>
<option value="dhcp">dhcp</option>
<option value="firewall">firewall</option>
<option value="system">system</option>
</select>
</label>
<label>
Section
<input id="uciSection" type="text" placeholder="lan or @wifi-iface[0]">
</label>
<label>
Option
<input id="uciOption" type="text" placeholder="ipaddr">
</label>
<label>
Value
<input id="uciValue" type="text" placeholder="192.168.1.1">
</label>
<label class="check-row">
<input id="uciCommitNow" type="checkbox">
Commit immediately
</label>
</div>
<div class="uci-actions">
<button id="uciBackupBtn" type="button">Backup</button>
<button id="uciPreviewBtn" type="button">Preview</button>
<button id="uciShowBtn" type="button">Show</button>
<button id="uciSetBtn" type="button">Stage set</button>
<button id="uciCommitBtn" type="button">Commit</button>
<button id="uciCommitConfirmedBtn" type="button">Commit + check</button>
<button id="uciRevertBtn" type="button">Revert</button>
<button id="uciRestoreBtn" type="button">Restore backup</button>
</div>
</section>
<section class="panel" data-device-tab="expert">
<div class="section-title">
<h2>Packages</h2>
</div>
<div class="package-grid">
<select id="packageCommand">
<option value="pkg_list_installed">List installed</option>
<option value="pkg_update">Update package index</option>
<option value="pkg_list_upgradable">List upgradable</option>
<option value="pkg_install">Install package</option>
<option value="pkg_remove">Remove package</option>
</select>
<input id="packageName" type="text" placeholder="package name">
<button id="sendPackageCommandBtn" type="button">Queue</button>
</div>
</section>
<section id="remoteAccessPanel" class="panel" data-device-tab="operations">
<div class="section-title">
<div>
<h2>Удаленный доступ</h2>
<p class="section-subtitle">Временный доступ к LuCI и SSH автоматически закроется по таймеру</p>
</div>
<div class="section-actions">
<span id="remoteSummary" class="summary-pill">Нет активных сессий</span>
</div>
</div>
<div id="cloudAccessCard" class="cloud-access-card" data-state="closed">
<div class="cloud-access-icon" aria-hidden="true">☁</div>
<div class="cloud-access-copy">
<strong>Доступ через облако RMM</strong>
<span id="cloudAccessStatus">Соединение создаётся только по вашему запросу</span>
</div>
<button id="openCloudAccessBtn" class="primary" type="button">Открыть LuCI</button>
</div>
<details class="advanced-settings">
<summary>Экспертный режим: SSH и параметры туннеля</summary>
<div class="remote-quick-form">
<label>
Длительность доступа
<select id="remoteDuration">
<option value="900">15 минут</option>
<option value="1800">30 минут</option>
<option value="3600">1 час</option>
<option value="7200">2 часа</option>
</select>
</label>
<button id="createRemoteSessionBtn" type="button">Создать ручную сессию</button>
</div>
<div class="remote-form">
<label>
Сервер туннеля
<input id="remoteServerHost" type="text" placeholder="10.10.10.2">
</label>
<label>
SSH-порт сервера
<input id="remoteServerPort" type="number" min="1" max="65535" value="2222">
</label>
<label>
Внешний порт
<input id="remotePort" type="number" min="1" max="65535" placeholder="автоматически">
</label>
<label>
SSH-порт роутера
<input id="remoteLocalPort" type="number" min="1" max="65535" value="22">
</label>
<label>
Протокол LuCI
<select id="remoteLuCIScheme">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
</label>
</div>
</details>
<div id="remoteSessionList" class="table-list remote-session-list"></div>
</section>
<section class="panel" data-device-tab="operations">
<div class="section-title">
<div>
<h2>Быстрые настройки</h2>
<p class="section-subtitle">Сначала проверьте изменения, затем примените их</p>
</div>
</div>
<div class="preset-grid">
<div class="preset">
<h3>LAN-адрес</h3>
<p>Изменяет адрес локальной сети роутера</p>
<input id="presetLanIp" type="text" placeholder="10.10.10.1/24">
<button class="preset-review-btn" data-preset="lan_ip" type="button">Проверить изменение</button>
</div>
<div class="preset">
<h3>Имя устройства</h3>
<p>Изменяет hostname роутера</p>
<input id="presetHostname" type="text" placeholder="OpenWrt">
<button class="preset-review-btn" data-preset="hostname" type="button">Проверить изменение</button>
</div>
<div class="preset">
<h3>Название Wi-Fi</h3>
<p>Изменяет SSID основной беспроводной сети</p>
<input id="presetWifiSsid" type="text" placeholder="SSID">
<button class="preset-review-btn" data-preset="wifi_ssid" type="button">Проверить изменение</button>
</div>
<div class="preset">
<h3>Пароль Wi-Fi</h3>
<p>Изменяет пароль основной беспроводной сети</p>
<input id="presetWifiKey" type="password" placeholder="Новый пароль">
<button class="preset-review-btn" data-preset="wifi_key" type="button">Проверить изменение</button>
</div>
<div class="preset">
<h3>DHCP-сервер</h3>
<p>Включает или отключает выдачу адресов в LAN</p>
<select id="presetDhcpLan">
<option value="0">Включен</option>
<option value="1">Отключен</option>
</select>
<button class="preset-review-btn" data-preset="dhcp_lan" type="button">Проверить изменение</button>
</div>
</div>
<section id="presetReviewPanel" class="preset-review-panel is-hidden">
<div class="preset-review-header">
<div>
<span>Подготовленное изменение</span>
<strong id="presetReviewTitle">-</strong>
</div>
<span id="presetReviewStatus" class="summary-pill">Проверка</span>
</div>
<div id="presetReviewChange" class="preset-review-change"></div>
<pre id="presetReviewOutput" class="preset-review-output">Ожидание результата проверки...</pre>
<div class="preset-review-actions">
<button id="cancelPresetReviewBtn" type="button">Отменить</button>
<button id="applyPresetReviewBtn" class="primary" type="button" disabled>Применить безопасно</button>
</div>
<p class="preset-safety-note">После применения роутер проверит связь с сервером. Если связь пропадет, конфигурация будет автоматически восстановлена.</p>
</section>
</section>
<section id="commandsPanel" class="panel" data-device-tab="expert">
<div class="section-title">
<h2>История команд</h2>
<div class="section-actions">
<span id="commandSummary" class="summary-pill">0 total</span>
</div>
</div>
<div class="filters command-filters">
<button class="command-filter is-active" data-command-filter="all" type="button">Все</button>
<button class="command-filter" data-command-filter="running" type="button">Выполняются</button>
<button class="command-filter" data-command-filter="failed" type="button">Ошибки</button>
<button class="command-filter" data-command-filter="completed" type="button">Готовые</button>
<select id="commandStatusFilter" aria-label="Точный статус команды">
<option value="">Любой статус</option>
<option value="queued">В очереди</option>
<option value="claimed">Выполняется</option>
<option value="completed">Готово</option>
<option value="failed">Ошибка</option>
<option value="expired">Истекла</option>
<option value="cancelled">Отменена</option>
</select>
</div>
<div id="commandList" class="table-list"></div>
<button id="loadMoreCommandsBtn" class="load-more-btn is-hidden" type="button">Загрузить ещё</button>
</section>
<section id="commandDetailPanel" class="panel is-hidden" data-device-tab="expert">
<div class="section-title">
<h2>Детали команды</h2>
<button id="copyCommandOutputBtn" type="button">Копировать вывод</button>
</div>
<div id="commandDetailMeta" class="detail-meta"></div>
<pre id="commandDetailOutput" class="command-detail-output"></pre>
</section>
<section class="panel" data-device-tab="expert">
<div class="section-title">
<h2>Audit</h2>
<div class="section-actions">
<span id="auditSummary" class="summary-pill">0 events</span>
</div>
</div>
<div id="auditList" class="table-list"></div>
<button id="loadMoreAuditBtn" class="load-more-btn is-hidden" type="button">Загрузить ещё</button>
</section>
<section class="panel" data-device-tab="expert">
<div class="section-title">
<div>
<h2>Обслуживание данных</h2>
<p class="section-subtitle">Очистка локальной истории RMM. Настройки роутера не меняются.</p>
</div>
</div>
<div class="maintenance-grid">
<button id="clearAlertsBtn" type="button">Очистить алерты</button>
<button id="clearCommandsBtn" type="button">Очистить историю команд</button>
<button id="clearAuditBtn" type="button">Очистить аудит устройства</button>
<button id="deleteDeviceBtn" class="danger" type="button">Удалить устройство</button>
</div>
</section>
</section>
</main>
<nav class="mobile-nav" aria-label="Мобильная навигация">
<button class="mobile-nav-item is-active" data-mobile-route="fleet" type="button"><span>◫</span>Объекты</button>
<button class="mobile-nav-item" data-mobile-route="problems" type="button"><span>!</span>Проблемы</button>
<button class="mobile-nav-item" data-mobile-route="operations" type="button"><span>⌁</span>Операции</button>
<button class="mobile-nav-item" data-mobile-route="profile" type="button"><span>○</span>Профиль</button>
</nav>
</div>
<dialog id="enrollmentGrantDialog">
<form method="dialog" class="login-form">
<h2>Добавление роутера</h2>
<p>Grant действует 15 минут и используется только один раз. Вставьте его в LuCI → Службы → RMM agent.</p>
<label>
Одноразовый grant
<input id="enrollmentTokenOutput" type="text" readonly>
</label>
<div class="topbar-actions">
<button id="copyEnrollmentTokenBtn" type="button">Копировать</button>
<button class="primary" type="submit">Готово</button>
</div>
</form>
</dialog>
<dialog id="createUserDialog">
<form id="createUserForm" method="dialog" class="login-form">
<h2>Новый пользователь</h2>
<label>
Логин
<input id="newUsername" type="text" minlength="3" maxlength="64" autocomplete="off" required>
</label>
<label>
E-mail для восстановления
<input id="newUserEmail" type="email" maxlength="254" autocomplete="off" placeholder="name@example.com">
</label>
<label>
Временный пароль
<input id="newUserPassword" type="password" minlength="12" maxlength="1024" autocomplete="new-password" required>
</label>
<label>
Роль
<select id="newUserRole"><option value="user">Пользователь</option><option value="admin">Администратор</option></select>
</label>
<div class="topbar-actions">
<button id="cancelCreateUserBtn" value="cancel" type="button">Отмена</button>
<button class="primary" type="submit">Создать</button>
</div>
</form>
</dialog>
<dialog id="profileDialog" class="profile-dialog">
<div class="profile-sheet">
<div class="dialog-heading">
<div>
<span class="eyebrow">Аккаунт пользователя</span>
<h2>Личный кабинет</h2>
</div>
<button id="closeProfileBtn" class="icon-button" type="button" aria-label="Закрыть">×</button>
</div>
<div class="profile-identity">
<span id="profileAvatar" class="profile-avatar">О</span>
<div>
<strong id="profileUsername">operator</strong>
<span id="profileRole">Пользователь</span>
</div>
</div>
<form id="profileForm" class="profile-form">
<div class="profile-section-heading"><strong>Контактные данные</strong><span>Используются только в вашем аккаунте</span></div>
<label>Отображаемое имя<input id="profileDisplayName" type="text" maxlength="80" autocomplete="name" placeholder="Как к вам обращаться"></label>
<label>E-mail<input id="profileEmail" type="email" maxlength="254" autocomplete="email" placeholder="name@example.com"></label>
<button class="primary" type="submit">Сохранить профиль</button>
<p id="profileMessage" class="form-message" role="status"></p>
</form>
<form id="passwordForm" class="profile-form profile-security-form">
<div class="profile-section-heading"><strong>Смена пароля</strong><span>Остальные активные сессии будут завершены</span></div>
<label>Текущий пароль<input id="currentPassword" type="password" autocomplete="current-password" required></label>
<label>Новый пароль<input id="newPassword" type="password" minlength="12" maxlength="1024" autocomplete="new-password" required></label>
<label>Повторите новый пароль<input id="confirmPassword" type="password" minlength="12" maxlength="1024" autocomplete="new-password" required></label>
<button type="submit">Изменить пароль</button>
<p id="passwordMessage" class="form-message" role="status"></p>
</form>
<form id="notificationSettingsForm" class="profile-form notification-settings-form">
<div class="profile-section-heading"><strong>Уведомления и пороги</strong><span>Настройки применяются к вашим роутерам</span></div>
<div class="notification-channel-grid">
<label class="notification-toggle"><input id="notificationEmailEnabled" type="checkbox"><span><strong>E-mail</strong><small id="notificationEmailHint">Используется e-mail из профиля</small></span></label>
<label class="notification-toggle"><input id="notificationTelegramEnabled" type="checkbox"><span><strong>Telegram</strong><small id="notificationTelegramHint">Нужен Chat ID</small></span></label>
</div>
<label id="notificationTelegramChatRow">Telegram Chat ID<input id="notificationTelegramChatId" type="text" maxlength="32" inputmode="numeric" placeholder="Например, 123456789"></label>
<div class="notification-toggle-grid">
<label class="notification-toggle compact"><input id="notificationWarningEnabled" type="checkbox"><span>Предупреждения</span></label>
<label class="notification-toggle compact"><input id="notificationCriticalEnabled" type="checkbox"><span>Критические</span></label>
<label class="notification-toggle compact"><input id="notificationResolvedEnabled" type="checkbox"><span>Восстановление</span></label>
</div>
<div class="notification-threshold-grid">
<label>Память, %<input id="notificationMemoryThreshold" type="number" min="50" max="99" required></label>
<label>Диск, %<input id="notificationDiskThreshold" type="number" min="50" max="99" required></label>
<label>Потери, %<input id="notificationPacketLossThreshold" type="number" min="1" max="100" required></label>
<label>Задержка, мс<input id="notificationLatencyThreshold" type="number" min="10" max="5000" required></label>
</div>
<label>Повторять активную проблему<select id="notificationRepeatMinutes"><option value="0">Не повторять</option><option value="15">Каждые 15 минут</option><option value="60">Каждый час</option><option value="360">Каждые 6 часов</option><option value="1440">Раз в сутки</option></select></label>
<div class="notification-form-actions"><button class="primary" type="submit">Сохранить уведомления</button><button id="testNotificationsBtn" type="button">Отправить тест</button></div>
<p id="notificationSettingsMessage" class="form-message" role="status"></p>
<div class="notification-history-heading"><strong>Последние отправки</strong><button id="refreshNotificationsBtn" class="text-button" type="button">Обновить</button></div>
<div id="notificationHistory" class="notification-history"></div>
</form>
<section id="userManagementSection" class="profile-form user-management is-hidden">
<div class="profile-section-heading"><strong>Пользователи</strong><span>Роли и доступы всех аккаунтов</span></div>
<div id="userList" class="user-list"></div>
</section>
<div class="profile-session-actions">
<button id="logoutAllBtn" class="danger" type="button">Завершить все сессии</button>
<button id="profileLogoutBtn" type="button">Выйти на этом устройстве</button>
</div>
</div>
</dialog>
<dialog id="forgotPasswordDialog">
<form id="forgotPasswordForm" class="login-form">
<div class="dialog-heading"><div><span class="eyebrow">Восстановление доступа</span><h2>Сбросить пароль</h2></div><button id="closeForgotPasswordBtn" class="icon-button" type="button" aria-label="Закрыть">×</button></div>
<p>Введите логин или e-mail. Если для аккаунта настроено восстановление, мы отправим одноразовую ссылку на 30 минут.</p>
<label>Логин или e-mail<input id="passwordResetIdentifier" type="text" maxlength="254" autocomplete="username" required></label>
<button class="primary" type="submit">Отправить ссылку</button>
<p id="forgotPasswordMessage" class="form-message" role="status"></p>
</form>
</dialog>
<dialog id="passwordResetDialog">
<form id="passwordResetForm" class="login-form">
<div class="dialog-heading"><div><span class="eyebrow">Новый пароль</span><h2>Восстановление доступа</h2></div><button id="closePasswordResetBtn" class="icon-button" type="button" aria-label="Закрыть">×</button></div>
<label>Новый пароль<input id="resetNewPassword" type="password" minlength="12" maxlength="1024" autocomplete="new-password" required></label>
<label>Повторите пароль<input id="resetConfirmPassword" type="password" minlength="12" maxlength="1024" autocomplete="new-password" required></label>
<button class="primary" type="submit">Установить новый пароль</button>
<p id="passwordResetMessage" class="form-message" role="status"></p>
</form>
</dialog>
<dialog id="luciStateDialog" class="luci-state-dialog">
<div class="luci-state-sheet">
<div class="luci-state-route" aria-hidden="true">
<span class="luci-cloud">☁</span>
<i></i>
<span class="luci-warning">!</span>
<i></i>
<span class="luci-router">R</span>
</div>
<span id="luciStateCode" class="luci-state-code">409 · SESSION REQUIRED</span>
<h2 id="luciStateTitle">Удалённый доступ не запущен</h2>
<p id="luciStateDescription">Создайте временную сессию, чтобы безопасно открыть LuCI этого роутера.</p>
<div id="luciStateContext" class="luci-state-context"></div>
<div class="luci-state-actions">
<button id="luciStatePrimaryBtn" class="primary" type="button">Настроить доступ</button>
<button id="luciStateDiagnosticBtn" type="button">Открыть диагностику</button>
<button id="closeLuciStateBtn" class="text-button" type="button">Вернуться к роутеру</button>
</div>
<small id="luciStateRequestId" class="luci-request-id"></small>
</div>
</dialog>
<script src="/landing-motion.js?v=1" defer></script>
<script src="/app.js?v=28" defer></script>
</body>
</html>