/home/user/work/luckfox/mybeacon-legacy/www/my-wifi/panel/
Что делает:
Примеры поиска:
ac:84 → найдет все MAC-адреса содержащие "ac:84"192.168 → найдет все устройства с IP в этой подсетиOffice_WiFi → найдет все устройства подключенные к этому SSIDbeacon.ru → найдет все устройства с этим адресом отправки данныхПочему важно:
Решение для нового проекта: ✅ ТАЩИМ - универсальное поисковое поле с поиском по всем полям
Что делает:
Колонки:
# 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
Почему важно:
Решение для нового проекта: ⚠️ ЧАСТИЧНО - компактная таблица с основным, детали в модальном окне
Что делает:
Use case:
Почему опасно:
Решение для нового проекта: ❌ НЕ ТАЩИМ - слишком опасно, заменяем модальными окнами с подтверждением
Что делает:
Use cases:
time → увидеть кто давно не подключалсяMAC → найти все устройства одного производителяip → сгруппировать по подсетямРешение для нового проекта: ✅ ТАЩИМ - сортировка обязательно
Что показывает:
# Логин Пароль Устройства Оплачено до Информация Оплата
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
Ключевая фича:
Решение для нового проекта: ✅ ТАЩИМ - список организаций с количеством устройств и статусом
Что делает:
Решение для нового проекта: ✅ ТАЩИМ - но через модальное окно, не отдельную страницу
Что хранится:
Формат: Текстовое поле (textarea), админ пишет произвольный текст
Решение для нового проекта: ✅ ТАЩИМ - но структурированно (отдельные поля, не текст)
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 из списка привязываются к клиенту
Почему удобно:
Решение для нового проекта: ✅ ТАЩИМ - форма создания организации с возможностью сразу добавить несколько устройств
Логика:
Use case:
Решение для нового проекта:
⚠️ ЧАСТИЧНО - заменяем на статусы организации: pending, active, suspended
Где:
Реализация:
Поисковое поле → backend ищет по полям:
- Devices: MAC, simple_id, organization.name, organization.owner_email, IP
- Organizations: name, contact_email, owner.email
- Users: email, full_name, organization.name
Фичи:
Devices:
Simple ID | MAC Address | Organization | Status | Last Seen | Actions
#1 | ac:84:c6:... | Shop LLC | online | 2 min ago | ✏️ Edit
#2 | cc:2d:e0:... | Mall Inc | offline | 5 days ago | ✏️ Edit
⚠️ ВАЖНО: Кнопка Delete НЕ в таблице!
Почему:
Сортировка:
Клик на заголовок → меняет сортировку Индикатор направления → стрелка ↑↓
Форма 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]
После создания:
Device status badges:
.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 → красный (заблокирована)
Вместо 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] │
│ │
│ ───────────────────────────────────│
│ Danger Zone: │
│ [🗑 Delete Device] │
│ (требует подтверждения) │
└─────────────────────────────────────┘
Преимущества:
Что:
Пример:
☑ Select All (5 selected)
[Change Organization ▼] [Delete Selected] [Export CSV]
Use cases:
Вместо dropdown:
Status: [All] [Online] [Offline] [Error]
───── (123) (45) (2)
Клик на chip → фильтрует таблицу Счетчик → показывает количество устройств в каждом статусе
Кнопка: "Export to Excel"
Что экспортирует:
Use case:
Почему НЕ тащим:
Замена: Модальные окна
Почему НЕ тащим:
ovpn_flagovpn_addrLegacy имело:
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="
Проблемы:
Новый подход:
wifi_enabled, ble_enabled/api/v1/device/config - получить конфиг/api/v1/device/wifi-events - отправить WiFi данные/api/v1/device/ble-events - отправить BLE данныеLegacy:
wf_client_ssid varchar(60) -- WiFi SSID
wf_client_psk varchar(60) -- WiFi пароль (plain text!)
Проблемы Legacy:
devices (нужно обновлять каждое устройство)Новый подход:
Хранение:
UI - Просмотр и редактирование:
В Organizations Panel (для superadmin/owner/admin):
┌────────────────────────────────────────┐
│ Organization: Shop LLC │
│ │
│ WiFi Settings: │
│ │
│ SSID: ___________________ │
│ Shop_Guest_WiFi │
│ │
│ Password: ●●●●●●●●●● [👁 Show] │
│ (click Show → myPassword123) │
│ │
│ [Save Changes] │
└────────────────────────────────────────┘
Кнопка "Show Password":
●●●●●●●●●● (скрыт)myPassword123Права доступа:
API Endpoints:
# GET /api/v1/client/organization/wifi-credentials
# Permissions: owner, admin
{
"ssid": "Shop_Guest_WiFi",
"password": "myPassword123" # Расшифрованный пароль
}
# PATCH /api/v1/client/organization/wifi-credentials
# Permissions: owner, admin
{
"ssid": "Shop_Guest_WiFi_New",
"password": "newPassword456"
}
Backend шифрование:
from cryptography.fernet import Fernet
class Organization(Base):
# ...
wifi_ssid = Column(String(100))
wifi_password_encrypted = Column(String(500)) # Зашифрованный пароль
@property
def wifi_password(self):
"""Расшифровать пароль для показа админу"""
if not self.wifi_password_encrypted:
return None
return decrypt_password(self.wifi_password_encrypted)
@wifi_password.setter
def wifi_password(self, plain_password):
"""Зашифровать пароль перед сохранением"""
self.wifi_password_encrypted = encrypt_password(plain_password)
Device получает через API:
/api/v1/device/config?mac=...organization_id устройстваПреимущества:
Legacy:
device text NOT NULL -- "b8:27:eb:c1:46:0e;cc:2d:e0:ca:9f:7e;d8:0d:17:5e:07:94;"
Проблемы:
Новый подход:
devices с organization_id (foreign key)Legacy:
password text NOT NULL -- "12345" (plain text)
Новый подход:
hashed_password varchar(255)Legacy:
Новый подход:
Backend:
@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
):
query = select(Device)
# Search filter
if search:
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 {"devices": devices.all(), "total": total}
Frontend:
<input
v-model="searchQuery"
@input="debouncedSearch"
placeholder="Поиск по MAC, организации, email..."
class="search-input"
/>
Форма Create Organization:
Добавить секцию:
☐ Bulk add devices
Paste MAC addresses:
┌────────────────────────────┐
│ ac:84:c6:42:17:90 │
│ cc:2d:e0:ca:9f:7e │
│ d8:0d:17:5e:07:94 │
└────────────────────────────┘
Separator: [; or , or newline ▼]
[Parse] → "✓ Found 3 valid MAC addresses"
[Create Organization with 3 devices]
Backend:
@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
UI:
<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:
@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}
| Фича | 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 credentials | ✅ Plain text в devices | ✅ Encrypted в org + UI | 🔨 TODO |
| Пароли юзеров | ❌ Plain text | ✅ Bcrypt hash | ✅ Done |