server groups: grouped sidebar, GroupDialog, context menus, i18n + cleanup old releases

- Groups CRUD in sidebar (create, rename, change color, reorder, delete)
- Collapsible group headers with color dots and server count
- "Move to Group" context menu on servers
- Group dropdown in ServerDialog (add/edit)
- 17 i18n keys (EN/RU/ZH)
- Search auto-expands groups
- Cleaned up old release binaries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-03 02:12:35 -05:00
parent 2f84429b10
commit c9e3ee8fc5
17 changed files with 852 additions and 24 deletions

414
plans/server-groups.md Normal file
View File

@@ -0,0 +1,414 @@
# 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. Переключить язык — все строки переведены