diff --git a/UI_CHECKLIST.md b/UI_CHECKLIST.md new file mode 100644 index 0000000..7cac3bc --- /dev/null +++ b/UI_CHECKLIST.md @@ -0,0 +1,190 @@ +# UI/UX Checklist + +Обозначения: + +- `[x]` — уже реализовано. +- `[ ]` — требуется сделать. +- `[~]` — реализовано частично, требуется переработка. + +## Текущее состояние + +- [x] Авторизация оператора. +- [x] Список устройств. +- [x] Просмотр состояния устройства. +- [x] Метрики и предупреждения. +- [x] DHCP- и Wi-Fi-клиенты. +- [x] Команды и история выполнения. +- [x] UCI-конфигурация и staged apply. +- [x] Управление пакетами. +- [x] Удаленный SSH-доступ и LuCI. +- [x] Аудит действий. +- [~] Обзор устройства понятен оператору. +- [~] История метрик пригодна для быстрого анализа. +- [~] Удаленный доступ запускается простым сценарием. +- [ ] Основные и экспертные функции визуально разделены. + +## Этап 0. UX-аудит + +- [ ] Составить полный список текущих экранов, панелей и кнопок. +- [ ] Для каждого элемента назначить действие: оставить, перенести, объединить или скрыть. +- [ ] Зафиксировать терминологию для оператора. +- [ ] Зафиксировать набор статусов и цветов. +- [ ] Определить основные пользовательские сценарии. +- [ ] Определить структуру навигации и маршруты страниц. +- [ ] Определить desktop и mobile макеты. + +## Этап 1. Каркас и объекты + +- [x] Добавить постоянную основную навигацию. +- [x] Добавить верхнюю панель с глобальным обновлением. +- [x] Создать полноширинную таблицу объектов. +- [x] Добавить статус объекта с текстом и индикатором. +- [x] Показывать имя, модель, группу, WAN, клиентов, проблемы и последний контакт. +- [x] Добавить поиск по имени, модели, ID, WAN-адресу, группе и тегам. +- [x] Добавить фильтр по статусу. +- [x] Добавить фильтр по группе. +- [x] Добавить фильтр по наличию проблем. +- [ ] Добавить сортировку колонок. +- [x] Открывать объект кликом по строке. +- [ ] Показывать массовые действия только после выбора объектов. +- [ ] Добавить loading, empty и error states таблицы. +- [ ] Проверить таблицу на 1366 px и 1920 px. +- [ ] Сделать мобильное представление списка объектов. + +## Этап 2. Карточка и обзор объекта + +- [ ] Создать верхнюю панель объекта. +- [ ] Добавить хлебную крошку возврата к объектам. +- [ ] Добавить вкладки «Обзор», «Клиенты», «Сеть», «Операции», «Сведения», «Эксперт». +- [ ] Показать статус и время последнего контакта. +- [ ] Показать WAN и доступность сервера. +- [ ] Показать CPU/load, память, uptime и число клиентов. +- [ ] Убрать дублирующиеся метрики. +- [ ] Показать активные предупреждения с приоритетами. +- [ ] Добавить компактные графики метрик. +- [ ] Добавить выбор периода графиков. +- [ ] Добавить основную кнопку открытия LuCI. +- [ ] Добавить кнопку запуска диагностики. +- [ ] Перенести перезагрузку и опасные действия в отдельное меню. +- [ ] Убрать raw inventory с обзорной страницы. +- [ ] Убрать произвольные команды с обзорной страницы. +- [ ] Убрать редактирование группы и тегов с обзорной страницы. + +## Этап 3. Клиенты + +- [ ] Объединить DHCP leases и Wi-Fi stations в единую модель клиента. +- [ ] Создать таблицу клиентов. +- [ ] Показывать online/offline состояние. +- [ ] Показывать имя, IP, MAC и тип подключения. +- [ ] Для Wi-Fi показывать сеть, сигнал и скорость. +- [ ] Для Ethernet показывать интерфейс или порт, если доступно. +- [ ] Добавить поиск по имени, IP и MAC. +- [ ] Добавить фильтры по состоянию и типу подключения. +- [ ] Добавить понятное состояние отсутствия клиентов. +- [ ] Проверить длинные имена и неизвестные значения. + +## Этап 3. Сеть + +- [ ] Создать сводку WAN-подключения. +- [ ] Показывать состояние интернет-доступа и внешний IP. +- [ ] Создать список LAN/WAN/Wi-Fi/VPN интерфейсов. +- [ ] Показывать адреса, состояние и счетчики трафика. +- [ ] Показать основные маршруты. +- [ ] Показывать Wi-Fi сети и подключенных клиентов. +- [ ] Спрятать низкоуровневые детали в раскрываемые блоки. +- [ ] Добавить loading, empty и error states. + +## Этап 4. Операции + +- [ ] Объединить отдельные диагностические кнопки в один сценарий. +- [ ] Показывать прогресс диагностики. +- [ ] Показывать итог диагностики понятным языком. +- [ ] Добавить рекомендации по найденным проблемам. +- [ ] Упростить создание удаленной сессии до выбора длительности. +- [ ] Спрятать host, порты и параметры туннеля в дополнительные настройки. +- [ ] Добавить действия «Открыть LuCI» и «Скопировать SSH». +- [ ] Показывать оставшееся время удаленной сессии. +- [ ] Добавить понятное закрытие удаленной сессии. +- [ ] Объединить Preview и Stage в сценарий «Проверить → Применить». +- [ ] Показывать изменения перед применением. +- [ ] Понятно объяснять commit confirmed и автоматический откат. +- [ ] Добавить безопасные пресеты настроек. +- [ ] Перенести низкоуровневое управление пакетами в экспертный режим. +- [ ] Добавить подтверждение опасных операций. + +## Этап 5. Сведения + +- [ ] Показать модель, плату и версию OpenWrt. +- [ ] Показать hostname и идентификатор устройства. +- [ ] Показать группу и теги. +- [ ] Добавить редактирование группы и тегов. +- [ ] Показать даты регистрации и последнего контакта. +- [ ] Показать версию агента, когда она будет доступна. +- [ ] Добавить копирование технических идентификаторов. + +## Этап 5. Экспертный режим + +- [ ] Перенести raw inventory. +- [ ] Перенести произвольные команды. +- [ ] Перенести raw UCI show/set/commit/revert. +- [ ] Перенести низкоуровневое управление пакетами. +- [ ] Перенести полную историю команд. +- [ ] Сократить основные фильтры истории до «Все», «Выполняются», «Ошибка». +- [ ] Перенести дополнительные фильтры в меню. +- [ ] Перенести аудит. +- [ ] Добавить предупреждение перед опасными действиями. +- [ ] Подготовить экспертный режим к разграничению прав. + +## Убираем и объединяем + +- [ ] Убрать локальные Reload / Refresh из отдельных панелей. +- [ ] Добавить автообновление данных. +- [x] Оставить одну глобальную кнопку обновления на экране объектов. +- [x] Убрать постоянный выбор массовой команды. +- [ ] Показывать массовые действия только при выбранных объектах. +- [ ] Объединить дублирующие блоки состояния и метрик. +- [ ] Объединить диагностические действия. +- [ ] Объединить Preview и Stage. +- [ ] Спрятать технические поля удаленного доступа. +- [ ] Спрятать raw JSON и raw UCI от оператора. +- [ ] Убрать опасные действия из основных кнопок. + +## Общие состояния и качество + +- [ ] Добавить skeleton loading. +- [ ] Добавить единый компонент пустого состояния. +- [ ] Добавить единый компонент ошибки. +- [ ] Добавить toast-уведомления. +- [ ] Добавить единые диалоги подтверждения. +- [ ] Показывать request ID в раскрываемых деталях ошибки. +- [ ] Добавить tooltips для непонятных иконок. +- [ ] Проверить навигацию клавиатурой. +- [ ] Проверить видимый focus state. +- [ ] Проверить контраст текста и статусов. +- [ ] Проверить отсутствие наложений элементов. +- [ ] Проверить отсутствие обрезанного текста. +- [ ] Проверить преднамеренность горизонтального скролла таблиц. +- [ ] Проверить интерфейс на desktop, tablet и mobile. +- [ ] Сделать браузерные скриншоты ключевых экранов. + +## Требования к API + +- [ ] Нормализовать данные клиентов. +- [ ] Нормализовать состояние интерфейсов. +- [ ] Подготовить временные ряды для графиков. +- [ ] Добавить операторские операции поверх сырых команд. +- [ ] Добавить серверную пагинацию списка объектов при необходимости. +- [ ] Добавить серверный поиск и сортировку при необходимости. + +## Приемка редизайна + +- [ ] За 10 секунд видно, какие объекты требуют внимания. +- [ ] Причина проблемы понятна без raw JSON. +- [ ] LuCI открывается максимум за два клика. +- [ ] Диагностика запускается максимум за три клика. +- [ ] Обычный оператор не видит raw UCI и произвольные команды. +- [ ] Опасные действия требуют явного подтверждения. +- [ ] Основные сценарии работают на ширине 1366 px. +- [ ] Основные сценарии работают на ширине 1920 px. +- [ ] Основные сценарии доступны на мобильном экране. +- [ ] Ошибки API показываются понятным языком. diff --git a/UI_ROADMAP.md b/UI_ROADMAP.md new file mode 100644 index 0000000..1d5a525 --- /dev/null +++ b/UI_ROADMAP.md @@ -0,0 +1,179 @@ +# UI/UX Roadmap + +## Цель + +Сделать RMM-интерфейс удобным для оператора, который обслуживает роутеры, но не обязан знать OpenWrt, UCI и внутренние команды агента. + +Главный экран должен за несколько секунд отвечать на три вопроса: + +1. Какие роутеры требуют внимания? +2. Что именно не работает? +3. Какое безопасное действие можно выполнить? + +Скриншоты Netcraze используются как ориентир по плотности, навигации и представлению данных, без копирования бренда и интерфейса один в один. + +## Роли + +### Оператор + +- Видит состояние объектов, клиентов, сети и предупреждений. +- Запускает готовые диагностики и безопасные операции. +- Открывает удаленный доступ и LuCI. +- Не работает с raw JSON, UCI-командами и техническими параметрами туннеля. + +### Эксперт + +- Имеет доступ ко всем существующим низкоуровневым функциям. +- Просматривает raw inventory, историю команд и аудит. +- Выполняет произвольные команды, UCI-операции и управление пакетами. + +На первом этапе экспертный режим будет отдельным разделом интерфейса. Разграничение прав можно добавить позже. + +## Принципы интерфейса + +- Плотный рабочий интерфейс без маркетинговых блоков и декоративных карточек. +- Единая навигация и предсказуемые вкладки. +- Статусы читаются по тексту и цвету: норма, внимание, ошибка, недоступен. +- Основные действия доступны сразу, редкие и опасные спрятаны в меню. +- Одна глобальная кнопка обновления вместо кнопок обновления в каждом блоке. +- Автообновление данных с понятным временем последнего контакта. +- Русские операторские названия; технические термины остаются в экспертном режиме. +- Любая опасная операция требует подтверждения и объясняет последствия. +- Ошибки показывают понятное описание; request ID доступен в раскрываемых деталях. + +## Целевая структура + +### 1. Объекты + +Первый экран после входа: полноширинная таблица всех роутеров. + +Основные колонки: + +- Статус. +- Имя объекта. +- Модель и группа. +- WAN / интернет. +- Клиенты. +- Проблемы. +- Последний контакт. + +Доступные действия: + +- Поиск. +- Фильтры по статусу, группе и проблемам. +- Открытие объекта кликом по строке. +- Массовые действия появляются только после выбора объектов. + +### 2. Карточка объекта + +Верхняя часть содержит имя, статус, последний контакт и основные действия. + +Вкладки: + +- **Обзор** — здоровье роутера, предупреждения, основные метрики и графики. +- **Клиенты** — единый список DHCP- и Wi-Fi-клиентов. +- **Сеть** — WAN, интерфейсы, Wi-Fi, маршруты и счетчики. +- **Операции** — диагностика, удаленный доступ, обновления и безопасные настройки. +- **Сведения** — модель, прошивка, группа, теги и идентификаторы. +- **Эксперт** — raw inventory, команды, UCI, пакеты, история и аудит. + +## Что меняем в текущем UI + +| Сейчас | После редизайна | +| --- | --- | +| Список устройств в узкой боковой панели | Полноширинная таблица объектов | +| Длинная страница со всеми функциями | Карточка объекта с вкладками | +| Много кнопок Reload / Refresh | Автообновление и одна глобальная кнопка | +| Raw Inventory на обзорной странице | Раздел «Эксперт» | +| Выбор произвольной команды на обзорной странице | Раздел «Эксперт» | +| Пять отдельных кнопок диагностики | Единый сценарий «Запустить диагностику» | +| Raw UCI и множество кнопок | Пошаговое применение безопасных настроек; raw UCI в «Эксперт» | +| Preview и Stage как отдельные действия | Сценарий «Проверить изменения → Применить» | +| Технические поля SSH-туннеля | Выбор длительности и кнопки «Открыть LuCI» / «Скопировать SSH» | +| Редактирование группы и тегов в обзоре | Вкладка «Сведения» | +| Все статусы истории команд отдельными кнопками | Основные фильтры и меню дополнительных фильтров | +| Опасные действия рядом с обычными | Отдельное меню с подтверждением | + +## Этапы реализации + +### Этап 0. UX-аудит и фундамент + +- Сопоставить каждый текущий элемент с действием: оставить, перенести, объединить или скрыть. +- Зафиксировать операторские названия и статусы. +- Подготовить общий layout, компоненты статусов, кнопок, таблиц и уведомлений. +- Определить адаптивное поведение для desktop и mobile. + +### Этап 1. Навигация и список объектов + +- Добавить постоянную навигацию по основным разделам. +- Перенести список роутеров в плотную таблицу. +- Добавить поиск, фильтры и сортировку. +- Показывать проблемы и последний контакт непосредственно в строке объекта. +- Показывать массовые действия только после выбора строк. + +### Этап 2. Обзор объекта + +- Добавить верхнюю панель объекта и вкладки. +- Собрать компактную строку состояния. +- Показать WAN, загрузку, память, uptime, клиентов и предупреждения. +- Добавить основные графики за выбранный период. +- Оставить только главные действия: LuCI, диагностика и меню операций. + +### Этап 3. Клиенты и сеть + +- Объединить DHCP leases и Wi-Fi stations в таблицу клиентов. +- Добавить поиск и фильтры клиентов. +- Создать отдельный экран интерфейсов и подключений. +- Показать понятные состояния WAN, LAN и Wi-Fi. + +### Этап 4. Операторские операции + +- Сделать пошаговую диагностику с прогрессом и итогом. +- Упростить создание удаленной сессии. +- Добавить безопасные формы настроек вместо raw UCI. +- Представить обновления и пакеты как понятные операции. +- Показывать ход, результат и рекомендации после операции. + +### Этап 5. Экспертный режим + +- Перенести существующие технические возможности без потери функциональности. +- Разместить raw inventory, команды, UCI, пакеты, историю и аудит. +- Отделить опасные операции и добавить явные подтверждения. +- Подготовить интерфейс к будущему разграничению прав. + +### Этап 6. Состояния и полировка + +- Добавить skeleton, empty, loading, offline и error states. +- Добавить toast-уведомления и единый стиль подтверждений. +- Проверить навигацию клавиатурой и фокус. +- Проверить таблицы, переносы текста и отсутствие наложений. +- Проверить desktop, tablet и mobile. + +### Этап 7. Недостающие данные API + +- Нормализовать запись клиента из DHCP и Wi-Fi. +- Нормализовать состояние интерфейсов и подключений. +- Подготовить временные ряды для графиков. +- Добавить операторские операции поверх сырых команд. +- При необходимости добавить серверную пагинацию, поиск и сортировку. + +## Приоритет первой итерации + +1. Новый layout и полноширинный список объектов. +2. Карточка объекта с вкладками. +3. Новый обзор объекта. +4. Упрощенный удаленный доступ и LuCI. +5. Перенос технических функций в «Эксперт». + +## Критерии готовности + +- Оператор за 10 секунд понимает, какие объекты требуют внимания. +- Состояние объекта понятно без просмотра JSON и истории команд. +- LuCI открывается не более чем за два клика. +- Диагностика запускается не более чем за три клика. +- В обычном режиме нет raw UCI, произвольных команд и параметров туннеля. +- Опасные операции всегда требуют явного подтверждения. +- Интерфейс корректно работает на ширине 1366, 1920 и на мобильном экране. +- Нет случайного горизонтального скролла, наложений и обрезанного текста. +- Ошибки API отображаются понятно и позволяют раскрыть технические детали. + diff --git a/web/app.js b/web/app.js index 072f6e6..3b0687e 100644 --- a/web/app.js +++ b/web/app.js @@ -22,10 +22,17 @@ const els = { apiState: document.querySelector("#apiState"), refreshBtn: document.querySelector("#refreshBtn"), deviceList: document.querySelector("#deviceList"), + fleetView: document.querySelector("#fleetView"), + fleetTotalCount: document.querySelector("#fleetTotalCount"), + fleetOnlineCount: document.querySelector("#fleetOnlineCount"), + fleetOfflineCount: document.querySelector("#fleetOfflineCount"), + fleetAlertCount: document.querySelector("#fleetAlertCount"), + navAlertCount: document.querySelector("#navAlertCount"), statusLine: document.querySelector("#statusLine"), pageTitle: document.querySelector("#pageTitle"), emptyState: document.querySelector("#emptyState"), deviceView: document.querySelector("#deviceView"), + backToFleetBtn: document.querySelector("#backToFleetBtn"), deviceName: document.querySelector("#deviceName"), deviceMeta: document.querySelector("#deviceMeta"), deviceBadge: document.querySelector("#deviceBadge"), @@ -269,6 +276,26 @@ function deviceDisplayName(device) { return device.id || "unknown"; } +function deviceModel(device) { + return device && device.inventory && device.inventory.board && device.inventory.board.model + ? device.inventory.board.model + : "-"; +} + +function deviceClientCount(device) { + const leases = Array.isArray(device.inventory && device.inventory.dhcp_leases) ? device.inventory.dhcp_leases.length : 0; + const wifi = Array.isArray(device.inventory && device.inventory.wifi_clients) ? device.inventory.wifi_clients.length : 0; + return { leases, wifi, total: Math.max(leases, wifi) }; +} + +function deviceWanSummary(device) { + const wanIP = device.inventory && device.inventory.wan_ip; + const checks = Array.isArray(device.metrics && device.metrics.connectivity_checks) ? device.metrics.connectivity_checks : []; + if (!device.online) return { label: "Нет данных", className: "offline" }; + if (checks.some((check) => !check.reachable)) return { label: wanIP || "Есть потери связи", className: "warning" }; + return { label: wanIP || "Подключено", className: "online" }; +} + function filteredDevices() { const search = els.fleetSearch.value.trim().toLowerCase(); const group = els.fleetGroupFilter.value.trim().toLowerCase(); @@ -284,6 +311,8 @@ function filteredDevices() { deviceDisplayName(device), device.id, device.openwrt_version, + deviceModel(device), + device.inventory && device.inventory.wan_ip, device.group, ...(device.tags || []), ].join(" ").toLowerCase(); @@ -296,27 +325,46 @@ function filteredDevices() { function renderDevices() { const devices = filteredDevices(); els.deviceList.innerHTML = ""; + const online = state.devices.filter((device) => device.online).length; + const alerts = state.devices.filter((device) => device.active_alerts).length; + els.fleetTotalCount.textContent = state.devices.length; + els.fleetOnlineCount.textContent = online; + els.fleetOfflineCount.textContent = state.devices.length - online; + els.fleetAlertCount.textContent = alerts; + els.navAlertCount.textContent = alerts; if (devices.length === 0) { - const empty = document.createElement("div"); - empty.className = "device-item"; - empty.textContent = "No devices"; - els.deviceList.appendChild(empty); + els.emptyState.classList.remove("is-hidden"); return; } + els.emptyState.classList.add("is-hidden"); for (const device of devices) { const displayName = deviceDisplayName(device); + const clients = deviceClientCount(device); + const wan = deviceWanSummary(device); const item = document.createElement("button"); item.type = "button"; item.className = `device-item ${device.id === state.selectedDeviceId ? "is-selected" : ""}`; item.innerHTML = ` -
+ + ${device.online ? "На связи" : "Не на связи"} + +
${escapeHtml(displayName)} - ${device.active_alerts ? `${device.active_alerts}` : ""} + ${escapeHtml(device.openwrt_version || device.id)}
- ${device.online ? "online" : "offline"} - ${escapeHtml(device.openwrt_version || "unknown")} - ${escapeHtml([device.group || "", ...(device.tags || [])].filter(Boolean).join(" / ") || device.id)} +
+ ${escapeHtml(deviceModel(device))} + ${escapeHtml([device.group || "", ...(device.tags || [])].filter(Boolean).join(" / ") || "Без группы")} +
+ ${escapeHtml(wan.label)} + ${clients.total} + + ${device.active_alerts ? `${device.active_alerts} активн.` : "Нет"} + + ${escapeHtml(formatDate(device.last_seen_at))} + `; item.addEventListener("click", () => selectDevice(device.id)); els.deviceList.appendChild(item); @@ -325,13 +373,13 @@ function renderDevices() { function renderDeviceDetail(device) { if (!device) { - els.emptyState.classList.remove("is-hidden"); + els.fleetView.classList.remove("is-hidden"); els.deviceView.classList.add("is-hidden"); - els.pageTitle.textContent = "Devices"; + els.pageTitle.textContent = "Объекты"; return; } - els.emptyState.classList.add("is-hidden"); + els.fleetView.classList.add("is-hidden"); els.deviceView.classList.remove("is-hidden"); const displayName = deviceDisplayName(device); els.pageTitle.textContent = displayName; @@ -509,18 +557,18 @@ function renderAlerts(alerts) { } async function loadDevices() { - setStatus("Loading devices"); + setStatus("Обновление"); const data = await api("/api/devices"); state.devices = data.devices || []; - if (!state.selectedDeviceId && state.devices.length > 0) { - state.selectedDeviceId = state.devices[0].id; + if (state.selectedDeviceId && !currentDevice()) { + state.selectedDeviceId = null; } renderDevices(); renderDeviceDetail(currentDevice()); if (state.selectedDeviceId) { await Promise.all([loadCommands(), loadAudit(), loadMetricsHistory(), loadAlerts(), loadRemoteSessions()]); } - setStatus("Ready"); + setStatus(`Обновлено ${new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`); } async function selectDevice(id) { @@ -530,6 +578,13 @@ async function selectDevice(id) { await Promise.all([loadCommands(), loadAudit(), loadMetricsHistory(), loadAlerts(), loadRemoteSessions()]); } +function showFleet() { + state.selectedDeviceId = null; + state.selectedCommand = null; + renderDevices(); + renderDeviceDetail(null); +} + async function loadCommands() { if (!state.selectedDeviceId) return; const data = await api(`/api/devices/${encodeURIComponent(state.selectedDeviceId)}/commands?limit=50`); @@ -987,6 +1042,7 @@ els.loginForm.addEventListener("submit", (event) => { els.logoutBtn.addEventListener("click", () => logout().catch((error) => setStatus(error.message))); els.refreshBtn.addEventListener("click", () => loadDevices().catch((error) => setStatus(error.message))); +els.backToFleetBtn.addEventListener("click", showFleet); 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 a7bd645..cc78bd9 100644 --- a/web/index.html +++ b/web/index.html @@ -35,37 +35,38 @@
R

OpenWrt RMM

-

API: checking

+

Проверка сервера

-
-
-
- Signed in as - operator -
- -
-
+ -
-
-

Devices

- + + +
+
О
+
+ Оператор + operator
-
- - - - -
-
- - - -
-
+ +
+ + -
-
+
-

Fleet

-

Devices

+

Управление

+

Объекты

+
+
+ Готово +
-
Idle
-
-

No device selected

-

Waiting for agent check-in.

+
+
+
+ Всего + 0 +
+
+ На связи + 0 +
+
+ Не на связи + 0 +
+
+ Требуют внимания + 0 +
+
+ +
+ +
+ + + + +
+ + +
+ +
+
+ Статус + Объект + Модель и группа + WAN / интернет + Клиенты + Проблемы + Последний контакт + +
+
+
+ +
-
- + diff --git a/web/styles.css b/web/styles.css index bb5b5c7..f4e5cc0 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1,17 +1,18 @@ :root { - color-scheme: light; - --bg: #f4f6f8; - --surface: #ffffff; - --surface-muted: #eef2f5; - --text: #17202a; - --muted: #667085; - --line: #d9e0e7; - --accent: #166f7a; - --accent-strong: #10545c; - --good: #137a48; - --bad: #b42318; - --warn: #b54708; - --shadow: 0 14px 35px rgba(23, 32, 42, 0.08); + color-scheme: dark; + --bg: #14191f; + --surface: #1e252d; + --surface-muted: #29333d; + --surface-raised: #242d36; + --text: #f4f7fa; + --muted: #9baaba; + --line: #3a4652; + --accent: #21a8d8; + --accent-strong: #1687b0; + --good: #73c83e; + --bad: #f05252; + --warn: #f3b43f; + --shadow: none; } * { @@ -63,14 +64,14 @@ select { min-height: 36px; border: 1px solid var(--line); border-radius: 6px; - background: #fff; + background: var(--surface); color: var(--text); padding: 0 10px; } .app-shell { display: grid; - grid-template-columns: 360px minmax(0, 1fr); + grid-template-columns: 220px minmax(0, 1fr); min-height: 100vh; } @@ -116,9 +117,11 @@ select { .sidebar { border-right: 1px solid var(--line); - background: #fbfcfd; - padding: 18px; + background: #1b2128; + padding: 16px 12px; overflow: auto; + display: flex; + flex-direction: column; } .brand { @@ -161,7 +164,12 @@ select { .workspace { min-width: 0; - padding: 22px; + padding: 0 28px 28px; +} + +.topbar { + min-height: 78px; + border-bottom: 1px solid var(--line); } .topbar, @@ -186,6 +194,12 @@ select { font-size: 14px; } +.topbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + .panel { background: var(--surface); border: 1px solid var(--line); @@ -195,6 +209,172 @@ select { padding: 14px; } +.main-nav { + display: grid; + gap: 5px; +} + +.nav-item { + display: grid; + grid-template-columns: 24px minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + min-height: 42px; + border-color: transparent; + background: transparent; + color: var(--muted); + text-align: left; +} + +.nav-item.is-active { + border-color: #31596a; + background: #223640; + color: var(--text); +} + +.nav-item:disabled { + cursor: default; + opacity: 0.58; +} + +.nav-icon { + color: var(--accent); + font-weight: 800; + text-align: center; +} + +.nav-count { + min-width: 22px; + border-radius: 999px; + background: var(--surface-muted); + padding: 2px 6px; + text-align: center; +} + +.sidebar-spacer { + flex: 1; + min-height: 32px; +} + +.operator-row { + border-top: 1px solid var(--line); + padding-top: 14px; +} + +.operator-avatar { + display: grid !important; + width: 32px; + height: 32px; + flex: 0 0 32px; + place-items: center; + border-radius: 50%; + background: #2e5869; + color: #fff; + font-weight: 800; +} + +.operator-copy { + min-width: 0; + flex: 1; +} + +.operator-copy strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.icon-button { + width: 36px; + padding: 0; + font-size: 20px; +} + +.fleet-view { + display: grid; + gap: 16px; +} + +.fleet-summary { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); +} + +.fleet-summary > div { + display: grid; + gap: 4px; + min-height: 72px; + border-right: 1px solid var(--line); + padding: 14px 18px; +} + +.fleet-summary > div:last-child { + border-right: 0; +} + +.fleet-summary span { + color: var(--muted); + font-size: 12px; +} + +.fleet-summary strong { + font-size: 22px; +} + +.fleet-summary .summary-good strong { + color: var(--good); +} + +.fleet-summary .summary-bad strong { + color: var(--bad); +} + +.fleet-summary .summary-warn strong { + color: var(--warn); +} + +.fleet-toolbar { + display: grid; + grid-template-columns: minmax(260px, 1fr) auto 180px 180px; + gap: 10px; + align-items: center; +} + +.fleet-toolbar .filters { + display: flex; + gap: 6px; + margin: 0; +} + +.fleet-table-shell { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); +} + +.fleet-table-head, +.device-item { + display: grid; + grid-template-columns: 125px minmax(200px, 1.25fr) minmax(180px, 1fr) minmax(160px, 1fr) 80px 110px 170px 28px; + gap: 14px; + align-items: center; + min-width: 1180px; +} + +.fleet-table-head { + min-height: 46px; + border-bottom: 1px solid var(--line); + background: var(--surface-muted); + color: var(--muted); + font-size: 12px; + font-weight: 700; + padding: 0 14px; +} + .auth-panel label { display: block; color: var(--muted); @@ -469,21 +649,30 @@ select { .device-list { display: grid; - gap: 8px; } .device-item { - display: grid; - gap: 5px; width: 100%; - min-height: 72px; - padding: 10px; + min-height: 68px; + border: 0; + border-bottom: 1px solid var(--line); + border-radius: 0; + background: transparent; + padding: 8px 14px; text-align: left; } +.device-item:last-child { + border-bottom: 0; +} + +.device-item:hover { + border-color: var(--line); + background: #242e37; +} + .device-item.is-selected { - border-color: var(--accent); - background: #f1fbfc; + background: #223640; } .device-item strong { @@ -493,25 +682,77 @@ select { white-space: nowrap; } -.device-item span { +.device-item span, +.device-item small { color: var(--muted); font-size: 13px; } -.device-item .device-line { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; +.device-identity, +.device-context { + display: grid; + min-width: 0; + gap: 4px; } -.device-alert-count { - border-radius: 999px; - background: #fef3f2; +.device-identity strong, +.device-identity small, +.device-context span, +.device-context small, +.last-contact, +.wan-state { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.fleet-status { + display: inline-flex; + gap: 7px; + align-items: center; + font-weight: 700; +} + +.fleet-status i { + width: 8px; + height: 8px; + flex: 0 0 8px; + border-radius: 50%; + background: currentColor; +} + +.fleet-status.online, +.wan-state.online { + color: var(--good); +} + +.fleet-status.offline, +.wan-state.offline { color: var(--bad); - font-size: 12px; +} + +.wan-state.warning { + color: var(--warn); +} + +.client-count { + color: var(--text) !important; font-weight: 800; - padding: 2px 7px; +} + +.problem-count { + color: var(--good) !important; +} + +.problem-count.has-problems { + color: var(--warn) !important; + font-weight: 800; +} + +.row-chevron { + color: var(--accent) !important; + font-size: 22px !important; + text-align: right; } .badge { @@ -541,6 +782,14 @@ select { text-align: center; } +.back-button { + min-height: 30px; + border: 0; + background: transparent; + color: var(--accent); + padding: 0; +} + .metrics-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -775,6 +1024,28 @@ select { border-bottom: 1px solid var(--line); } + .main-nav { + grid-template-columns: repeat(3, 1fr); + } + + .sidebar-spacer { + display: none; + } + + .operator-row { + margin-top: 10px; + } + + .fleet-summary, + .fleet-toolbar { + grid-template-columns: 1fr 1fr; + } + + .fleet-toolbar > input:first-child, + .fleet-toolbar .filters { + grid-column: 1 / -1; + } + .row, .row.audit, .command-form,