Initial commit: Debian Manager — управление Debian-серверами

- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
dinlo
2026-09-05 19:46:45 +08:00
commit f624fee5e0
65 changed files with 6981 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
"""Portainer: статус, открытие в браузере, установка/перезапуск."""
import flet as ft
from app.core.ssh import SSHClient
from app.core import docker_svc as d
from app.db.models import Server
from app.ui.components import snack
PORTAINER_PORT = 9000
class PortainerPage(ft.Column):
def __init__(self, page: ft.Page, go):
super().__init__(expand=True, scroll=ft.ScrollMode.AUTO, spacing=12)
self._page = page
self.go = go
self.cards_col = ft.Column(spacing=10)
self.controls = [
ft.Text("Portainer", size=28, weight=ft.FontWeight.BOLD),
ft.Row([
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
on_click=lambda e: self._reload()),
]),
self.cards_col,
]
self._reload()
def _reload(self):
from app.core import servers as servers_core
self.cards_col.controls = []
for srv in servers_core.list_servers():
self.cards_col.controls.append(self._card_placeholder(srv))
self._page.update()
from app.core import servers as servers_core
for srv, card in zip(servers_core.list_servers(), self.cards_col.controls):
self._page.run_task(self._fill, srv, card)
def _card_placeholder(self, srv):
return ft.Card(content=ft.Container(
padding=14, width=500,
content=ft.Column([
ft.Text(srv.name, weight=ft.FontWeight.BOLD, size=16),
ft.ProgressBar(height=3),
]),
))
async def _fill(self, srv, card):
url = f"http://{srv.host}:{PORTAINER_PORT}"
try:
async with SSHClient(srv) as c:
containers = await d.list_containers(c)
portainer = next((x for x in containers if "portainer" in x["Names"].lower()), None)
if not portainer:
card.content.content.controls[1] = ft.Column([
ft.Text("Portainer не установлен", color=ft.Colors.GREY_500),
ft.FilledButton("Установить", icon=ft.Icons.DOWNLOAD,
on_click=lambda e, s=srv: self._install(s)),
])
else:
running = "Up" in portainer["Status"]
cid = portainer["ID"]
card.content.content.controls[1] = ft.Column([
ft.Row([
ft.Icon(ft.Icons.CIRCLE, size=12,
color=ft.Colors.GREEN_400 if running else ft.Colors.RED_400),
ft.Text(portainer["Status"], size=12),
]),
ft.Row([
ft.FilledButton("Открыть в браузере", icon=ft.Icons.OPEN_IN_BROWSER,
on_click=lambda e, u=url: self._page.launch_url(u)),
ft.OutlinedButton(
"Перезапустить" if running else "Запустить",
on_click=lambda e, s=srv, c=cid, a="restart" if running else "start":
self._action(s, c, a)),
]),
ft.Text(url, size=11, color=ft.Colors.BLUE_300, selectable=True),
])
except Exception as e:
card.content.content.controls[1] = ft.Text(f"ошибка: {e}", size=12,
color=ft.Colors.RED_300)
self._page.update()
def _install(self, srv):
from app.core.tasks import tasks
from app.ui.components import append_log, log_view
async def job(log):
async with SSHClient(srv) as c:
log("$ docker volume create portainer_data")
await c.sudo("docker volume create portainer_data 2>/dev/null || true")
log("$ docker run portainer")
r = await c.sudo(
"docker rm -f portainer 2>/dev/null; "
"docker run -d -p 9000:9000 --name portainer --restart=always "
"-v /var/run/docker.sock:/var/run/docker.sock "
"-v portainer_data:/data portainer/portainer-ce:lts", timeout=600)
log(r.stdout + r.stderr)
tasks.start("portainer", "установка Portainer", srv.id, job,
on_done=lambda r: self._reload())
snack(self._page, f"Установка Portainer на {srv.name} запущена")
def _action(self, srv, cid, action):
async def _do():
async with SSHClient(srv) as c:
await d.container_action(c, cid, action)
self._reload()
self._page.run_task(_do)