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
+81
View File
@@ -0,0 +1,81 @@
"""Очередь длинных задач с журналом."""
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from typing import Awaitable, Callable, Coroutine, List, Optional
from app.db.database import get_session
from app.db.models import TaskRecord
LogCallback = Callable[[str], None]
class TaskManager:
def __init__(self):
self._running: dict[int, asyncio.Task] = {}
def start(self, kind: str, title: str, server_id: Optional[int],
coro_factory: Callable[[LogCallback], Coroutine],
on_log: Optional[LogCallback] = None,
on_done: Optional[Callable[[TaskRecord], None]] = None) -> int:
"""Запустить задачу. coro_factory(log) — корутина, пишущая строки в log."""
with get_session() as s:
rec = TaskRecord(kind=kind, title=title, server_id=server_id, status="running")
s.add(rec)
s.commit()
s.refresh(rec)
task_id = rec.id
def log(line: str) -> None:
with get_session() as s:
r = s.get(TaskRecord, task_id)
if r:
r.log += line + "\n"
s.add(r)
s.commit()
if on_log:
on_log(line)
async def runner():
status = "success"
try:
await coro_factory(log)
except asyncio.CancelledError:
status = "cancelled"
log("[отменено пользователем]")
except Exception as e:
status = "error"
log(f"[ОШИБКА] {e}")
with get_session() as s:
r = s.get(TaskRecord, task_id)
if r:
r.status = status
r.finished_at = datetime.now(timezone.utc)
s.add(r)
s.commit()
s.refresh(r)
if on_done:
on_done(r)
self._running[task_id] = asyncio.ensure_future(runner())
return task_id
def cancel(self, task_id: int) -> None:
t = self._running.get(task_id)
if t and not t.done():
t.cancel()
def get(self, task_id: int) -> Optional[TaskRecord]:
with get_session() as s:
return s.get(TaskRecord, task_id)
def list_recent(self, limit: int = 50) -> List[TaskRecord]:
from sqlmodel import select
with get_session() as s:
return list(s.exec(
select(TaskRecord).order_by(TaskRecord.id.desc()).limit(limit)
).all())
tasks = TaskManager()