f624fee5e0
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
"""Debian Manager — управление Debian-серверами."""
|
|
import asyncio
|
|
|
|
import flet as ft
|
|
|
|
from app.db.database import init_db
|
|
from app.core import servers as servers_core
|
|
from app.core import scheduler
|
|
from app.db.models import Setting
|
|
from app.db.database import get_session
|
|
from app.ui.pages.dashboard import DashboardPage
|
|
from app.ui.pages.servers import ServersPage
|
|
from app.ui.pages.server_detail import ServerDetailPage
|
|
from app.ui.pages.group_ops import GroupOpsPage
|
|
from app.ui.pages.playbooks import PlaybooksPage
|
|
from app.ui.pages.diff import DiffPage
|
|
from app.ui.pages.gitea import GiteaPage
|
|
from app.ui.pages.scheduler import SchedulerPage
|
|
from app.ui.pages.backups import BackupsPage
|
|
from app.ui.pages.reports import ReportsPage
|
|
from app.ui.pages.portainer import PortainerPage
|
|
from app.ui.pages.agent import AgentPage
|
|
from app.ui.pages.proxmox_page import ProxmoxPage
|
|
from app.ui.pages.wizard import WizardPage
|
|
from app.ui.pages.settings import SettingsPage
|
|
from app.ui.pages.tasks import TasksPage
|
|
|
|
|
|
def _get_setting(key: str) -> str:
|
|
with get_session() as s:
|
|
row = s.get(Setting, key)
|
|
return row.value if row else ""
|
|
|
|
|
|
def _set_setting(key: str, value: str) -> None:
|
|
with get_session() as s:
|
|
row = s.get(Setting, key) or Setting(key=key)
|
|
row.value = value
|
|
s.add(row)
|
|
s.commit()
|
|
|
|
|
|
def main(page: ft.Page):
|
|
page.title = "Debian Manager"
|
|
page.window.width = 1400
|
|
page.window.height = 900
|
|
page.padding = 0
|
|
|
|
init_db()
|
|
|
|
# тема из настроек
|
|
theme = _get_setting("theme")
|
|
page.theme_mode = (ft.ThemeMode.DARK if theme != "light" else ft.ThemeMode.LIGHT)
|
|
|
|
# seed тестового сервера при первом запуске
|
|
if not servers_core.list_servers():
|
|
servers_core.add_server(
|
|
name="Test VPS", host="132.243.17.164", port=2292,
|
|
username="dimon", password="123456789987654321", tags="удалённые",
|
|
)
|
|
|
|
# запуск планировщика и фоновых сервисов
|
|
async def _start_bg():
|
|
scheduler.start()
|
|
try:
|
|
from app.core import webhook, agent
|
|
await webhook.start()
|
|
await agent.start()
|
|
except Exception:
|
|
pass
|
|
page.run_task(_start_bg)
|
|
|
|
content = ft.Container(expand=True, padding=20)
|
|
|
|
PAGES = {
|
|
"dashboard": ("Дашборд", ft.Icons.DASHBOARD, DashboardPage),
|
|
"servers": ("Серверы", ft.Icons.DNS, ServersPage),
|
|
"wizard": ("Мастер настройки", ft.Icons.AUTO_FIX_HIGH, WizardPage),
|
|
"group": ("Групповые операции", ft.Icons.GROUPS, GroupOpsPage),
|
|
"scheduler": ("Планировщик", ft.Icons.SCHEDULE, SchedulerPage),
|
|
"playbooks": ("Плейбуки", ft.Icons.PLAYLIST_PLAY, PlaybooksPage),
|
|
"backups": ("Бэкапы", ft.Icons.BACKUP, BackupsPage),
|
|
"reports": ("Отчёты", ft.Icons.SUMMARIZE, ReportsPage),
|
|
"portainer": ("Portainer", ft.Icons.WIDGETS, PortainerPage),
|
|
"agent": ("Агент", ft.Icons.SENSORS, AgentPage),
|
|
"proxmox": ("Proxmox", ft.Icons.CLOUD_QUEUE, ProxmoxPage),
|
|
"diff": ("Сравнение", ft.Icons.COMPARE_ARROWS, DiffPage),
|
|
"gitea": ("Gitea Deploy", ft.Icons.ROCKET_LAUNCH, GiteaPage),
|
|
"tasks": ("Журнал задач", ft.Icons.TASK_ALT, TasksPage),
|
|
"settings": ("Настройки", ft.Icons.SETTINGS, SettingsPage),
|
|
}
|
|
|
|
state = {"route": "dashboard", "detail_server_id": None,
|
|
"group_filter": "Все"}
|
|
|
|
# top_bar объявлен до render, чтобы render мог его обновлять
|
|
top_bar = ft.Container(padding=ft.Padding.only(left=20, top=10))
|
|
|
|
def render():
|
|
top_bar.content = group_chips()
|
|
route = state["route"]
|
|
if route == "server_detail":
|
|
content.content = ServerDetailPage(page, state["detail_server_id"], go)
|
|
elif route == "dashboard":
|
|
content.content = DashboardPage(page, go, group_filter=state["group_filter"])
|
|
else:
|
|
cls = PAGES[route][2]
|
|
content.content = cls(page, go)
|
|
nav.selected_index = list(PAGES.keys()).index(route) if route in PAGES else 0
|
|
page.update()
|
|
|
|
def go(route: str, server_id=None, group_filter=None):
|
|
state["route"] = route
|
|
state["detail_server_id"] = server_id
|
|
if group_filter is not None:
|
|
state["group_filter"] = group_filter
|
|
render()
|
|
|
|
def toggle_theme(e):
|
|
new = "light" if page.theme_mode == ft.ThemeMode.DARK else "dark"
|
|
page.theme_mode = ft.ThemeMode.LIGHT if new == "light" else ft.ThemeMode.DARK
|
|
_set_setting("theme", new)
|
|
theme_btn.icon = ft.Icons.LIGHT_MODE if new == "dark" else ft.Icons.DARK_MODE
|
|
page.update()
|
|
|
|
# группы серверов для дашборда
|
|
def group_chips():
|
|
servers = servers_core.list_servers()
|
|
tags = sorted({t.strip() for s in servers for t in (s.tags or "").split(",") if t.strip()})
|
|
groups = ["Все"] + tags
|
|
return ft.Row([
|
|
ft.Chip(
|
|
label=ft.Text(g),
|
|
selected=state["group_filter"] == g,
|
|
on_select=lambda e, g=g: go("dashboard", group_filter=g),
|
|
) for g in groups
|
|
], spacing=6)
|
|
|
|
theme_btn = ft.IconButton(
|
|
ft.Icons.LIGHT_MODE if page.theme_mode == ft.ThemeMode.DARK else ft.Icons.DARK_MODE,
|
|
tooltip="Сменить тему", on_click=toggle_theme,
|
|
)
|
|
|
|
# NavigationRail — компактный режим (только иконки), tooltip через label
|
|
nav = ft.NavigationRail(
|
|
selected_index=0,
|
|
label_type=ft.NavigationRailLabelType.NONE,
|
|
min_width=60,
|
|
destinations=[
|
|
ft.NavigationRailDestination(
|
|
icon=ft.Icon(v[1], tooltip=v[0]), # tooltip на самой иконке
|
|
label=v[0],
|
|
)
|
|
for v in PAGES.values()
|
|
],
|
|
on_change=lambda e: go(list(PAGES.keys())[e.control.selected_index]),
|
|
trailing=ft.Column([theme_btn], alignment=ft.MainAxisAlignment.END),
|
|
)
|
|
|
|
# верхняя панель с группами (только на дашборде)
|
|
top_bar.content = group_chips()
|
|
|
|
page.add(
|
|
ft.Column([
|
|
top_bar,
|
|
ft.Row(
|
|
[nav, ft.VerticalDivider(width=1), content],
|
|
expand=True,
|
|
spacing=0,
|
|
),
|
|
], expand=True, spacing=0),
|
|
)
|
|
render()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ft.app(target=main)
|