Improve device information UX

This commit is contained in:
benya
2026-06-04 20:42:40 +03:00
parent 063374150c
commit 615c256f8b
4 changed files with 245 additions and 16 deletions
+40
View File
@@ -43,6 +43,18 @@ const els = {
deviceName: document.querySelector("#deviceName"),
deviceMeta: document.querySelector("#deviceMeta"),
deviceBadge: document.querySelector("#deviceBadge"),
informationStatus: document.querySelector("#informationStatus"),
infoHostname: document.querySelector("#infoHostname"),
infoModel: document.querySelector("#infoModel"),
infoOpenWrt: document.querySelector("#infoOpenWrt"),
infoTarget: document.querySelector("#infoTarget"),
infoSystem: document.querySelector("#infoSystem"),
infoKernel: document.querySelector("#infoKernel"),
infoCreatedAt: document.querySelector("#infoCreatedAt"),
infoLastSeenAt: document.querySelector("#infoLastSeenAt"),
infoDeviceId: document.querySelector("#infoDeviceId"),
infoBoardName: document.querySelector("#infoBoardName"),
infoRootfs: document.querySelector("#infoRootfs"),
healthSummary: document.querySelector("#healthSummary"),
lastSeen: document.querySelector("#lastSeen"),
loadAvg: document.querySelector("#loadAvg"),
@@ -424,11 +436,30 @@ function renderDeviceDetail(device) {
els.fleetGroup.value = device.group || "";
els.fleetTags.value = Array.isArray(device.tags) ? device.tags.join(", ") : "";
els.inventoryJson.textContent = JSON.stringify(device.inventory || {}, null, 2);
renderDeviceInformation(device);
renderHealthSummary(device);
renderClients(device);
renderInterfaceCounters(device);
}
function renderDeviceInformation(device) {
const board = device.inventory && device.inventory.board ? device.inventory.board : {};
const release = board.release || {};
els.informationStatus.textContent = device.online ? "На связи" : "Не на связи";
els.informationStatus.className = `badge ${device.online ? "online" : "offline"}`;
els.infoHostname.textContent = deviceDisplayName(device);
els.infoModel.textContent = board.model || "-";
els.infoOpenWrt.textContent = release.description || device.openwrt_version || "-";
els.infoTarget.textContent = release.target || "-";
els.infoSystem.textContent = board.system || "-";
els.infoKernel.textContent = board.kernel || "-";
els.infoCreatedAt.textContent = formatDate(device.created_at);
els.infoLastSeenAt.textContent = formatDate(device.last_seen_at);
els.infoDeviceId.textContent = device.id || "-";
els.infoBoardName.textContent = board.board_name || "-";
els.infoRootfs.textContent = board.rootfs_type || "-";
}
function formatMemory(memory) {
if (!memory || !memory.total_kb) return "-";
return `${kbToMb(memory.used_kb)} / ${kbToMb(memory.total_kb)} MB`;
@@ -1224,6 +1255,15 @@ els.copyCommandOutputBtn.addEventListener("click", async () => {
setStatus("Output copied");
});
for (const button of document.querySelectorAll(".copy-info-btn")) {
button.addEventListener("click", async () => {
const source = document.querySelector(`#${button.dataset.copyInfo}`);
if (!source) return;
await navigator.clipboard.writeText(source.textContent);
setStatus("Значение скопировано");
});
}
for (const button of document.querySelectorAll(".preset-btn")) {
button.addEventListener("click", () => {
sendPresetCommand(button.dataset.preset, button.dataset.action).catch((error) => setStatus(error.message));
+87 -8
View File
@@ -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=4">
<link rel="stylesheet" href="/styles.css?v=5">
</head>
<body>
<main id="loginView" class="login-view">
@@ -173,23 +173,95 @@
<div id="healthSummary" class="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="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">
<h2>Группа и теги</h2>
<button id="saveFleetBtn" type="button">Сохранить</button>
<div>
<h2>Организация</h2>
<p class="section-subtitle">Группа и теги помогают находить и фильтровать объекты</p>
</div>
<button id="saveFleetBtn" class="primary" type="button">Сохранить</button>
</div>
<div class="fleet-form">
<label>
Group
<input id="fleetGroup" type="text" placeholder="core, lab, home">
Группа
<input id="fleetGroup" type="text" placeholder="Офис, дом, филиал">
</label>
<label>
Tags
<input id="fleetTags" type="text" placeholder="edge, vpn, test">
Теги через запятую
<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>
@@ -229,6 +301,13 @@
</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">
@@ -552,6 +631,6 @@
</main>
</div>
<script src="/app.js?v=11" defer></script>
<script src="/app.js?v=12" defer></script>
</body>
</html>
+111 -1
View File
@@ -1134,6 +1134,100 @@ select {
display: none;
}
.information-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.information-item {
display: grid;
gap: 6px;
min-height: 82px;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
background: var(--surface-raised);
padding: 14px;
}
.information-item:nth-child(4n) {
border-right: 0;
}
.information-item:nth-last-child(-n + 4) {
border-bottom: 0;
}
.information-item span,
.technical-details span {
color: var(--muted);
font-size: 12px;
}
.information-item strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.technical-details {
display: grid;
gap: 8px;
}
.technical-details > div {
display: grid;
grid-template-columns: 160px minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
min-height: 48px;
border-bottom: 1px solid var(--line);
padding: 4px 0;
}
.technical-details > div:last-child {
border-bottom: 0;
}
.technical-details code {
overflow: hidden;
color: var(--text);
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-info-btn {
min-height: 30px;
color: var(--accent);
}
.expert-notice {
display: flex;
gap: 12px;
align-items: center;
border: 1px solid #715d31;
border-radius: 8px;
background: #342e22;
margin-bottom: 14px;
padding: 14px;
}
.expert-notice div {
display: grid;
gap: 4px;
}
.expert-notice strong {
color: var(--warn);
}
.expert-notice span {
color: var(--muted);
font-size: 13px;
}
.metric {
min-width: 0;
border: 1px solid var(--line);
@@ -1400,7 +1494,23 @@ select {
}
.client-toolbar,
.network-health {
.network-health,
.information-grid {
grid-template-columns: 1fr;
}
.information-item,
.information-item:nth-child(4n),
.information-item:nth-last-child(-n + 4) {
border-right: 0;
border-bottom: 1px solid var(--line);
}
.information-item:last-child {
border-bottom: 0;
}
.technical-details > div {
grid-template-columns: 1fr;
}