# Server Groups — полный план UI/UX и реализации ## Контекст ServerManager хранит серверы в плоском списке. При 10+ серверах разных типов (SSH, SQL, Redis, VPN, API, тестовые) становится неудобно. Нужна организация по группам: "Production", "Testing", "VPN Servers", "Hosting" и т.д. ## Исследование рынка | Инструмент | Подход | |---|---| | MobaXterm, mRemoteNG, SecureCRT | Дерево папок, вложенность, drag-and-drop | | Termius | Группы (вложенные) + теги + цвета — золотой стандарт | | Royal TS | Папки + наследование credentials от папки | | AWS/Azure/GCP | Resource Groups + теги (key:value) | | Zabbix, Datadog | Host Groups + теги, сервер в нескольких группах | **Выбранный паттерн: Группы с цветами (как Termius)** — один уровень групп, цветовая кодировка, сворачивание/разворачивание. --- ## 1. Модель данных ### Группы хранятся в `servers.json` (тот же зашифрованный файл): ```json { "servers": [...], "ssh_key": {...}, "groups": [ { "id": "a1b2c3d4", "name": "Production", "color": "#ef4444", "collapsed": false, "order": 0 }, { "id": "e5f6g7h8", "name": "Testing", "color": "#22c55e", "collapsed": false, "order": 1 } ] } ``` ### Сервер получает поле `group`: ```json { "alias": "my-prod-server", "ip": "1.2.3.4", "type": "ssh", "group": "a1b2c3d4", ... } ``` - `group` ссылается на `groups[].id` - Без `group` или пустой = "Без группы" - При удалении группы серверы становятся ungrouped ### Почему в `servers.json`, а не `settings.json`: - Группы тесно связаны с серверами - Одна атомарная операция save - `ssh.py` просто игнорирует ключ `groups` — обратная совместимость --- ## 2. API в ServerStore ```python # Группы CRUD get_groups() -> list[dict] # Все группы, отсортированы по order get_group(group_id) -> dict | None # Одна группа по ID add_group(name, color) -> dict # Создать, вернуть dict update_group(group_id, **kwargs) # Обновить name/color/collapsed/order remove_group(group_id) # Удалить, серверы → ungrouped reorder_groups(ordered_ids: list[str]) # Установить порядок # Серверы в группах set_server_group(alias, group_id|None) # Переместить сервер в группу get_servers_in_group(group_id|None) # Серверы группы (None = ungrouped) ``` --- ## 3. UI Sidebar — Сгруппированный вид ### Было (плоский список): ``` +-------------------------------------+ | Servers | +-------------------------------------+ | [ Search... ] | +-------------------------------------+ | ● SSH investor 1.2.3.4 | | ● SSH main-ovh 5.6.7.8 | | ● SSH API TOR... 9.10.11.12 | | ● RDP 116 Windows 13.14.15.16 | | ● RDS Reddis main 5.6.7.8 | | ● MDB Maria Db... 5.6.7.8 | | ● SSH sensey24.ru 17.18.19.20 | | | | Active: 2 | | [+ Add] [Edit] [Delete] | +-------------------------------------+ ``` ### Станет (группы): ``` +------------------------------------------+ | Servers | +------------------------------------------+ | [ Search... ] [+ Grp] | +------------------------------------------+ | | | v ● Production (3) | | +----------------------------------+ | | | ○ SSH main-ovh 5.6.7.8 | | | +----------------------------------+ | | | ○ RDS Reddis main 5.6.7.8 | | | +----------------------------------+ | | | ○ MDB Maria Db 5.6.7.8 | | | +----------------------------------+ | | | | > ● VPN/Proxy (2) | | (свёрнуто — серверы скрыты) | | | | v ● Hosting (3) | | +----------------------------------+ | | | ○ SSH sensey24.ru 17.18.19.20 | | | +----------------------------------+ | | | ○ SSH git.sensey 17.18.19.20 | | | +----------------------------------+ | | | ○ SSH 1gb server 21.22.23.24 | | | +----------------------------------+ | | | | v ● Testing (1) | | +----------------------------------+ | | | ○ SSH thehost 25.26.27.28 | | | +----------------------------------+ | | | | Без группы (1) | | +----------------------------------+ | | | ○ RDP 116 Windows 13.14.15.16 | | | +----------------------------------+ | | | | Active: 2 | | [+ Add] [Edit] [Delete] | +------------------------------------------+ ``` ### Заголовок группы (детально): ``` +----------------------------------------------+ | v ● Production (3) | | ^ ^ ^ ^ | | | | | | | | | цвет название кол-во | | | группы группы серверов | | стрелка | | свернуть/развернуть | +----------------------------------------------+ ``` - `v` / `>` — кликабельная стрелка toggle - `●` — цветная точка (`\u25cf`) цветом группы - Название — жирный шрифт - `(3)` — серый, кол-во серверов - Вся строка кликабельна для toggle - ПКМ — контекстное меню группы ### Серверы внутри группы: - Отступ `padx=(12, 2)` для визуальной вложенности - Всё остальное как сейчас: StatusBadge, TypeBadge, Name, IP ### Поведение: - **Нет групп** → плоский список (как сейчас, полная обратная совместимость) - **Есть группы** → автоматически сгруппированный вид - **Поиск** → все группы разворачиваются, пустые группы скрываются - **"Без группы"** → только если есть группы И есть серверы без группы --- ## 4. Создание группы ### Кнопка `[+ Grp]` рядом с поиском: ``` | [ Search... ] [+ Grp] | ``` ### GroupDialog (новый файл `gui/group_dialog.py`): ``` +----------------------------------+ | New Group | +----------------------------------+ | | | Name: | | [ Production ] | | | | Color: | | (●)(●)(●)(●)(●)(●)(●)(●) | | red org amb grn blu ind pur pnk | | | | [ Cancel ] [ Save ] | +----------------------------------+ ``` Палитра из 8 цветов: ```python GROUP_COLORS = [ "#ef4444", # красный "#f97316", # оранжевый "#f59e0b", # янтарный "#22c55e", # зелёный "#3b82f6", # синий "#6366f1", # индиго "#a855f7", # фиолетовый "#ec4899", # розовый ] ``` Каждый цвет — кнопка-кружок. Выбранный — с рамкой. --- ## 5. Перемещение серверов между группами ### Способ 1: ПКМ на сервере → "Переместить в группу" ``` +-----------------------------+ | Open Terminal | | Browse Files | | Install Key | |-----------------------------| | Move to Group > | | +---------------------+ | | | ● Production | | | | ● VPN/Proxy | | | | ● Hosting | | | | ● Testing | | | |---------------------| | | | Без группы | | | +---------------------+ | |-----------------------------| | Check Status | | Copy Alias | |-----------------------------| | Edit | | Delete | +-----------------------------+ ``` ### Способ 2: Поле "Группа" в ServerDialog (создание/редактирование) ``` +----------------------------------+ | Add Server | +----------------------------------+ | Alias: [ ] | | IP: [ ] | | Type: [ SSH v ] | | Port: [ 22 ] | | Group: [ ● Production v ] | | +-----------------------+| | | No group || | | ● Production || | | ● VPN/Proxy || | | ● Hosting || | | ● Testing || | +-----------------------+| | User: [ root ] | | Pass: [ •••••••• [👁] ] | | ... | +----------------------------------+ ``` - Dropdown появляется только когда есть группы - По умолчанию "No group" - При редактировании — предвыбрана текущая группа ### Drag-and-drop: **НЕ реализуем.** CustomTkinter не имеет нативного DnD для ScrollableFrame. Реализация через низкоуровневый tkinter DnD = хрупко и сложно. ПКМ "Move to Group" — надёжнее и понятнее. --- ## 6. Контекстное меню группы (ПКМ на заголовке) ``` +------------------------+ | Rename | | Change Color > | | +------------------+ | | | ● Red | | | | ● Orange | | | | ● Amber | | | | ● Green | | | | ● Blue | | | | ● Indigo | | | | ● Purple | | | | ● Pink | | | +------------------+ | |------------------------| | Move Up | | Move Down | |------------------------| | Delete Group | +------------------------+ ``` - **Rename** → открывает GroupDialog в режиме редактирования - **Change Color** → подменю с 8 цветами - **Move Up/Down** → меняет `order` местами с соседней группой - **Delete Group** → подтверждение, серверы → ungrouped --- ## 7. i18n ключи (~17 штук) | Ключ | EN | RU | ZH | |------|----|----|-----| | group | Group | Группа | 分组 | | groups | Groups | Группы | 分组 | | no_group | No group | Без группы | 无分组 | | ungrouped | Ungrouped | Без группы | 未分组 | | add_group | Add Group | Добавить группу | 添加分组 | | edit_group | Edit Group | Редактировать группу | 编辑分组 | | rename_group | Rename | Переименовать | 重命名 | | delete_group | Delete Group | Удалить группу | 删除分组 | | delete_group_confirm | Delete '{name}'? Servers become ungrouped. | Удалить '{name}'? Серверы станут без группы. | 删除'{name}'?服务器将变为未分组。 | | group_name | Group Name | Название группы | 分组名称 | | group_color | Color | Цвет | 颜色 | | group_name_required | Group name is required | Название обязательно | 名称为必填项 | | move_to_group | Move to Group | Переместить в группу | 移动到分组 | | move_up | Move Up | Вверх | 上移 | | move_down | Move Down | Вниз | 下移 | | change_color | Change Color | Изменить цвет | 更改颜色 | | new_group | New Group | Новая группа | 新建分组 | --- ## 8. Файлы и изменения | Файл | Что меняется | |------|-------------| | `core/server_store.py` | +8 методов для Groups CRUD, `get_servers_in_group()`, `set_server_group()` | | `gui/sidebar.py` | Рефакторинг `_refresh_list()` на grouped layout, заголовки групп, контекстные меню, collapse/expand, кнопка "+ Grp", "Move to Group" submenu | | `gui/group_dialog.py` | **НОВЫЙ** — диалог создания/редактирования группы (имя + палитра цветов) | | `gui/server_dialog.py` | +dropdown "Group" (виден только когда есть группы) | | `core/i18n.py` | +17 ключей EN/RU/ZH | | `gui/app.py` | Привязка `sidebar.add_group_callback` к открытию GroupDialog | --- ## 9. Порядок реализации ### Фаза 1: Данные (без UI) 1. Методы GroupsCRUD в `server_store.py` 2. Проверить что `ssh.py` не ломается ### Фаза 2: Sidebar — grouped rendering 1. Рефакторинг `_refresh_list()` на helper-методы 2. `_render_group_header()` с collapse toggle 3. Отступ для серверов внутри групп 4. Поиск с автораскрытием групп ### Фаза 3: Управление группами 1. `group_dialog.py` 2. Кнопка "+ Grp" в sidebar 3. Контекстное меню группы (rename, delete, reorder, color) ### Фаза 4: Назначение серверов группам 1. "Move to Group" в контекстном меню сервера 2. Dropdown "Group" в ServerDialog ### Фаза 5: i18n + polish 1. Все переводы 2. Edge cases: пустые группы, все ungrouped, одна группа --- ## 10. Edge Cases - **Нет групп** → плоский список, полная обратная совместимость - **Удаление группы** → серверы переходят в "Без группы" - **Группа ссылается на удалённый ID** → сервер отображается как ungrouped - **Поиск** → все группы разворачиваются, пустые скрываются - **Миграция** → старый `servers.json` без `groups` → `get_groups()` возвращает `[]` - **ssh.py** → игнорирует `groups` и `group` поля, нулевой impact --- ## 11. Верификация 1. `python build.py` — собрать exe 2. Без групп — плоский список как раньше 3. Создать группу "Production" красным цветом 4. Создать группу "Testing" зелёным 5. Переместить серверы в группы через ПКМ 6. Свернуть/развернуть группу 7. Поиск — группы разворачиваются 8. Добавить новый сервер — выбрать группу в диалоге 9. Удалить группу — серверы стали ungrouped 10. Переименовать группу, сменить цвет 11. Move Up/Down — порядок меняется 12. Переключить язык — все строки переведены