Simplify operator operations
This commit is contained in:
+69
-20
@@ -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 = '<div class="inline-empty">Удаленный доступ еще не открывался</div>';
|
||||
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 = `
|
||||
<div>
|
||||
<strong>${escapeHtml((session.target || "ssh").toUpperCase())} ${escapeHtml(statusLabel(session.status))}</strong><br>
|
||||
<small>${escapeHtml(session.id)}</small>
|
||||
<div class="remote-session-main">
|
||||
<span class="remote-session-status ${escapeHtml(session.status || "")}"><i></i>${escapeHtml(remoteStatusLabel(session.status))}</span>
|
||||
<div>
|
||||
<strong>Доступ к роутеру</strong>
|
||||
<small>${escapeHtml(endpoint)} · до ${escapeHtml(formatShortDate(session.expires_at))}</small>
|
||||
</div>
|
||||
</div>
|
||||
<span>${escapeHtml(endpoint)}</span>
|
||||
<span>expires ${escapeHtml(formatShortDate(session.expires_at))}</span>
|
||||
<code>${escapeHtml(connectCommand)}</code>
|
||||
<div class="row-actions">
|
||||
<button type="button" data-action="luci" ${canOpenLuCI ? "" : "disabled"}>Open LuCI</button>
|
||||
<button type="button" data-action="commands">Commands</button>
|
||||
<button type="button" data-action="close" ${canClose ? "" : "disabled"}>Close</button>
|
||||
<button type="button" data-action="copy" ${session.remote_port ? "" : "disabled"}>Копировать SSH</button>
|
||||
<button class="primary" type="button" data-action="luci" ${canOpenLuCI ? "" : "disabled"}>Открыть LuCI</button>
|
||||
<button type="button" data-action="close" ${canClose ? "" : "disabled"}>Закрыть</button>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<span class="operation-icon">↻</span>
|
||||
<div><strong>Диагностика запускается</strong><small>Отправляем проверки на роутер</small></div>
|
||||
`;
|
||||
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 = `
|
||||
<span class="operation-icon">✓</span>
|
||||
<div><strong>Диагностика запущена</strong><small>Результаты появятся в истории команд</small></div>
|
||||
<button id="openDiagnosticResultsBtn" type="button">Открыть результаты</button>
|
||||
`;
|
||||
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)));
|
||||
|
||||
+56
-37
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenWrt RMM</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=3">
|
||||
<link rel="stylesheet" href="/styles.css?v=4">
|
||||
</head>
|
||||
<body>
|
||||
<main id="loginView" class="login-view">
|
||||
@@ -258,9 +258,20 @@
|
||||
|
||||
<section class="panel" data-device-tab="operations">
|
||||
<div class="section-title">
|
||||
<h2>Быстрая диагностика</h2>
|
||||
<span class="summary-pill">без SSH</span>
|
||||
<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>
|
||||
@@ -397,38 +408,18 @@
|
||||
|
||||
<section id="remoteAccessPanel" class="panel" data-device-tab="operations">
|
||||
<div class="section-title">
|
||||
<h2>Remote access</h2>
|
||||
<div>
|
||||
<h2>Удаленный доступ</h2>
|
||||
<p class="section-subtitle">Временный доступ к LuCI и SSH автоматически закроется по таймеру</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<span id="remoteSummary" class="summary-pill">0 sessions</span>
|
||||
<button id="reloadRemoteSessionsBtn" type="button">Reload</button>
|
||||
<span id="remoteSummary" class="summary-pill">Нет активных сессий</span>
|
||||
<button id="reloadRemoteSessionsBtn" class="icon-button" type="button" title="Обновить сессии" aria-label="Обновить сессии">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="remote-form">
|
||||
<div class="remote-quick-form">
|
||||
<label>
|
||||
Tunnel server
|
||||
<input id="remoteServerHost" type="text" placeholder="10.10.10.2">
|
||||
</label>
|
||||
<label>
|
||||
Server SSH port
|
||||
<input id="remoteServerPort" type="number" min="1" max="65535" value="2222">
|
||||
</label>
|
||||
<label>
|
||||
Remote port
|
||||
<input id="remotePort" type="number" min="1" max="65535" placeholder="auto">
|
||||
</label>
|
||||
<label>
|
||||
Router SSH port
|
||||
<input id="remoteLocalPort" type="number" min="1" max="65535" value="22">
|
||||
</label>
|
||||
<label>
|
||||
LuCI protocol
|
||||
<select id="remoteLuCIScheme">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Duration
|
||||
Длительность доступа
|
||||
<select id="remoteDuration">
|
||||
<option value="900">15 min</option>
|
||||
<option value="1800">30 min</option>
|
||||
@@ -436,17 +427,45 @@
|
||||
<option value="7200">2 hours</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="createRemoteSessionBtn" type="button">Open remote access</button>
|
||||
</div>
|
||||
<div class="remote-hint">
|
||||
Active sessions provide SSH and an authenticated Open LuCI button. Choose the protocol used by this router.
|
||||
<button id="createRemoteSessionBtn" class="primary" type="button">Открыть удаленный доступ</button>
|
||||
</div>
|
||||
<details class="advanced-settings">
|
||||
<summary>Дополнительные настройки подключения</summary>
|
||||
<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">
|
||||
<h2>UCI presets</h2>
|
||||
<div>
|
||||
<h2>Быстрые настройки</h2>
|
||||
<p class="section-subtitle">Сначала проверьте изменения, затем примените их</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preset-grid">
|
||||
<div class="preset">
|
||||
@@ -533,6 +552,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/app.js?v=10" defer></script>
|
||||
<script src="/app.js?v=11" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+140
-4
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user