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 = '
-
${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
-
+ Нет активных сессий
+
-