|
|
@@ -1,622 +1,746 @@
|
|
|
-# Анализ Legacy Devices Panel
|
|
|
+# Legacy Admin Panel - Анализ ключевых фич
|
|
|
|
|
|
-## Обзор legacy проекта
|
|
|
+## Расположение
|
|
|
+`/home/user/work/luckfox/mybeacon-legacy/www/my-wifi/panel/`
|
|
|
|
|
|
-Расположение: `/home/user/work/luckfox/mybeacon-legacy/www/my-wifi/panel/`
|
|
|
+## Структура legacy админки
|
|
|
|
|
|
-Технологии:
|
|
|
-- **Backend**: PHP 5.x/7.x + MySQL (MariaDB)
|
|
|
-- **Frontend**: jQuery + DataTables + jEditable
|
|
|
-- **Стиль**: Bootstrap 3 + кастомная тема
|
|
|
+### 3 основные страницы:
|
|
|
|
|
|
-## Структура БД (Legacy)
|
|
|
+1. **Devices (index.php)** - управление устройствами (радарами)
|
|
|
+2. **Clients (clients.php)** - управление клиентами (организациями)
|
|
|
+3. **Add Device (adddev.php)** - добавление клиента с устройствами
|
|
|
|
|
|
-### Таблица `configs` (устройства)
|
|
|
-```sql
|
|
|
-CREATE TABLE `configs` (
|
|
|
- `id` int(11) PRIMARY KEY AUTO_INCREMENT,
|
|
|
- `time` timestamp DEFAULT current_timestamp(), -- Last seen
|
|
|
- `mac` varchar(40) UNIQUE NOT NULL,
|
|
|
- `wf_client_ssid` varchar(60), -- WiFi SSID для подключения
|
|
|
- `wf_client_psk` varchar(60), -- WiFi пароль
|
|
|
- `ovpn_flag` tinyint(1), -- OpenVPN включен
|
|
|
- `ovpn_addr` varchar(60), -- URL для получения VPN конфига
|
|
|
- `wf_flag` tinyint(1), -- WiFi сбор включен
|
|
|
- `wf_addr` varchar(60), -- URL для отправки WiFi данных
|
|
|
- `bt_flag` tinyint(1), -- BLE сбор включен
|
|
|
- `bt_addr` varchar(60), -- URL для отправки BLE данных
|
|
|
- `fw_flag` tinyint(1), -- Firmware update включен
|
|
|
- `fw_addr` varchar(60), -- URL для получения firmware
|
|
|
- `reboot_flag` tinyint(1), -- Флаг перезагрузки
|
|
|
- `ip` varchar(40) -- IP адрес устройства
|
|
|
-);
|
|
|
-```
|
|
|
-
|
|
|
-### Таблица `users` (клиенты/организации)
|
|
|
+---
|
|
|
+
|
|
|
+## КЛЮЧЕВЫЕ ФИЧИ (без технической реализации)
|
|
|
+
|
|
|
+### 1. Devices Panel - Управление устройствами
|
|
|
+
|
|
|
+#### A. Универсальный поиск ⭐ КЛЮЧЕВАЯ ФИЧА
|
|
|
+
|
|
|
+**Что делает:**
|
|
|
+- Пользователь вводит любой текст в одно поле
|
|
|
+- Система ищет по **всем** колонкам таблицы одновременно
|
|
|
+- Фильтрация в реальном времени (при вводе)
|
|
|
+
|
|
|
+**Примеры поиска:**
|
|
|
+- `ac:84` → найдет все MAC-адреса содержащие "ac:84"
|
|
|
+- `192.168` → найдет все устройства с IP в этой подсети
|
|
|
+- `Office_WiFi` → найдет все устройства подключенные к этому SSID
|
|
|
+- `beacon.ru` → найдет все устройства с этим адресом отправки данных
|
|
|
+
|
|
|
+**Почему важно:**
|
|
|
+- Админ не знает точный MAC → вводит часть → быстро находит
|
|
|
+- Админ помнит что "где-то было 192.168..." → вводит → находит
|
|
|
+- Не нужно помнить в какой колонке искать
|
|
|
+- Один поисковый запрос вместо 15 фильтров
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+✅ ТАЩИМ - универсальное поисковое поле с поиском по всем полям
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### B. Компактная таблица с максимумом информации
|
|
|
+
|
|
|
+**Что делает:**
|
|
|
+- Вся важная информация видна сразу в таблице
|
|
|
+- Не нужно кликать на строку чтобы увидеть детали
|
|
|
+- Все 15 полей конфига видны в одной строке
|
|
|
+
|
|
|
+**Колонки:**
|
|
|
+```
|
|
|
+# MAC time wf_ssid wf_psk ovpn_flag ovpn_addr wf_flag wf_addr bt_flag bt_addr fw_flag fw_addr reboot_flag ip
|
|
|
+1 ac:84:... 10:25 Office pass123 1 https:// 1 https:// 1 https:// 0 - 0 192.168.5.244
|
|
|
+```
|
|
|
+
|
|
|
+**Почему важно:**
|
|
|
+- Быстрый просмотр состояния всех устройств
|
|
|
+- Сравнение конфигов разных устройств
|
|
|
+- Видно кто онлайн (last seen), кто офлайн
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+⚠️ ЧАСТИЧНО - компактная таблица с основным, детали в модальном окне
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### C. Быстрое редактирование прямо в таблице (inline editing)
|
|
|
+
|
|
|
+**Что делает:**
|
|
|
+- Кликнул на ячейку → она превратилась в input
|
|
|
+- Изменил значение → нажал "Ok" → сохранено
|
|
|
+- Не нужно открывать формы, модальные окна
|
|
|
+- Изменение 1 поля = 2 клика (клик на ячейку + клик Ok)
|
|
|
+
|
|
|
+**Use case:**
|
|
|
+- Админ видит что у device #5 неправильный wf_ssid
|
|
|
+- Кликает на ячейку
|
|
|
+- Меняет "Office" на "Office_Guest"
|
|
|
+- Клик Ok → сохранено
|
|
|
+- Время: 5 секунд
|
|
|
+
|
|
|
+**Почему опасно:**
|
|
|
+- Случайный клик → случайное изменение
|
|
|
+- Нет подтверждения
|
|
|
+- Нет валидации
|
|
|
+- Нет истории кто и что изменил
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+❌ НЕ ТАЩИМ - слишком опасно, заменяем модальными окнами с подтверждением
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### D. Сортировка по любой колонке
|
|
|
+
|
|
|
+**Что делает:**
|
|
|
+- Клик на заголовок колонки → сортировка по возрастанию
|
|
|
+- Еще клик → сортировка по убыванию
|
|
|
+- Работает для всех колонок (кроме действий)
|
|
|
+
|
|
|
+**Use cases:**
|
|
|
+- Отсортировать по `time` → увидеть кто давно не подключался
|
|
|
+- Отсортировать по `MAC` → найти все устройства одного производителя
|
|
|
+- Отсортировать по `ip` → сгруппировать по подсетям
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+✅ ТАЩИМ - сортировка обязательно
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2. Clients Panel - Управление клиентами (организациями)
|
|
|
+
|
|
|
+#### A. Список клиентов с их устройствами
|
|
|
+
|
|
|
+**Что показывает:**
|
|
|
+```
|
|
|
+# Логин Пароль Устройства Оплачено до Информация Оплата
|
|
|
+1 shop@example.com 12345 ac:84:c6:42:17:90 2025-05-01 ИП Иванов, +79001234567 Оплачено до мая
|
|
|
+ cc:2d:e0:ca:9f:7e
|
|
|
+ d8:0d:17:5e:07:94
|
|
|
+
|
|
|
+2 mall@example.com 54321 b8:27:eb:c1:46:0e 2025-03-15 ТЦ "Мега", Петров Лимит 1000
|
|
|
+```
|
|
|
+
|
|
|
+**Ключевая фича:**
|
|
|
+- Сразу видно сколько устройств у клиента
|
|
|
+- MAC-адреса отображаются вертикально (каждый с новой строки)
|
|
|
+- Информация о клиенте и оплате в одной строке
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+✅ ТАЩИМ - список организаций с количеством устройств и статусом
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### B. Редактирование даты оплаты
|
|
|
+
|
|
|
+**Что делает:**
|
|
|
+- В колонке "Оплачено до" дата является ссылкой
|
|
|
+- Клик → переход на tilledit.php?id=123
|
|
|
+- Отдельная страница с календарем
|
|
|
+- Выбрать новую дату → сохранить
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+✅ ТАЩИМ - но через модальное окно, не отдельную страницу
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### C. Хранение доп. информации о клиенте
|
|
|
+
|
|
|
+**Что хранится:**
|
|
|
+- Юридическое лицо / ИП
|
|
|
+- Контактное лицо
|
|
|
+- Телефон
|
|
|
+- Email
|
|
|
+- Адрес
|
|
|
+- Комментарии
|
|
|
+
|
|
|
+**Формат:** Текстовое поле (textarea), админ пишет произвольный текст
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+✅ ТАЩИМ - но структурированно (отдельные поля, не текст)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3. Add Device Panel - Создание клиента с устройствами
|
|
|
+
|
|
|
+#### A. Одновременное создание клиента и привязка устройств
|
|
|
+
|
|
|
+**Workflow:**
|
|
|
+```
|
|
|
+1. Админ вводит:
|
|
|
+ - Логин клиента (email)
|
|
|
+ - Пароль
|
|
|
+ - Список MAC-адресов устройств (textarea, через ;)
|
|
|
+ Пример: d8:0d:17:5e:07:94;ac:84:c6:42:17:90;cc:2d:e0:ca:9f:7e;
|
|
|
+ - Информация о клиенте (textarea)
|
|
|
+ - Оплата (checkbox: оплачен / лимит 1000)
|
|
|
+
|
|
|
+2. Система создает:
|
|
|
+ - Запись в таблице `users` (клиент)
|
|
|
+ - Записи в таблице `user_devices` (связь клиент-устройство)
|
|
|
+ - Автоматически все MAC из списка привязываются к клиенту
|
|
|
+```
|
|
|
+
|
|
|
+**Почему удобно:**
|
|
|
+- Не нужно сначала создать клиента, потом добавлять устройства по одному
|
|
|
+- Массовое добавление устройств (скопировал список MAC → вставил → готово)
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+✅ ТАЩИМ - форма создания организации с возможностью сразу добавить несколько устройств
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### B. Простой контроль оплаты/лимитов
|
|
|
+
|
|
|
+**Логика:**
|
|
|
+- Checkbox "Оплачен"
|
|
|
+- Если НЕ оплачен → устанавливается лимит 1000 (детекций/событий)
|
|
|
+- Поле "Оплачено до" (дата)
|
|
|
+- Поле "Оплата" (текст статуса)
|
|
|
+
|
|
|
+**Use case:**
|
|
|
+- Клиент на пробном периоде → checkbox снят → лимит 1000
|
|
|
+- Клиент оплатил → checkbox включен → безлимит
|
|
|
+- Админ видит когда заканчивается период → продлевает
|
|
|
+
|
|
|
+**Решение для нового проекта:**
|
|
|
+⚠️ ЧАСТИЧНО - заменяем на статусы организации: `pending`, `active`, `suspended`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ЧТО ТАЩИМ В НОВЫЙ ПРОЕКТ
|
|
|
+
|
|
|
+### Обязательные фичи (Must Have)
|
|
|
+
|
|
|
+#### 1. ✅ Универсальный поиск
|
|
|
+
|
|
|
+**Где:**
|
|
|
+- Devices panel
|
|
|
+- Organizations panel
|
|
|
+- Users panel
|
|
|
+
|
|
|
+**Реализация:**
|
|
|
+```
|
|
|
+Поисковое поле → backend ищет по полям:
|
|
|
+- Devices: MAC, simple_id, organization.name, organization.owner_email, IP
|
|
|
+- Organizations: name, contact_email, owner.email
|
|
|
+- Users: email, full_name, organization.name
|
|
|
+```
|
|
|
+
|
|
|
+**Фичи:**
|
|
|
+- Debounce 300ms (не слать запрос на каждый символ)
|
|
|
+- Минимум 2 символа для поиска
|
|
|
+- Подсветка найденного (опционально)
|
|
|
+- Placeholder с примерами: "Поиск по MAC, организации, владельцу..."
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ✅ Компактная таблица с сортировкой
|
|
|
+
|
|
|
+**Devices:**
|
|
|
+```
|
|
|
+Simple ID | MAC Address | Organization | Status | Last Seen | Actions
|
|
|
+ #1 | ac:84:c6:... | Shop LLC | online | 2 min ago | Edit Delete
|
|
|
+ #2 | cc:2d:e0:... | Mall Inc | offline | 5 days ago | Edit Delete
|
|
|
+```
|
|
|
+
|
|
|
+**Сортировка:**
|
|
|
+- По Simple ID (default: newest first)
|
|
|
+- По Organization (alphabetically)
|
|
|
+- По Status (online → offline → error)
|
|
|
+- По Last Seen (newest → oldest)
|
|
|
+
|
|
|
+**Клик на заголовок** → меняет сортировку
|
|
|
+**Индикатор направления** → стрелка ↑↓
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ✅ Создание организации с устройствами
|
|
|
+
|
|
|
+**Форма Create Organization:**
|
|
|
+
|
|
|
+**Шаг 1: Информация об организации**
|
|
|
+```
|
|
|
+Name: ________________
|
|
|
+Contact Email: ________
|
|
|
+Owner Email: __________
|
|
|
+WiFi Enabled: ☐
|
|
|
+BLE Enabled: ☐
|
|
|
+```
|
|
|
+
|
|
|
+**Шаг 2: Добавить устройства (опционально)**
|
|
|
+```
|
|
|
+⊞ Add Device
|
|
|
+
|
|
|
+MAC Address: __________________ [Remove]
|
|
|
+MAC Address: __________________ [Remove]
|
|
|
+MAC Address: __________________ [Remove]
|
|
|
+
|
|
|
+[+ Add Another Device]
|
|
|
+```
|
|
|
+
|
|
|
+**Или массовое добавление:**
|
|
|
+```
|
|
|
+☐ Bulk import from list
|
|
|
+
|
|
|
+Paste MAC addresses (one per line or separated by ; or ,):
|
|
|
+┌─────────────────────────────────────┐
|
|
|
+│ ac:84:c6:42:17:90 │
|
|
|
+│ cc:2d:e0:ca:9f:7e;d8:0d:17:5e:07:94│
|
|
|
+│ b8:27:eb:c1:46:0e │
|
|
|
+└─────────────────────────────────────┘
|
|
|
+
|
|
|
+[Parse & Add] → "Found 4 MAC addresses"
|
|
|
+```
|
|
|
+
|
|
|
+**Кнопки:**
|
|
|
+```
|
|
|
+[Create Organization] [Cancel]
|
|
|
+```
|
|
|
+
|
|
|
+**После создания:**
|
|
|
+- Organization создана
|
|
|
+- Devices добавлены и автоматически привязаны
|
|
|
+- Owner получает email с доступом
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 4. ✅ Статусы и индикаторы
|
|
|
+
|
|
|
+**Device status badges:**
|
|
|
+```css
|
|
|
+.status-online → зеленый (#10b981)
|
|
|
+.status-offline → серый (#6b7280)
|
|
|
+.status-error → красный (#ef4444)
|
|
|
+```
|
|
|
+
|
|
|
+**Last seen human-readable:**
|
|
|
+```
|
|
|
+2 minutes ago
|
|
|
+5 hours ago
|
|
|
+3 days ago
|
|
|
+Never (для новых устройств)
|
|
|
+```
|
|
|
+
|
|
|
+**Organization status:**
|
|
|
+```
|
|
|
+pending → желтый (ожидает активации superadmin)
|
|
|
+active → зеленый (работает)
|
|
|
+suspended → красный (заблокирована)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 5. ✅ Модальные окна для редактирования
|
|
|
+
|
|
|
+**Вместо inline editing:**
|
|
|
+
|
|
|
+**Edit Device Modal:**
|
|
|
+```
|
|
|
+┌─────────────────────────────────────┐
|
|
|
+│ Edit Device #5 │
|
|
|
+│ │
|
|
|
+│ MAC Address: ac:84:c6:42:17:90 │
|
|
|
+│ (read-only, не редактируется) │
|
|
|
+│ │
|
|
|
+│ Organization: [Dropdown ▼] │
|
|
|
+│ → Shop LLC │
|
|
|
+│ → Mall Inc │
|
|
|
+│ → Not assigned │
|
|
|
+│ │
|
|
|
+│ Status: [Dropdown ▼] │
|
|
|
+│ → Online │
|
|
|
+│ → Offline │
|
|
|
+│ → Error │
|
|
|
+│ │
|
|
|
+│ Config (JSON): │
|
|
|
+│ ┌─────────────────────────┐ │
|
|
|
+│ │ { │ │
|
|
|
+│ │ "wifi_enabled": true │ │
|
|
|
+│ │ } │ │
|
|
|
+│ └─────────────────────────┘ │
|
|
|
+│ │
|
|
|
+│ [Save Changes] [Cancel] │
|
|
|
+└─────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+**Преимущества:**
|
|
|
+- Валидация перед сохранением
|
|
|
+- Подтверждение действия
|
|
|
+- Невозможно случайно изменить
|
|
|
+- Audit log: кто, когда, что изменил
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Опциональные фичи (Nice to Have)
|
|
|
+
|
|
|
+#### 6. ⚠️ Bulk actions (массовые действия)
|
|
|
+
|
|
|
+**Что:**
|
|
|
+- Checkbox в каждой строке таблицы
|
|
|
+- Checkbox "Select All" в заголовке
|
|
|
+- Панель массовых действий появляется при выборе
|
|
|
+
|
|
|
+**Пример:**
|
|
|
+```
|
|
|
+☑ Select All (5 selected)
|
|
|
+
|
|
|
+[Change Organization ▼] [Delete Selected] [Export CSV]
|
|
|
+```
|
|
|
+
|
|
|
+**Use cases:**
|
|
|
+- Выбрать 10 устройств → переназначить в другую организацию
|
|
|
+- Выбрать 5 офлайн устройств → удалить
|
|
|
+- Выбрать все → экспорт в Excel
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 7. ⚠️ Фильтры по статусу (chips)
|
|
|
+
|
|
|
+**Вместо dropdown:**
|
|
|
+
|
|
|
+```
|
|
|
+Status: [All] [Online] [Offline] [Error]
|
|
|
+ ───── (123) (45) (2)
|
|
|
+```
|
|
|
+
|
|
|
+**Клик на chip** → фильтрует таблицу
|
|
|
+**Счетчик** → показывает количество устройств в каждом статусе
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 8. ⚠️ Экспорт в CSV/Excel
|
|
|
+
|
|
|
+**Кнопка:** "Export to Excel"
|
|
|
+
|
|
|
+**Что экспортирует:**
|
|
|
+- Текущий отфильтрованный список (с учетом поиска)
|
|
|
+- Все колонки таблицы
|
|
|
+- Формат: .xlsx (Excel) или .csv
|
|
|
+
|
|
|
+**Use case:**
|
|
|
+- Админ хочет отчет по всем устройствам организации "Shop LLC"
|
|
|
+- Поиск "Shop LLC" → 10 devices найдено
|
|
|
+- Export → получает Excel с 10 строками
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ЧТО НЕ ТАЩИМ
|
|
|
+
|
|
|
+### ❌ 1. Inline editing
|
|
|
+
|
|
|
+**Почему НЕ тащим:**
|
|
|
+- Опасно (случайные изменения)
|
|
|
+- Нет валидации
|
|
|
+- Нет подтверждения
|
|
|
+- Нет audit trail
|
|
|
+
|
|
|
+**Замена:** Модальные окна
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ❌ 2. OpenVPN туннели
|
|
|
+
|
|
|
+**Почему НЕ тащим:**
|
|
|
+- По ТЗ: "Сейчас у нас VPN не будет"
|
|
|
+- Убираем:
|
|
|
+ - `ovpn_flag`
|
|
|
+ - `ovpn_addr`
|
|
|
+ - VPN страницу
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ❌ 3. Множественные флаги и URL в БД
|
|
|
+
|
|
|
+**Legacy имело:**
|
|
|
```sql
|
|
|
-CREATE TABLE `users` (
|
|
|
- `id` int(11) PRIMARY KEY AUTO_INCREMENT,
|
|
|
- `login` text NOT NULL,
|
|
|
- `password` text NOT NULL,
|
|
|
- `device` text NOT NULL, -- MAC-адреса через ";" (b8:27:eb:c1:46:0e;cc:2d:e0:ca:9f:7e;)
|
|
|
- `addTime` timestamp DEFAULT current_timestamp(),
|
|
|
- `dopinfo` text, -- Доп. информация о клиенте
|
|
|
- `paidTill` text, -- Текст об оплате
|
|
|
- `folder` text, -- Папка для файлов (не использовалось)
|
|
|
- `till` date DEFAULT '2022-05-03', -- Дата оплаты до
|
|
|
- `dopfolder` varchar(128) -- Доп. папка (не использовалось)
|
|
|
-);
|
|
|
-```
|
|
|
-
|
|
|
-### Таблица `user_devices` (связь)
|
|
|
+wf_flag tinyint(1)
|
|
|
+wf_addr varchar(60) -- "https://beacon.e-bash.ru/wifi_receiver.php"
|
|
|
+bt_flag tinyint(1)
|
|
|
+bt_addr varchar(60) -- "https://beacon.e-bash.ru/newfilebeacon.php"
|
|
|
+fw_flag tinyint(1)
|
|
|
+fw_addr varchar(60) -- "https://beacon.e-bash.ru/fw_update.php?mac="
|
|
|
+```
|
|
|
+
|
|
|
+**Проблемы:**
|
|
|
+- Хардкод URL в БД
|
|
|
+- Для изменения URL нужно обновить все записи
|
|
|
+- Нет версионирования API
|
|
|
+
|
|
|
+**Новый подход:**
|
|
|
+- Модульность на уровне Organization: `wifi_enabled`, `ble_enabled`
|
|
|
+- Единые API endpoints (не хардкод в БД):
|
|
|
+ - `/api/v1/device/config` - получить конфиг
|
|
|
+ - `/api/v1/device/wifi-events` - отправить WiFi данные
|
|
|
+ - `/api/v1/device/ble-events` - отправить BLE данные
|
|
|
+- Device config как JSONB для гибкости
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ❌ 4. WiFi credentials в таблице устройств
|
|
|
+
|
|
|
+**Legacy:**
|
|
|
```sql
|
|
|
-CREATE TABLE `user_devices` (
|
|
|
- `mac` varchar(40),
|
|
|
- `name` varchar(255),
|
|
|
- `user_id` int(11),
|
|
|
- `max1000` tinyint(1), -- Лимит (0=оплачен, 1=лимит 1000)
|
|
|
- `segment` tinyint(1) -- Для Yandex.Аудитории
|
|
|
-);
|
|
|
+wf_client_ssid varchar(60) -- WiFi SSID
|
|
|
+wf_client_psk varchar(60) -- WiFi пароль (plain text!)
|
|
|
```
|
|
|
|
|
|
-## Навигация (Legacy)
|
|
|
+**Проблемы:**
|
|
|
+- Пароли в открытом виде
|
|
|
+- Одинаковые для всех устройств организации
|
|
|
+- При смене пароля нужно обновлять все devices
|
|
|
+
|
|
|
+**Новый подход:**
|
|
|
+- WiFi credentials на уровне Organization (в `config` JSONB)
|
|
|
+- Шифрование паролей
|
|
|
+- Device получает через защищенный API endpoint
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
+### ❌ 5. Текстовые связи через разделители
|
|
|
+
|
|
|
+**Legacy:**
|
|
|
+```sql
|
|
|
+device text NOT NULL -- "b8:27:eb:c1:46:0e;cc:2d:e0:ca:9f:7e;d8:0d:17:5e:07:94;"
|
|
|
```
|
|
|
-1. Устройства (index.php) - главная страница, список всех устройств
|
|
|
-2. Добавление (adddev.php) - форма добавления клиента с устройствами
|
|
|
-3. Клиенты (clients.php) - список клиентов
|
|
|
-4. Инструкция (info.php) - документация
|
|
|
-5. VPN (внешняя ссылка) - сканер VPN подключений
|
|
|
+
|
|
|
+**Проблемы:**
|
|
|
+- Невозможно сделать JOIN
|
|
|
+- Невозможно сделать foreign key constraint
|
|
|
+- Невозможно индексировать
|
|
|
+- Парсинг строки каждый раз
|
|
|
+
|
|
|
+**Новый подход:**
|
|
|
+- Нормализованная БД
|
|
|
+- Таблица `devices` с `organization_id` (foreign key)
|
|
|
+- Один device = одна строка
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ❌ 6. Пароли в открытом виде
|
|
|
+
|
|
|
+**Legacy:**
|
|
|
+```sql
|
|
|
+password text NOT NULL -- "12345" (plain text)
|
|
|
```
|
|
|
|
|
|
-## Функционал Devices Panel (index.php)
|
|
|
+**Новый подход:**
|
|
|
+- `hashed_password` varchar(255)
|
|
|
+- bcrypt с salt
|
|
|
+- Минимум 8 символов, complexity requirements
|
|
|
|
|
|
-### Таблица устройств
|
|
|
+---
|
|
|
|
|
|
-**Колонки:**
|
|
|
-1. `#` - порядковый номер
|
|
|
-2. `MAC-адрес` - читаемый (не редактируемый)
|
|
|
-3. `время` - last seen (не редактируемый)
|
|
|
-4. `wf_ssid` - **РЕДАКТИРУЕМАЯ** (inline editing)
|
|
|
-5. `wf_psk` - **РЕДАКТИРУЕМАЯ**
|
|
|
-6. `ovpn_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
-7. `ovpn_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
-8. `wf_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
-9. `wf_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
-10. `bt_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
-11. `bt_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
-12. `fw_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
-13. `fw_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
-14. `reboot_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
-15. `ip` - **РЕДАКТИРУЕМАЯ**
|
|
|
-
|
|
|
-### Ключевая фича: Inline Editing
|
|
|
-
|
|
|
-**Библиотека**: [jEditable](http://www.appelsiini.net/projects/jeditable)
|
|
|
-
|
|
|
-**Как работает:**
|
|
|
-1. Пользователь кликает на ячейку таблицы (с классом `.editable-td`)
|
|
|
-2. Ячейка превращается в `<input>` с кнопками "Ok" и "Cancel"
|
|
|
-3. При нажатии "Ok" отправляется AJAX запрос:
|
|
|
- ```javascript
|
|
|
- $.get('savewf.php', {
|
|
|
- p1: value, // новое значение
|
|
|
- p2: $(this).attr('id'), // ID ячейки (имя поля)
|
|
|
- p3: $(this).parent('tr').attr('id') // ID строки (id записи)
|
|
|
- })
|
|
|
- ```
|
|
|
-4. Backend (`savewf.php`) выполняет SQL:
|
|
|
- ```sql
|
|
|
- UPDATE `configs` SET `{field_name}` = '{value}' WHERE `id`='{id}'
|
|
|
- ```
|
|
|
-5. Ячейка возвращается к обычному виду с новым значением
|
|
|
-
|
|
|
-### Поиск и фильтрация
|
|
|
-
|
|
|
-**Библиотека**: [DataTables](https://datatables.net/)
|
|
|
-
|
|
|
-**Функционал:**
|
|
|
-- Глобальный поиск по **всем** колонкам таблицы
|
|
|
-- Пользователь вводит любой текст в поле поиска
|
|
|
-- DataTables фильтрует строки в реальном времени
|
|
|
-- Работает по: MAC, SSID, IP, адресам, времени, флагам
|
|
|
-
|
|
|
-**Код:**
|
|
|
-```javascript
|
|
|
-var editableTable = exampleDatatable.dataTable({
|
|
|
- order: [[ 1, 'desc' ]], // Сортировка по ID по убыванию
|
|
|
- columnDefs: [ { orderable: false, targets: [ 0 ] } ]
|
|
|
-});
|
|
|
-$('.dataTables_filter input').attr('placeholder', 'Search');
|
|
|
-```
|
|
|
-
|
|
|
-### Сортировка
|
|
|
-
|
|
|
-- Сортировка по любой колонке (кроме первой)
|
|
|
-- По умолчанию: по ID (времени регистрации) по убыванию
|
|
|
-
|
|
|
-## Функционал Clients Panel (clients.php)
|
|
|
-
|
|
|
-### Таблица клиентов
|
|
|
+### ❌ 7. Отсутствие аутентификации в админке
|
|
|
|
|
|
-**Колонки:**
|
|
|
-1. `#` - порядковый номер
|
|
|
-2. `Логин`
|
|
|
-3. `Пароль`
|
|
|
-4. `Устройства` - MAC-адреса через `<br>` (разделены по `;`)
|
|
|
-5. `Оплачено до` - ссылка на редактирование даты
|
|
|
-6. `Информация` - допинфо о клиенте
|
|
|
-7. `Оплата` - текст статуса оплаты
|
|
|
-
|
|
|
-### Особенности
|
|
|
-
|
|
|
-- Список устройств выводится вертикально (`implode('<br>', explode(';', $line['device']))`)
|
|
|
-- Ссылка на редактирование даты: `<a href="tilledit.php?id={id}">{till}</a>`
|
|
|
-- Нет inline editing (в отличие от devices)
|
|
|
-- Только чтение данных
|
|
|
-
|
|
|
-## Функционал Add Device (adddev.php)
|
|
|
-
|
|
|
-### Форма создания клиента
|
|
|
-
|
|
|
-**Поля:**
|
|
|
-1. `Логин` - required
|
|
|
-2. `Пароль` - required
|
|
|
-3. `Список устройств` - textarea, через `;` в конце
|
|
|
- Пример: `d8:0d:17:5e:07:94;ac:84:c6:42:17:90;`
|
|
|
-4. `Информация о клиенте` - textarea (юрлицо, имя, телефон, email)
|
|
|
-5. `Оплата` - textarea (текст до какого числа оплачено)
|
|
|
-6. `Оплачен` - checkbox (max1000: 0=оплачен, 1=лимит)
|
|
|
-
|
|
|
-### Логика создания
|
|
|
-
|
|
|
-```php
|
|
|
-// 1. Валидация
|
|
|
-if (login == null || password == null || device == null) {
|
|
|
- error("Поля логин, пароль и устройства не могут быть пустыми");
|
|
|
-}
|
|
|
-
|
|
|
-// 2. Проверка существования логина
|
|
|
-$query = "SELECT * FROM `users` WHERE `login` = '{login}'";
|
|
|
-if (row_count > 0) {
|
|
|
- error("Логин уже занят");
|
|
|
-}
|
|
|
-
|
|
|
-// 3. Создание пользователя
|
|
|
-INSERT INTO `users` (login, password, device, dopinfo, paidTill, till)
|
|
|
-VALUES ('{login}', '{password}', '{device}', '{dopinfo}', '{paidTill}', CURDATE());
|
|
|
-
|
|
|
-$user_id = mysqli_insert_id();
|
|
|
-
|
|
|
-// 4. Создание связей user_devices
|
|
|
-$macs = explode(";", device);
|
|
|
-foreach ($macs as $mac) {
|
|
|
- INSERT INTO `user_devices` (mac, name, user_id, max1000, segment)
|
|
|
- VALUES ('{mac}', '{mac}', {user_id}, {max1000}, 0);
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-## Проблемы Legacy кода
|
|
|
-
|
|
|
-### Безопасность
|
|
|
-
|
|
|
-1. **SQL Injection** - прямая конкатенация в запросах:
|
|
|
- ```php
|
|
|
- $query = "UPDATE `configs` SET `".$_REQUEST['p2']."` = '".$_REQUEST['p1']."' WHERE `id`='".$_REQUEST['p3']."'";
|
|
|
- ```
|
|
|
-2. **XSS** - нет экранирования вывода
|
|
|
-3. **Пароли в открытом виде** - хранятся как plain text
|
|
|
-4. **Нет CSRF защиты**
|
|
|
-5. **Нет валидации типов данных**
|
|
|
-
|
|
|
-### Архитектура
|
|
|
-
|
|
|
-1. **Нет разделения ролей** - все пользователи видят всё
|
|
|
-2. **Нет multi-tenant изоляции** - одна таблица на всех
|
|
|
-3. **Нет аутентификации в админке** - доступ без проверки
|
|
|
-4. **Хардкод credentials** в каждом файле:
|
|
|
- ```php
|
|
|
- $dbname = 'wifi';
|
|
|
- $dbuser = 'p328882_wifi';
|
|
|
- $dbpass = '0354c0598ld';
|
|
|
- ```
|
|
|
-5. **Связь many-to-many через текст** - MAC-адреса через `;`
|
|
|
-
|
|
|
-### UX
|
|
|
-
|
|
|
-1. **Inline editing опасен** - легко случайно изменить данные
|
|
|
-2. **Нет подтверждения удаления**
|
|
|
-3. **Нет истории изменений** (audit logs)
|
|
|
-4. **Нет валидации MAC-адресов**
|
|
|
-5. **Нет статусов устройств** (online/offline/error)
|
|
|
-
|
|
|
-## Анализ требований для нового проекта
|
|
|
-
|
|
|
-### Что взять из Legacy
|
|
|
-
|
|
|
-#### 1. **Универсальный поиск по всем полям** ⭐ КЛЮЧЕВАЯ ФИЧА
|
|
|
+**Legacy:**
|
|
|
+- Нет проверки кто заходит в админку
|
|
|
+- Любой кто знает URL может зайти
|
|
|
|
|
|
-**Почему важно:**
|
|
|
-- Быстро найти устройство по MAC
|
|
|
-- Найти устройства организации
|
|
|
-- Найти по IP адресу
|
|
|
-- Найти по owner/admin email
|
|
|
+**Новый подход:**
|
|
|
+- JWT аутентификация
|
|
|
+- Role-based access control
|
|
|
+- Session management
|
|
|
+- Audit logging
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ПРИОРИТЕТЫ РЕАЛИЗАЦИИ
|
|
|
|
|
|
-**Реализация в новом проекте:**
|
|
|
+### ✅ Уже есть (MVP)
|
|
|
+
|
|
|
+1. CRUD устройств с модальными окнами
|
|
|
+2. CRUD организаций
|
|
|
+3. CRUD пользователей
|
|
|
+4. JWT аутентификация
|
|
|
+5. Multi-tenant изоляция
|
|
|
+6. RBAC (6 ролей)
|
|
|
+7. Базовая таблица с сортировкой
|
|
|
+
|
|
|
+### 🔨 TODO Шаг 1: Поиск (приоритет HIGH)
|
|
|
|
|
|
**Backend:**
|
|
|
```python
|
|
|
-# backend/app/api/v1/superadmin/devices.py или client/devices.py
|
|
|
-@router.get("/devices", response_model=DeviceListResponse)
|
|
|
+@router.get("/devices")
|
|
|
async def get_devices(
|
|
|
search: Optional[str] = None,
|
|
|
organization_id: Optional[int] = None,
|
|
|
+ status: Optional[str] = None,
|
|
|
+ sort_by: str = "simple_id",
|
|
|
+ sort_order: str = "desc",
|
|
|
offset: int = 0,
|
|
|
- limit: int = 50,
|
|
|
- db: AsyncSession = Depends(get_db),
|
|
|
- current_user: User = Depends(get_current_user)
|
|
|
+ limit: int = 50
|
|
|
):
|
|
|
query = select(Device)
|
|
|
|
|
|
- # Фильтр по организации (для multi-tenant)
|
|
|
- if current_user.role != "superadmin":
|
|
|
- query = query.where(Device.organization_id == current_user.organization_id)
|
|
|
- elif organization_id:
|
|
|
- query = query.where(Device.organization_id == organization_id)
|
|
|
-
|
|
|
- # Универсальный поиск
|
|
|
+ # Search filter
|
|
|
if search:
|
|
|
- search_filter = or_(
|
|
|
- Device.mac_address.ilike(f"%{search}%"),
|
|
|
- Device.simple_id.cast(String).ilike(f"%{search}%"),
|
|
|
- Organization.name.ilike(f"%{search}%"),
|
|
|
- User.email.ilike(f"%{search}%"),
|
|
|
- User.full_name.ilike(f"%{search}%")
|
|
|
- )
|
|
|
- query = (
|
|
|
- query
|
|
|
- .outerjoin(Organization)
|
|
|
- .outerjoin(User, User.organization_id == Device.organization_id)
|
|
|
- .where(search_filter)
|
|
|
+ query = query.join(Organization).where(
|
|
|
+ or_(
|
|
|
+ Device.mac_address.ilike(f"%{search}%"),
|
|
|
+ cast(Device.simple_id, String).ilike(f"%{search}%"),
|
|
|
+ Organization.name.ilike(f"%{search}%"),
|
|
|
+ Organization.contact_email.ilike(f"%{search}%")
|
|
|
+ )
|
|
|
)
|
|
|
|
|
|
+ # Status filter
|
|
|
+ if status:
|
|
|
+ query = query.where(Device.status == status)
|
|
|
+
|
|
|
+ # Sorting
|
|
|
+ if sort_order == "asc":
|
|
|
+ query = query.order_by(asc(getattr(Device, sort_by)))
|
|
|
+ else:
|
|
|
+ query = query.order_by(desc(getattr(Device, sort_by)))
|
|
|
+
|
|
|
total = await db.scalar(select(func.count()).select_from(query.subquery()))
|
|
|
devices = await db.scalars(query.offset(offset).limit(limit))
|
|
|
|
|
|
- return DeviceListResponse(devices=devices.all(), total=total)
|
|
|
+ return {"devices": devices.all(), "total": total}
|
|
|
```
|
|
|
|
|
|
-**Frontend (Vue 3):**
|
|
|
+**Frontend:**
|
|
|
```vue
|
|
|
-<!-- frontend/src/views/superadmin/DevicesView.vue -->
|
|
|
-<template>
|
|
|
- <div class="page">
|
|
|
- <div class="page-header">
|
|
|
- <h1>{{ $t('devices.title') }}</h1>
|
|
|
-
|
|
|
- <!-- Поле поиска -->
|
|
|
- <div class="search-box">
|
|
|
- <input
|
|
|
- v-model="searchQuery"
|
|
|
- @input="debouncedSearch"
|
|
|
- :placeholder="$t('devices.searchPlaceholder')"
|
|
|
- type="text"
|
|
|
- />
|
|
|
- <i class="search-icon">🔍</i>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <table class="data-table">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>{{ $t('devices.simpleId') }}</th>
|
|
|
- <th>{{ $t('devices.macAddress') }}</th>
|
|
|
- <th>{{ $t('devices.organization') }}</th>
|
|
|
- <th>{{ $t('common.status') }}</th>
|
|
|
- <th>{{ $t('devices.lastSeen') }}</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr v-for="device in devices" :key="device.id">
|
|
|
- <td>#{{ device.simple_id }}</td>
|
|
|
- <td><code>{{ device.mac_address }}</code></td>
|
|
|
- <td>{{ getOrganizationName(device.organization_id) }}</td>
|
|
|
- <td><span class="badge" :class="`status-${device.status}`">{{ device.status }}</span></td>
|
|
|
- <td>{{ formatDate(device.last_seen_at) }}</td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
-
|
|
|
- <!-- Пагинация -->
|
|
|
- <div class="pagination">
|
|
|
- <button @click="prevPage" :disabled="offset === 0">Prev</button>
|
|
|
- <span>{{ currentPage }} / {{ totalPages }}</span>
|
|
|
- <button @click="nextPage" :disabled="!hasMore">Next</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, computed } from 'vue'
|
|
|
-import { debounce } from 'lodash-es'
|
|
|
-import devicesApi from '@/api/devices'
|
|
|
-
|
|
|
-const devices = ref([])
|
|
|
-const searchQuery = ref('')
|
|
|
-const offset = ref(0)
|
|
|
-const limit = ref(50)
|
|
|
-const total = ref(0)
|
|
|
-
|
|
|
-const hasMore = computed(() => offset.value + limit.value < total.value)
|
|
|
-const totalPages = computed(() => Math.ceil(total.value / limit.value))
|
|
|
-const currentPage = computed(() => Math.floor(offset.value / limit.value) + 1)
|
|
|
-
|
|
|
-async function loadDevices() {
|
|
|
- try {
|
|
|
- const response = await devicesApi.getAllSuperadmin({
|
|
|
- search: searchQuery.value,
|
|
|
- offset: offset.value,
|
|
|
- limit: limit.value
|
|
|
- })
|
|
|
- devices.value = response.devices
|
|
|
- total.value = response.total
|
|
|
- } catch (err) {
|
|
|
- console.error(err)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const debouncedSearch = debounce(() => {
|
|
|
- offset.value = 0 // Reset to first page
|
|
|
- loadDevices()
|
|
|
-}, 300) // 300ms delay
|
|
|
-
|
|
|
-function nextPage() {
|
|
|
- offset.value += limit.value
|
|
|
- loadDevices()
|
|
|
-}
|
|
|
-
|
|
|
-function prevPage() {
|
|
|
- offset.value = Math.max(0, offset.value - limit.value)
|
|
|
- loadDevices()
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(loadDevices)
|
|
|
-</script>
|
|
|
-```
|
|
|
-
|
|
|
-**Переводы (i18n):**
|
|
|
-```javascript
|
|
|
-// frontend/src/i18n/index.js
|
|
|
-const messages = {
|
|
|
- ru: {
|
|
|
- devices: {
|
|
|
- searchPlaceholder: 'Поиск по MAC, организации, владельцу...'
|
|
|
- }
|
|
|
- },
|
|
|
- en: {
|
|
|
- devices: {
|
|
|
- searchPlaceholder: 'Search by MAC, organization, owner...'
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-#### 2. **Быстрый просмотр в таблице**
|
|
|
-
|
|
|
-- Компактная таблица с основной информацией
|
|
|
-- Сортировка по любой колонке
|
|
|
-- Pagination для больших списков
|
|
|
-
|
|
|
-#### 3. **Inline индикаторы**
|
|
|
-
|
|
|
-- Статус устройства (online/offline/error) - цветные badges
|
|
|
-- Дата last_seen с человекочитаемым форматом
|
|
|
-- Organization name вместо ID
|
|
|
-
|
|
|
-### Что НЕ брать из Legacy
|
|
|
-
|
|
|
-#### 1. **Inline Editing** ❌
|
|
|
-
|
|
|
-**Проблемы:**
|
|
|
-- Случайные изменения (кликнул мимо)
|
|
|
-- Нет валидации
|
|
|
-- Нет подтверждения
|
|
|
-- Нет истории изменений
|
|
|
-
|
|
|
-**Решение:** Модальные окна для редактирования (как сейчас)
|
|
|
-
|
|
|
-#### 2. **Текстовые связи (MAC через `;`)** ❌
|
|
|
+<input
|
|
|
+ v-model="searchQuery"
|
|
|
+ @input="debouncedSearch"
|
|
|
+ placeholder="Поиск по MAC, организации, email..."
|
|
|
+ class="search-input"
|
|
|
+/>
|
|
|
+```
|
|
|
|
|
|
-**Проблема:** `device` text NOT NULL - "b8:27:eb:c1:46:0e;cc:2d:e0:ca:9f:7e;"
|
|
|
+### 🔨 TODO Шаг 2: Bulk import устройств (приоритет MEDIUM)
|
|
|
|
|
|
-**Решение:** Нормализованная БД с foreign keys (как сейчас)
|
|
|
+**Форма Create Organization:**
|
|
|
|
|
|
-#### 3. **OpenVPN туннели** ❌
|
|
|
+Добавить секцию:
|
|
|
+```
|
|
|
+☐ Bulk add devices
|
|
|
|
|
|
-**Из ТЗ:** "Сейчас у нас VPN не будет"
|
|
|
+Paste MAC addresses:
|
|
|
+┌────────────────────────────┐
|
|
|
+│ ac:84:c6:42:17:90 │
|
|
|
+│ cc:2d:e0:ca:9f:7e │
|
|
|
+│ d8:0d:17:5e:07:94 │
|
|
|
+└────────────────────────────┘
|
|
|
|
|
|
-**Убираем:**
|
|
|
-- `ovpn_flag`
|
|
|
-- `ovpn_addr`
|
|
|
-- VPN страница в навигации
|
|
|
+Separator: [; or , or newline ▼]
|
|
|
|
|
|
-#### 4. **Множественные флаги и адреса** ❌
|
|
|
+[Parse] → "✓ Found 3 valid MAC addresses"
|
|
|
|
|
|
-**Legacy имело:**
|
|
|
-- `wf_flag` + `wf_addr` (WiFi)
|
|
|
-- `bt_flag` + `bt_addr` (BLE)
|
|
|
-- `fw_flag` + `fw_addr` (Firmware)
|
|
|
+[Create Organization with 3 devices]
|
|
|
+```
|
|
|
|
|
|
-**Новый подход:**
|
|
|
-- Модульность на уровне Organization: `wifi_enabled`, `ble_enabled`
|
|
|
-- Единый API endpoint вместо хардкод URL в БД
|
|
|
-- Config как JSONB для гибкости
|
|
|
+**Backend:**
|
|
|
+```python
|
|
|
+@router.post("/organizations")
|
|
|
+async def create_organization(
|
|
|
+ data: OrganizationCreateSchema,
|
|
|
+ devices: Optional[List[str]] = None # List of MAC addresses
|
|
|
+):
|
|
|
+ # Create organization
|
|
|
+ org = Organization(**data.dict())
|
|
|
+ db.add(org)
|
|
|
+ await db.flush()
|
|
|
+
|
|
|
+ # Create devices if provided
|
|
|
+ if devices:
|
|
|
+ for mac in devices:
|
|
|
+ device = Device(
|
|
|
+ mac_address=mac,
|
|
|
+ organization_id=org.id,
|
|
|
+ simple_id=await get_next_simple_id()
|
|
|
+ )
|
|
|
+ db.add(device)
|
|
|
+
|
|
|
+ await db.commit()
|
|
|
+ return org
|
|
|
+```
|
|
|
|
|
|
-#### 5. **Inline WiFi credentials** ❌
|
|
|
+### 🔨 TODO Шаг 3: Фильтры по статусу (приоритет LOW)
|
|
|
|
|
|
-**Legacy:** `wf_client_ssid` и `wf_client_psk` в таблице configs
|
|
|
+**UI:**
|
|
|
+```vue
|
|
|
+<div class="status-filters">
|
|
|
+ <button
|
|
|
+ v-for="status in ['all', 'online', 'offline', 'error']"
|
|
|
+ :class="{ active: selectedStatus === status }"
|
|
|
+ @click="selectedStatus = status"
|
|
|
+ >
|
|
|
+ {{ status }} ({{ counts[status] }})
|
|
|
+ </button>
|
|
|
+</div>
|
|
|
+```
|
|
|
|
|
|
-**Проблема:**
|
|
|
-- Пароли в открытом виде
|
|
|
-- Одинаковые для всех устройств организации
|
|
|
+**Backend:**
|
|
|
+```python
|
|
|
+@router.get("/devices/counts")
|
|
|
+async def get_device_counts():
|
|
|
+ counts = await db.execute(
|
|
|
+ select(
|
|
|
+ Device.status,
|
|
|
+ func.count(Device.id).label('count')
|
|
|
+ ).group_by(Device.status)
|
|
|
+ )
|
|
|
+ return {row.status: row.count for row in counts}
|
|
|
+```
|
|
|
|
|
|
-**Решение:**
|
|
|
-- Credentials на уровне Organization (в зашифрованном виде или в vault)
|
|
|
-- Device получает через защищенный API
|
|
|
-
|
|
|
-## Новая структура Devices Panel
|
|
|
-
|
|
|
-### Фильтры (обязательно)
|
|
|
-
|
|
|
-**Для новой админки:**
|
|
|
-
|
|
|
-1. **Глобальный поиск** (текстовое поле)
|
|
|
- - По MAC-адресу
|
|
|
- - По simple_id (#1, #2, #3)
|
|
|
- - По имени организации
|
|
|
- - По email владельца организации
|
|
|
-
|
|
|
-2. **Фильтр по организации** (dropdown для superadmin)
|
|
|
- - "Все организации"
|
|
|
- - "Не назначены"
|
|
|
- - Список организаций
|
|
|
-
|
|
|
-3. **Фильтр по статусу** (chips/badges)
|
|
|
- - Online
|
|
|
- - Offline
|
|
|
- - Error
|
|
|
-
|
|
|
-4. **Сортировка**
|
|
|
- - По Simple ID (по умолчанию)
|
|
|
- - По дате последнего подключения
|
|
|
- - По организации
|
|
|
- - По статусу
|
|
|
-
|
|
|
-### Таблица устройств
|
|
|
-
|
|
|
-**Колонки (минимум):**
|
|
|
-
|
|
|
-| Колонка | Описание | Для Superadmin | Для Client |
|
|
|
-|---------|----------|----------------|------------|
|
|
|
-| Simple ID | #1, #2, #3 | ✅ | ✅ |
|
|
|
-| MAC Address | `ac:84:c6:d4:9c:c4` | ✅ | ✅ |
|
|
|
-| Organization | Название организации | ✅ | ❌ (скрыта) |
|
|
|
-| Owner | Email владельца | ✅ | ✅ |
|
|
|
-| Status | online/offline/error | ✅ | ✅ |
|
|
|
-| Last Seen | "2 минуты назад" | ✅ | ✅ |
|
|
|
-| Actions | Edit, Delete | ✅ | Owner/Admin only |
|
|
|
-
|
|
|
-### Детали устройства (модальное окно или отдельная страница)
|
|
|
-
|
|
|
-**Общая информация:**
|
|
|
-- Simple ID
|
|
|
-- MAC Address
|
|
|
-- Organization
|
|
|
-- Status
|
|
|
-- Created At
|
|
|
-- Last Seen At
|
|
|
-- IP Address (если онлайн)
|
|
|
-
|
|
|
-**Конфигурация (JSONB):**
|
|
|
-```json
|
|
|
-{
|
|
|
- "wifi_enabled": true,
|
|
|
- "ble_enabled": true,
|
|
|
- "upload_interval": 300,
|
|
|
- "scan_interval": 60,
|
|
|
- "custom_settings": {}
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-**История подключений:**
|
|
|
-- Таблица последних 10 подключений
|
|
|
-- Timestamp, IP, Duration
|
|
|
-
|
|
|
-**Действия:**
|
|
|
-- Изменить организацию (superadmin)
|
|
|
-- Изменить статус
|
|
|
-- Удалить устройство (с подтверждением)
|
|
|
-- Просмотр audit logs
|
|
|
-
|
|
|
-## Сравнение Legacy vs Новый проект
|
|
|
-
|
|
|
-| Функция | Legacy | Новый проект |
|
|
|
-|---------|--------|--------------|
|
|
|
-| **Аутентификация** | ❌ Нет | ✅ JWT (access + refresh) |
|
|
|
-| **Multi-tenant** | ❌ Все в одной куче | ✅ Изоляция по organization_id |
|
|
|
-| **Роли** | ❌ Нет | ✅ 6 ролей (superadmin, owner, admin, manager, operator, viewer) |
|
|
|
-| **Поиск** | ✅ По всем полям | ✅ По MAC, org, user, simple_id |
|
|
|
-| **Редактирование** | ✅ Inline (опасно) | ✅ Модальные окна с валидацией |
|
|
|
-| **Simple ID** | ❌ Нет | ✅ Auto-increment (#1, #2, #3) |
|
|
|
-| **OpenVPN** | ✅ Было | ❌ Убрали |
|
|
|
-| **Audit Logs** | ❌ Нет | ✅ Полное логирование |
|
|
|
-| **Статусы** | ❌ Нет | ✅ online/offline/error |
|
|
|
-| **Пагинация** | ✅ DataTables | ✅ Backend pagination |
|
|
|
-| **i18n** | ❌ Только русский | ✅ RU/EN |
|
|
|
-
|
|
|
-## Приоритеты реализации
|
|
|
-
|
|
|
-### Шаг 1: Базовый функционал (уже есть)
|
|
|
-- ✅ CRUD устройств
|
|
|
-- ✅ Фильтр по организации
|
|
|
-- ✅ Статусы
|
|
|
-- ✅ Last seen
|
|
|
-
|
|
|
-### Шаг 2: Поиск (TODO)
|
|
|
-- [ ] Универсальное поле поиска
|
|
|
-- [ ] Backend endpoint с фильтрацией
|
|
|
-- [ ] Debounce для поиска
|
|
|
-- [ ] Подсветка результатов
|
|
|
-
|
|
|
-### Шаг 3: Детали устройства (TODO)
|
|
|
-- [ ] Модальное окно с полной информацией
|
|
|
-- [ ] История подключений
|
|
|
-- [ ] Редактирование конфига (JSONB)
|
|
|
-- [ ] Просмотр audit logs для устройства
|
|
|
-
|
|
|
-### Шаг 4: Улучшения UX (TODO)
|
|
|
-- [ ] Сортировка таблицы
|
|
|
-- [ ] Фильтр по статусу (chips)
|
|
|
-- [ ] Экспорт в CSV/Excel
|
|
|
-- [ ] Bulk actions (массовое удаление/изменение)
|
|
|
-
|
|
|
-## Выводы
|
|
|
-
|
|
|
-### Ключевые изменения от Legacy
|
|
|
-
|
|
|
-1. **Безопасность на первом месте:**
|
|
|
- - JWT аутентификация
|
|
|
- - RBAC система прав
|
|
|
- - SQL injection защита (SQLAlchemy)
|
|
|
- - Audit logging
|
|
|
-
|
|
|
-2. **Multi-tenant архитектура:**
|
|
|
- - Изоляция данных по организациям
|
|
|
- - Разные уровни доступа
|
|
|
-
|
|
|
-3. **UX улучшения:**
|
|
|
- - Simple ID вместо длинных MAC
|
|
|
- - Модальные окна вместо inline edit
|
|
|
- - Подтверждение опасных действий
|
|
|
- - i18n поддержка
|
|
|
-
|
|
|
-4. **Современный стек:**
|
|
|
- - FastAPI вместо PHP
|
|
|
- - Vue 3 вместо jQuery
|
|
|
- - PostgreSQL вместо MySQL
|
|
|
- - REST API вместо прямых SQL запросов
|
|
|
-
|
|
|
-### Что сохранили
|
|
|
-
|
|
|
-1. **Универсальный поиск** - ключевая фича для удобства
|
|
|
-2. **Компактная таблица** - быстрый просмотр списка
|
|
|
-3. **Статусы и индикаторы** - визуальная обратная связь
|
|
|
-4. **Пагинация** - для больших списков
|
|
|
-
|
|
|
-### Что убрали
|
|
|
-
|
|
|
-1. OpenVPN туннели (по ТЗ)
|
|
|
-2. Inline editing (небезопасно)
|
|
|
-3. Текстовые связи через `;`
|
|
|
-4. Хардкод URL в БД
|
|
|
-5. WiFi credentials в таблице устройств
|
|
|
+---
|
|
|
+
|
|
|
+## ИТОГО: Сравнение Legacy → New
|
|
|
+
|
|
|
+| Фича | Legacy | New Project | Status |
|
|
|
+|------|--------|-------------|--------|
|
|
|
+| **Универсальный поиск** | ✅ DataTables | ✅ Backend SQL | 🔨 TODO |
|
|
|
+| **Компактная таблица** | ✅ Все поля видны | ✅ Основные поля | ✅ Done |
|
|
|
+| **Сортировка** | ✅ По всем колонкам | ✅ По ключевым | ✅ Done |
|
|
|
+| **Inline editing** | ✅ jEditable | ❌ Убрали | - |
|
|
|
+| **Модальные окна** | ❌ Не было | ✅ Для редактирования | ✅ Done |
|
|
|
+| **Bulk add devices** | ✅ Textarea с ";" | ✅ Парсинг списка | 🔨 TODO |
|
|
|
+| **Multi-tenant** | ❌ Все в одной куче | ✅ Изоляция | ✅ Done |
|
|
|
+| **RBAC** | ❌ Нет ролей | ✅ 6 ролей | ✅ Done |
|
|
|
+| **Audit logs** | ❌ Нет | ✅ Полное логирование | ✅ Done |
|
|
|
+| **JWT auth** | ❌ Нет | ✅ Access + refresh | ✅ Done |
|
|
|
+| **Статусы** | ❌ Только last seen | ✅ online/offline/error | ✅ Done |
|
|
|
+| **Simple ID** | ❌ Только MAC | ✅ #1, #2, #3 | ✅ Done |
|
|
|
+| **OpenVPN** | ✅ Было | ❌ Убрали | - |
|
|
|
+| **WiFi creds в БД** | ✅ Plain text | ❌ API endpoint | ✅ Done |
|
|
|
+| **Пароли юзеров** | ❌ Plain text | ✅ Bcrypt hash | ✅ Done |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ВЫВОДЫ
|
|
|
+
|
|
|
+### Что сохранили (essence, не реализацию):
|
|
|
+
|
|
|
+1. ✅ **Универсальный поиск** - самая полезная фича для быстрой работы
|
|
|
+2. ✅ **Компактное отображение** - вся информация на виду
|
|
|
+3. ✅ **Быстрое создание** - организация + устройства одним действием
|
|
|
+4. ✅ **Сортировка** - удобный просмотр больших списков
|
|
|
+
|
|
|
+### Что выбросили:
|
|
|
+
|
|
|
+1. ❌ **Inline editing** - опасно, заменили на модальные окна
|
|
|
+2. ❌ **OpenVPN** - не нужно по ТЗ
|
|
|
+3. ❌ **Хардкод в БД** - заменили на JSONB config + API endpoints
|
|
|
+4. ❌ **Текстовые связи** - заменили на нормализованную БД
|
|
|
+5. ❌ **Plain text пароли** - заменили на bcrypt
|
|
|
+6. ❌ **Отсутствие auth** - добавили JWT + RBAC
|
|
|
+
|
|
|
+### Что улучшили:
|
|
|
+
|
|
|
+1. ✨ **Безопасность:** JWT, RBAC, bcrypt, SQL injection защита
|
|
|
+2. ✨ **Multi-tenant:** изоляция данных по организациям
|
|
|
+3. ✨ **Audit trail:** кто, когда, что сделал
|
|
|
+4. ✨ **Модальные окна:** валидация, подтверждение, UX
|
|
|
+5. ✨ **Simple ID:** удобные номера вместо MAC
|
|
|
+6. ✨ **Статусы:** online/offline/error с индикаторами
|
|
|
+7. ✨ **i18n:** RU/EN поддержка
|
|
|
+
|
|
|
+### Next steps:
|
|
|
+
|
|
|
+1. 🔨 Реализовать универсальный поиск (HIGH priority)
|
|
|
+2. 🔨 Bulk import устройств (MEDIUM priority)
|
|
|
+3. 🔨 Фильтры по статусу (LOW priority)
|
|
|
+4. 🔨 Export to CSV/Excel (LOW priority)
|