diff --git a/UI_CHECKLIST.md b/UI_CHECKLIST.md index a06929b..f0166cf 100644 --- a/UI_CHECKLIST.md +++ b/UI_CHECKLIST.md @@ -96,15 +96,15 @@ ## Этап 4. Операции -- [ ] Объединить отдельные диагностические кнопки в один сценарий. -- [ ] Показывать прогресс диагностики. -- [ ] Показывать итог диагностики понятным языком. +- [x] Объединить отдельные диагностические кнопки в один сценарий. +- [~] Показывать прогресс диагностики. +- [~] Показывать итог диагностики понятным языком. - [ ] Добавить рекомендации по найденным проблемам. -- [ ] Упростить создание удаленной сессии до выбора длительности. -- [ ] Спрятать host, порты и параметры туннеля в дополнительные настройки. -- [ ] Добавить действия «Открыть LuCI» и «Скопировать SSH». +- [x] Упростить создание удаленной сессии до выбора длительности. +- [x] Спрятать host, порты и параметры туннеля в дополнительные настройки. +- [x] Добавить действия «Открыть LuCI» и «Скопировать SSH». - [ ] Показывать оставшееся время удаленной сессии. -- [ ] Добавить понятное закрытие удаленной сессии. +- [x] Добавить понятное закрытие удаленной сессии. - [ ] Объединить Preview и Stage в сценарий «Проверить → Применить». - [ ] Показывать изменения перед применением. - [ ] Понятно объяснять commit confirmed и автоматический откат. diff --git a/web/app.js b/web/app.js index 4d15dff..d72973a 100644 --- a/web/app.js +++ b/web/app.js @@ -38,6 +38,8 @@ const els = { quickDiagnosticBtn: document.querySelector("#quickDiagnosticBtn"), openLuciBtn: document.querySelector("#openLuciBtn"), remoteAccessPanel: document.querySelector("#remoteAccessPanel"), + runFullDiagnosticBtn: document.querySelector("#runFullDiagnosticBtn"), + diagnosticStatus: document.querySelector("#diagnosticStatus"), deviceName: document.querySelector("#deviceName"), deviceMeta: document.querySelector("#deviceMeta"), deviceBadge: document.querySelector("#deviceBadge"), @@ -226,6 +228,17 @@ function statusLabel(status) { }[status] || status || "-"; } +function remoteStatusLabel(status) { + return { + requested: "Запрашивается", + queued: "Открывается", + active: "Активен", + closed: "Закрыт", + failed: "Ошибка", + expired: "Завершен", + }[status] || statusLabel(status); +} + function alertTypeLabel(type) { return { offline: "Роутер не выходит на связь", @@ -818,9 +831,9 @@ async function loadRemoteSessions() { function renderRemoteSessions(sessions) { els.remoteSessionList.innerHTML = ""; const active = sessions.filter((session) => ["requested", "queued", "active"].includes(session.status)).length; - els.remoteSummary.textContent = `${active} open / ${sessions.length} total`; + els.remoteSummary.textContent = active ? `${active} активн.` : "Нет активных сессий"; if (sessions.length === 0) { - els.remoteSessionList.textContent = "No remote sessions"; + els.remoteSessionList.innerHTML = '
Удаленный доступ еще не открывался
'; return; } for (const session of sessions) { @@ -829,25 +842,29 @@ function renderRemoteSessions(sessions) { const connectCommand = session.remote_port ? `ssh -p ${session.remote_port} root@${session.server_host || "server"}` : "-"; const canOpenLuCI = session.status === "active" && session.luci_port; const row = document.createElement("div"); - row.className = "row remote-session-row"; + row.className = "remote-session-row"; row.innerHTML = ` -
- ${escapeHtml((session.target || "ssh").toUpperCase())} ${escapeHtml(statusLabel(session.status))}
- ${escapeHtml(session.id)} +
+ ${escapeHtml(remoteStatusLabel(session.status))} +
+ Доступ к роутеру + ${escapeHtml(endpoint)} · до ${escapeHtml(formatShortDate(session.expires_at))} +
- ${escapeHtml(endpoint)} - expires ${escapeHtml(formatShortDate(session.expires_at))} ${escapeHtml(connectCommand)}
- - - + + +
`; row.querySelector('[data-action="luci"]').addEventListener("click", () => { window.open(`/luci/${encodeURIComponent(session.device_id)}/${encodeURIComponent(session.id)}/`, "_blank", "noopener"); }); - row.querySelector('[data-action="commands"]').addEventListener("click", scrollToCommands); + row.querySelector('[data-action="copy"]').addEventListener("click", async () => { + await navigator.clipboard.writeText(connectCommand); + setStatus("SSH-команда скопирована"); + }); row.querySelector('[data-action="close"]').addEventListener("click", () => closeRemoteSession(session.id)); els.remoteSessionList.appendChild(row); } @@ -889,7 +906,9 @@ async function createDeviceCommand(type, args, options = {}) { method: "POST", body: JSON.stringify({ type, args }), }); - await Promise.all([loadCommands(), loadAudit(), loadAlerts()]); + if (!options.skipRefresh) { + await Promise.all([loadCommands(), loadAudit(), loadAlerts()]); + } } async function sendBulkCommand() { @@ -967,8 +986,8 @@ async function createRemoteSession() { setStatus("Tunnel server is required"); return; } - if (!window.confirm("Open temporary SSH access to this router?")) return; - setStatus("Opening remote SSH access"); + if (!window.confirm(`Открыть временный доступ к роутеру на ${Math.round(durationSeconds / 60)} минут?`)) return; + setStatus("Открытие удаленного доступа"); await api(`/api/devices/${encodeURIComponent(state.selectedDeviceId)}/remote-sessions`, { method: "POST", body: JSON.stringify({ @@ -982,17 +1001,18 @@ async function createRemoteSession() { }), }); await Promise.all([loadRemoteSessions(), loadCommands(), loadAudit()]); - setStatus("Remote SSH command queued"); + setStatus("Команда открытия доступа отправлена"); } async function closeRemoteSession(sessionId) { if (!state.selectedDeviceId) return; - setStatus("Closing remote session"); + if (!window.confirm("Закрыть удаленный доступ к роутеру?")) return; + setStatus("Закрытие удаленного доступа"); await api(`/api/devices/${encodeURIComponent(state.selectedDeviceId)}/remote-sessions/${encodeURIComponent(sessionId)}/close`, { method: "POST", }); await Promise.all([loadRemoteSessions(), loadAudit()]); - setStatus("Remote session closed"); + setStatus("Удаленный доступ закрыт"); } function presetCommand(preset) { @@ -1097,12 +1117,40 @@ function diagnosticCommand(name) { async function sendDiagnostic(name) { const command = diagnosticCommand(name); if (!command) return; - setStatus(`Queueing ${command.type}`); + setStatus("Запуск проверки"); await createDeviceCommand(command.type, command.args); - setStatus("Diagnostic queued"); + setStatus("Проверка поставлена в очередь"); scrollToCommands(); } +async function runFullDiagnostic() { + const checks = ["ping_server", "ping_internet", "show_routes", "show_interfaces"]; + els.runFullDiagnosticBtn.disabled = true; + els.diagnosticStatus.classList.add("is-running"); + els.diagnosticStatus.innerHTML = ` + +
Диагностика запускаетсяОтправляем проверки на роутер
+ `; + try { + for (const name of checks) { + const command = diagnosticCommand(name); + await createDeviceCommand(command.type, command.args, { skipRefresh: true }); + } + await Promise.all([loadCommands(), loadAudit(), loadAlerts()]); + els.diagnosticStatus.classList.remove("is-running"); + els.diagnosticStatus.classList.add("is-complete"); + els.diagnosticStatus.innerHTML = ` + +
Диагностика запущенаРезультаты появятся в истории команд
+ + `; + els.diagnosticStatus.querySelector("#openDiagnosticResultsBtn").addEventListener("click", scrollToCommands); + setStatus("Полная диагностика поставлена в очередь"); + } finally { + els.runFullDiagnosticBtn.disabled = false; + } +} + async function runAlertDiagnostics(alert) { const target = alert.details && alert.details.target ? alert.details.target : "1.1.1.1"; if (alert.type === "latency_high" || alert.type === "packet_loss_high" || alert.type === "wan_down") { @@ -1150,6 +1198,7 @@ els.refreshBtn.addEventListener("click", () => loadDevices().catch((error) => se els.backToFleetBtn.addEventListener("click", showFleet); els.quickDiagnosticBtn.addEventListener("click", () => selectDeviceTab("operations")); els.openLuciBtn.addEventListener("click", openLuciOrRemoteAccess); +els.runFullDiagnosticBtn.addEventListener("click", () => runFullDiagnostic().catch((error) => setStatus(error.message))); els.reloadCommandsBtn.addEventListener("click", () => loadCommands().catch((error) => setStatus(error.message))); els.reloadAuditBtn.addEventListener("click", () => loadAudit().catch((error) => setStatus(error.message))); els.reloadAlertsBtn.addEventListener("click", () => loadAlerts().catch((error) => setStatus(error.message))); diff --git a/web/index.html b/web/index.html index 505c5d1..6f9765a 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ OpenWrt RMM - +
@@ -258,9 +258,20 @@
-

Быстрая диагностика

- без SSH +
+

Диагностика

+

Проверки выполняются агентом без подключения по SSH

+
+
+
+ +
+ Готово к проверке + Будут проверены сервер, интернет, маршруты и интерфейсы +
+
+

Выборочная проверка

@@ -397,38 +408,18 @@
-

Remote access

+
+

Удаленный доступ

+

Временный доступ к LuCI и SSH автоматически закроется по таймеру

+
- 0 sessions - + Нет активных сессий +
-
+
- - - - - - -
-
- Active sessions provide SSH and an authenticated Open LuCI button. Choose the protocol used by this router. +
+
+ Дополнительные настройки подключения +
+ + + + + +
+
-

UCI presets

+
+

Быстрые настройки

+

Сначала проверьте изменения, затем примените их

+
@@ -533,6 +552,6 @@
- + diff --git a/web/styles.css b/web/styles.css index b8262e1..06eea36 100644 --- a/web/styles.css +++ b/web/styles.css @@ -473,7 +473,7 @@ select { .remote-form { display: grid; - grid-template-columns: minmax(180px, 1.2fr) repeat(5, minmax(110px, 0.6fr)) auto; + grid-template-columns: minmax(180px, 1.2fr) repeat(4, minmax(110px, 0.6fr)); gap: 10px; align-items: end; } @@ -485,12 +485,36 @@ select { font-size: 12px; } -.remote-hint { - margin-top: 10px; +.remote-quick-form { + display: grid; + grid-template-columns: minmax(180px, 280px) auto; + gap: 10px; + align-items: end; +} + +.remote-quick-form label { + display: grid; + gap: 6px; color: var(--muted); font-size: 12px; } +.advanced-settings { + margin-top: 12px; + border-top: 1px solid var(--line); + padding-top: 12px; +} + +.advanced-settings summary { + color: var(--muted); + cursor: pointer; + font-size: 13px; +} + +.advanced-settings .remote-form { + margin-top: 12px; +} + .remote-session-list { margin-top: 12px; } @@ -501,6 +525,64 @@ select { white-space: nowrap; } +.remote-session-row { + display: grid; + grid-template-columns: minmax(260px, 1.3fr) minmax(260px, 1fr) auto; + gap: 14px; + align-items: center; + min-height: 70px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface-raised); + padding: 12px; +} + +.remote-session-main { + display: flex; + min-width: 0; + gap: 12px; + align-items: center; +} + +.remote-session-main div { + display: grid; + min-width: 0; + gap: 4px; +} + +.remote-session-main small { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.remote-session-status { + display: inline-flex; + gap: 6px; + align-items: center; + color: var(--warn); + font-size: 12px; + font-weight: 700; +} + +.remote-session-status i { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; +} + +.remote-session-status.active { + color: var(--good); +} + +.remote-session-status.closed, +.remote-session-status.failed, +.remote-session-status.expired { + color: var(--muted); +} + .health-summary, .diagnostic-grid { display: grid; @@ -547,7 +629,7 @@ select { } .diagnostic-grid { - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(130px, 1fr)); } .diagnostic-btn { @@ -555,6 +637,55 @@ select { white-space: normal; } +.operation-status { + display: flex; + gap: 12px; + align-items: center; + min-height: 64px; + border: 1px solid #3b663e; + border-radius: 8px; + background: #203329; + padding: 12px; +} + +.operation-status.is-running { + border-color: #31596a; + background: #223640; +} + +.operation-status div { + display: grid; + flex: 1; + gap: 3px; +} + +.operation-status small { + color: var(--muted); +} + +.operation-icon { + display: grid; + width: 34px; + height: 34px; + flex: 0 0 34px; + place-items: center; + border-radius: 50%; + background: #31533a; + color: var(--good); + font-size: 18px; + font-weight: 800; +} + +.operation-status.is-running .operation-icon { + background: #2e5869; + color: var(--accent); +} + +.operation-subheading { + margin: 18px 0 10px; + font-size: 13px; +} + .uci-grid { display: grid; grid-template-columns: 150px minmax(160px, 1fr) minmax(140px, 0.8fr) minmax(180px, 1fr) 180px; @@ -1263,6 +1394,11 @@ select { flex-wrap: wrap; } + .remote-quick-form, + .remote-session-row { + grid-template-columns: 1fr; + } + .client-toolbar, .network-health { grid-template-columns: 1fr;