concertado o deploy

This commit is contained in:
2026-02-03 00:51:41 +00:00
parent 935b15980c
commit 833378b307
9 changed files with 143 additions and 106 deletions

View File

@@ -3,11 +3,14 @@ import os
import shutil
import json
import asyncio
import datetime
from collections import deque # Import necessário para ler as últimas linhas de forma eficiente
# --- CONFIGURAÇÕES DE DIRETÓRIOS ---
SRC_ROOT = "/downloads"
DST_ROOT = "/media"
CONFIG_PATH = "/app/data/presets.json"
LOG_PATH = "/app/data/history.log" # Novo arquivo de log persistente
class DeployManager:
def __init__(self):
@@ -16,8 +19,42 @@ class DeployManager:
self.selected_items = []
self.container = None
self.presets = self.load_presets()
self.pendencies = [] # {'name':, 'src':, 'dst':}
self.logs = []
self.pendencies = []
# CARREGA OS LOGS DO ARQUIVO AO INICIAR
self.logs = self.load_logs_from_file()
# --- NOVO: GERENCIAMENTO DE LOGS PERSISTENTES ---
def load_logs_from_file(self):
"""Lê as últimas 50 linhas do arquivo de log"""
if not os.path.exists(LOG_PATH):
return []
try:
# Lê as últimas 50 linhas do arquivo
with open(LOG_PATH, 'r', encoding='utf-8') as f:
# deque(..., maxlen=50) pega automaticamente as últimas 50
last_lines = list(deque(f, maxlen=50))
# Inverte a ordem para mostrar o mais recente no topo da lista visual
# remove quebras de linha com .strip()
return [line.strip() for line in reversed(last_lines)]
except:
return []
def add_log(self, message, type="info"):
"""Adiciona log na memória E no arquivo"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
full_msg = f"[{timestamp}] {message}"
# 1. Atualiza a lista da memória (para a UI)
self.logs.insert(0, full_msg)
if len(self.logs) > 50: self.logs.pop() # Limpa memória excedente
# 2. Salva no arquivo (Modo 'a' = append/adicionar no fim)
try:
with open(LOG_PATH, 'a', encoding='utf-8') as f:
f.write(full_msg + "\n")
except Exception as e:
print(f"Erro ao salvar log: {e}")
# --- 1. PERSISTÊNCIA (JSON) ---
def load_presets(self):
@@ -43,49 +80,45 @@ class DeployManager:
json.dump(self.presets, f)
self.refresh()
# --- 2. DIÁLOGO DE CONFIRMAÇÃO DO PRESET (NOVO) ---
# --- 2. DIÁLOGO DE CONFIRMAÇÃO DO PRESET ---
def confirm_preset_execution(self, name, paths):
"""Abre janela para confirmar antes de rodar o Smart Deploy"""
src = paths['src']
dst = paths['dst']
# Verificação básica antes de abrir o diálogo
if not os.path.exists(src):
ui.notify(f'Erro: Pasta de origem não existe: {src}', type='negative')
return
# Conta itens para mostrar no aviso
try:
items = [f for f in os.listdir(src) if not f.startswith('.')] # Ignora ocultos
items = [f for f in os.listdir(src) if not f.startswith('.')]
count = len(items)
except: count = 0
with ui.dialog() as dialog, ui.card():
ui.label(f'Executar: {name}?').classes('text-xl font-bold text-blue-900')
ui.label('Isso moverá TODOS os arquivos de:').classes('text-xs text-gray-500 mt-2')
ui.label(src).classes('font-mono text-sm bg-gray-100 p-1 rounded w-full break-all')
ui.label('Para:').classes('text-xs text-gray-500 mt-2')
ui.label(dst).classes('font-mono text-sm bg-gray-100 p-1 rounded w-full break-all')
ui.label(f'Origem: {src}').classes('font-mono text-xs bg-gray-100 p-1 rounded w-full break-all')
ui.label(f'Destino: {dst}').classes('font-mono text-xs bg-gray-100 p-1 rounded w-full break-all')
if count > 0:
ui.label(f'{count} itens encontrados prontos para mover.').classes('font-bold text-green-700 mt-2')
ui.label(f'{count} itens encontrados.').classes('font-bold text-green-700 mt-2')
else:
ui.label('Atenção: A pasta de origem parece vazia.').classes('font-bold text-orange-600 mt-2')
# Função wrapper para rodar o async corretamente
async def execute_action():
dialog.close()
await asyncio.sleep(0.1)
await self.move_process_from_preset(paths)
with ui.row().classes('w-full justify-end mt-4'):
ui.button('Cancelar', on_click=dialog.close).props('flat text-color=grey')
# Botão que realmente executa a ação
ui.button('CONFIRMAR MOVIMENTAÇÃO',
on_click=lambda: [dialog.close(), self.move_process_from_preset(paths)])\
.props('color=green icon=check')
ui.button('CONFIRMAR', on_click=execute_action).props('color=green icon=check')
dialog.open()
# --- 3. MOVIMENTAÇÃO E PENDÊNCIAS ---
async def move_process(self, items_to_move, target_folder):
"""Move arquivos em background e detecta conflitos"""
for item_path in items_to_move:
if not os.path.exists(item_path): continue
@@ -109,7 +142,6 @@ class DeployManager:
self.refresh()
async def move_process_from_preset(self, paths):
"""Executa a movimentação após confirmação"""
src, dst = paths['src'], paths['dst']
if os.path.exists(src):
items = [os.path.join(src, f) for f in os.listdir(src)]
@@ -148,7 +180,7 @@ class DeployManager:
if refresh: self.refresh()
# --- 5. NAVEGAÇÃO (BREADCRUMBS) ---
# --- 5. NAVEGAÇÃO ---
def render_breadcrumbs(self, current_path, root_dir, nav_callback):
with ui.row().classes('items-center gap-1 bg-gray-100 p-1 rounded w-full mb-2'):
ui.button('🏠', on_click=lambda: nav_callback(root_dir)).props('flat dense size=sm')
@@ -177,10 +209,6 @@ class DeployManager:
self.refresh()
# --- 6. INTERFACE PRINCIPAL ---
def add_log(self, message, type="info"):
self.logs.insert(0, message)
if len(self.logs) > 30: self.logs.pop()
def refresh(self):
if self.container:
self.container.clear()
@@ -194,7 +222,6 @@ class DeployManager:
ui.label('SMART DEPLOYS:').classes('font-bold text-blue-900 mr-4')
for name, paths in self.presets.items():
with ui.button_group().props('rounded'):
# AQUI MUDOU: Chama o diálogo de confirmação em vez de mover direto
ui.button(name, on_click=lambda n=name, p=paths: self.confirm_preset_execution(n, p)).props('color=blue-6')
ui.button(on_click=lambda n=name: self.delete_preset(n)).props('icon=delete color=red-4')
@@ -233,10 +260,13 @@ class DeployManager:
# PAINEL DE LOGS
with ui.card().classes('flex-grow h-64 bg-slate-900 text-slate-200 shadow-none'):
ui.label('📜 Log de Atividades').classes('font-bold border-b border-slate-700 w-full pb-2')
ui.label('📜 Log de Atividades (Persistente)').classes('font-bold border-b border-slate-700 w-full pb-2')
with ui.scroll_area().classes('w-full h-full'):
# Renderiza os logs carregados
for log in self.logs:
ui.label(f"> {log}").classes('text-[10px] font-mono leading-tight')
# Pinta de vermelho se tiver erro, verde se sucesso
color_cls = 'text-red-400' if '' in log else 'text-green-400' if '' in log else 'text-slate-300'
ui.label(f"> {log}").classes(f'text-[10px] font-mono leading-tight {color_cls}')
# BOTÃO GLOBAL
ui.button('INICIAR MOVIMENTAÇÃO DOS SELECIONADOS', on_click=lambda: self.move_process(self.selected_items, self.dst_path))\
@@ -244,7 +274,7 @@ class DeployManager:
.props('color=green-7 icon=forward')\
.bind_enabled_from(self, 'selected_items', backward=lambda x: len(x) > 0)
# --- AUXILIARES (Listas, Checkbox, etc) ---
# --- AUXILIARES ---
def render_file_list(self, path, is_source):
try:
entries = sorted(os.scandir(path), key=lambda e: (not e.is_dir(), e.name.lower()))