from nicegui import ui, app import psutil import datetime import multiprocessing import subprocess import os from services import ( init_db, get_note_content, save_note_content_process, tarea_backup_process, tarea_alarma_thread, trafico_red, tareas_activas, tarea_scraping_thread, tarea_reproducir_mp3_thread, ejecutar_en_thread, ejecutar_en_proceso ) import time # Inicializar DB init_db() # ============================================================================= # UI LAYOUT # ============================================================================= @ui.page('/') def main(): # Estilos globales ui.add_head_html('') ui.colors(primary='#5898d4') # Un azul tipo VS Code # Estado local para las alarmas de este cliente # Usamos una lista para comunicar el thread con la UI alarmas_completadas = [] scraping_resultados = [] mp3_estado = [] with ui.header().classes('items-center justify-between bg-slate-800 text-white'): ui.label('PSP Dashboard').classes('text-xl font-bold') ui.label('NiceGUI + Python').classes('text-sm opacity-70') # Contenedor principal with ui.column().classes('w-full p-4 gap-4'): # --- FILA 1: RELOJ Y RED --- with ui.row().classes('w-full gap-4 items-stretch'): # Reloj with ui.card().classes('flex-1 w-0'): ui.label('Hora del sistema').classes('text-gray-500 text-sm') lbl_clock = ui.label().classes('text-4xl font-mono') # Timer para actualizar reloj ui.timer(1.0, lambda: lbl_clock.set_text(datetime.datetime.now().strftime('%H:%M:%S'))) # Red with ui.card().classes('flex-1 w-0 items-end'): ui.label('Tráfico de red').classes('text-gray-500 text-sm') with ui.row().classes('gap-6 mt-2 justify-end'): # Bajada with ui.column().classes('items-end'): with ui.row().classes('items-center gap-2 mb-1'): ui.label('Bajada').classes('text-xs text-gray-600') ui.icon('download', size='sm').classes('text-green-600') lbl_rx = ui.label('0 KB/s').classes('text-2xl font-bold text-green-600') # Subida with ui.column().classes('items-end'): with ui.row().classes('items-center gap-2 mb-1'): ui.label('Subida').classes('text-xs text-gray-600') ui.icon('upload', size='sm').classes('text-blue-600') lbl_tx = ui.label('0 KB/s').classes('text-2xl font-bold text-blue-600') def update_net(): lbl_rx.set_text(f"{trafico_red['bajada_kbps']:.1f} KB/s") lbl_tx.set_text(f"{trafico_red['subida_kbps']:.1f} KB/s") ui.timer(1.0, update_net) # --- FILA 2: RECURSOS --- with ui.card().classes('w-full'): ui.label('Recursos del sistema').classes('text-gray-500 text-sm mb-2') with ui.row().classes('w-full justify-around'): # CPU with ui.column().classes('items-center'): ui.label('CPU').classes('text-xs mb-2') with ui.element('div').classes('relative'): progress_cpu = ui.circular_progress(value=0, show_value=False, color='red').props('size=80px thickness=0.15') lbl_cpu = ui.label('0%').classes('text-lg font-bold absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2') # RAM with ui.column().classes('items-center'): ui.label('RAM').classes('text-xs mb-2') with ui.element('div').classes('relative'): progress_ram = ui.circular_progress(value=0, show_value=False, color='purple').props('size=80px thickness=0.15') lbl_ram = ui.label('0%').classes('text-lg font-bold absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2') # Threads with ui.column().classes('items-center'): ui.label('Tareas Activas').classes('text-xs mb-2') lbl_threads = ui.label('0').classes('text-3xl font-mono') lbl_threads_detalle = ui.label('').classes('text-xs text-gray-500 mt-1') def update_resources(): cpu = psutil.cpu_percent() ram = psutil.virtual_memory().percent progress_cpu.set_value(cpu / 100) lbl_cpu.set_text(f"{cpu:.0f}%") progress_ram.set_value(ram / 100) lbl_ram.set_text(f"{ram:.0f}%") # Contamos tareas activas en los pools total_tareas = tareas_activas['threads'] + tareas_activas['procesos'] lbl_threads.set_text(str(total_tareas)) lbl_threads_detalle.set_text(f"{tareas_activas['threads']} threads • {tareas_activas['procesos']} procesos") ui.timer(2.0, update_resources) # --- T1: MULTIPROCESOS --- ui.label('T1 · Multiprocesos').classes('text-lg font-bold mt-4') with ui.row().classes('w-full gap-4 items-stretch'): # Lanzar Apps with ui.card().classes('flex-1 min-w-0 flex flex-col'): ui.label('Lanzar Apps').classes('text-gray-500 text-sm') sel_app = ui.select(['Safari', 'Google Chrome'], value='Safari').classes('w-full') inp_url = ui.input(placeholder='https://...').classes('w-full') def launch(): try: subprocess.Popen(['open', '-a', sel_app.value, inp_url.value]) ui.notify(f'Lanzando {sel_app.value}...', type='positive') except Exception as e: ui.notify(f'Error: {e}', type='negative') with ui.element('div').classes('flex-grow'): pass ui.button('Lanzar', on_click=launch).classes('w-full mt-2 bg-cyan-600') # Backup with ui.card().classes('flex-1 min-w-0 flex flex-col'): ui.label('Backup').classes('text-gray-500 text-sm') inp_source = ui.input(placeholder='Ruta (vacío=actual)').classes('w-full') def do_backup(): origen = inp_source.value or os.path.dirname(os.path.dirname(os.path.abspath(__file__))) destino = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backups') # Lanzar proceso usando el pool (no bloqueante) ejecutar_en_proceso(tarea_backup_process, origen, destino) ui.notify('Backup iniciado en segundo plano...', type='info') with ui.element('div').classes('flex-grow'): pass ui.button('Crear ZIP', on_click=do_backup).classes('w-full mt-2 bg-indigo-600') # Editor with ui.card().classes('flex-1 min-w-0 flex flex-col'): ui.label('Editor (Persistencia)').classes('text-gray-500 text-sm') # Cargar contenido inicial txt_editor = ui.textarea(value=get_note_content()).classes('w-full flex-grow').props('rows=3') def save_editor(): content = txt_editor.value # Lanzar proceso usando el pool (no bloqueante) ejecutar_en_proceso(save_note_content_process, content) ui.notify('Guardando notas...', type='info') ui.button('Guardar', on_click=save_editor).classes('w-full mt-2 bg-teal-600') # --- T2: MULTIHILOS --- ui.label('T2 · Multihilos').classes('text-lg font-bold mt-4') with ui.row().classes('w-full gap-4 items-stretch'): # Alarma with ui.card().classes('flex-1 min-w-0 flex flex-col'): ui.label('Alarma').classes('text-gray-500 text-sm') with ui.column().classes('w-full flex-grow items-center justify-center'): # Reloj visual con progreso circular with ui.element('div').classes('relative mb-3'): alarm_progress = ui.circular_progress(value=0, show_value=False, color='orange').props('size=100px thickness=0.1') with ui.column().classes('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 items-center'): ui.icon('alarm', size='xl').classes('text-orange-500') lbl_alarm_countdown = ui.label('').classes('text-xs font-mono text-gray-600') # Controles compactos with ui.row().classes('w-full gap-2'): inp_sec = ui.number(label='Segundos', value=10, min=1, max=300).classes('flex-1') inp_msg = ui.input(label='Mensaje', value='¡Tiempo!').classes('flex-1') # Variables para el countdown alarm_state = {'tiempo_restante': 0, 'tiempo_total': 0, 'activa': False} def update_alarm_display(): if alarm_state['activa'] and alarm_state['tiempo_restante'] > 0: alarm_state['tiempo_restante'] -= 0.1 if alarm_state['tiempo_restante'] < 0: alarm_state['tiempo_restante'] = 0 # Actualizar progreso circular (invertido: empieza en 1 y va a 0) progreso = alarm_state['tiempo_restante'] / alarm_state['tiempo_total'] alarm_progress.set_value(1 - progreso) # Mostrar tiempo restante segundos = int(alarm_state['tiempo_restante']) lbl_alarm_countdown.set_text(f"{segundos}s") elif not alarm_state['activa']: lbl_alarm_countdown.set_text('') alarm_progress.set_value(0) ui.timer(0.1, update_alarm_display) def check_completed_alarms(): # Revisamos si hay alarmas en la lista compartida while alarmas_completadas: msg = alarmas_completadas.pop(0) alarm_state['activa'] = False alarm_state['tiempo_restante'] = 0 lbl_alarm_countdown.set_text('¡ALARMA!') ui.notify(f"⏰ ALARMA: {msg}", type='warning', close_button=True, timeout=0) ui.run_javascript('new Audio("https://actions.google.com/sounds/v1/alarms/beep_short.ogg").play()') # Timer que revisa la cola de alarmas cada 500ms ui.timer(0.5, check_completed_alarms) def start_alarm(): sec = inp_sec.value msg = inp_msg.value if sec <= 0 or sec > 300: ui.notify('Tiempo debe estar entre 1 y 300 segundos', type='negative') return # Lógica del thread de alarma usando el pool def thread_logic(s, m, lista_salida): time.sleep(s) lista_salida.append(m) # Ejecutar en el pool de threads (no bloqueante) ejecutar_en_thread(thread_logic, sec, msg, alarmas_completadas) # Iniciar countdown visual alarm_state['tiempo_total'] = sec alarm_state['tiempo_restante'] = sec alarm_state['activa'] = True ui.notify(f'Alarma configurada para {sec}s', type='positive') ui.button('⏰ Iniciar Alarma', on_click=start_alarm).classes('w-full mt-2 bg-orange-500') # Scraping with ui.card().classes('flex-1 min-w-0 flex flex-col'): ui.label('Web Scraping').classes('text-gray-500 text-sm') inp_scraping_url = ui.input(placeholder='https://example.com').classes('w-full') with ui.column().classes('w-full flex-grow mt-2'): lbl_scraping_titulo = ui.label('').classes('text-xs font-bold text-gray-700') lbl_scraping_stats = ui.label('').classes('text-xs text-gray-600') lbl_scraping_desc = ui.label('').classes('text-xs text-gray-500 italic') def check_scraping_results(): while scraping_resultados: resultado = scraping_resultados.pop(0) if 'error' in resultado: lbl_scraping_titulo.set_text("❌ Error") lbl_scraping_stats.set_text(str(resultado['error'])[:60]) lbl_scraping_desc.set_text('') ui.notify(f"Error en scraping: {resultado['error']}", type='negative') else: lbl_scraping_titulo.set_text(f"✅ {resultado['titulo'][:40]}...") lbl_scraping_stats.set_text(f"{resultado['num_links']} links • {resultado['num_imagenes']} imgs • {resultado['num_parrafos']} párrafos") lbl_scraping_desc.set_text(resultado.get('descripcion', '')[:80]) ui.notify(f"Scraping completado: {resultado['titulo']}", type='positive') ui.timer(0.5, check_scraping_results) def start_scraping(): url = inp_scraping_url.value if not url or not url.startswith('http'): ui.notify('URL inválida', type='negative') return # Ejecutar en el pool de threads (no bloqueante) ejecutar_en_thread(tarea_scraping_thread, url, scraping_resultados) lbl_scraping_titulo.set_text('🔄 Scraping en progreso...') lbl_scraping_stats.set_text('') lbl_scraping_desc.set_text('') ui.notify('Scraping iniciado...', type='info') ui.button('Scrapear', on_click=start_scraping).classes('w-full mt-2 bg-purple-600') # Reproductor MP3 with ui.card().classes('flex-1 min-w-0 flex flex-col'): ui.label('Reproductor MP3').classes('text-gray-500 text-sm') # Variable para almacenar la ruta del archivo mp3_file_path = {'path': ''} with ui.column().classes('w-full flex-grow'): lbl_mp3_filename = ui.label('Sin archivo seleccionado').classes('text-xs text-gray-500 mb-2') # Upload component para seleccionar/arrastrar archivo async def handle_upload(e): print("[Upload] Evento recibido") try: # El archivo viene en e.file (FileUpload object) # Leer contenido del archivo (es async!) content = await e.file.read() # Guardar archivo temporalmente temp_path = f"/tmp/{e.file.name}" with open(temp_path, 'wb') as f: f.write(content) mp3_file_path['path'] = temp_path lbl_mp3_filename.set_text(f"{e.file.name}") ui.notify(f'Archivo cargado: {e.file.name}', type='positive') print(f"[Upload] Guardado en: {temp_path}") except Exception as ex: print(f"[Upload] Error: {ex}") ui.notify(f'Error al cargar: {ex}', type='negative') ui.upload( on_upload=handle_upload, auto_upload=True, max_files=1, label='Arrastra MP3 aquí o haz clic' ).props('accept=".mp3,audio/mpeg" color="pink"').classes('w-full') lbl_mp3_estado = ui.label('').classes('text-xs text-gray-600 mt-2') def check_mp3_status(): while mp3_estado: estado = mp3_estado.pop(0) if estado['status'] == 'completed': lbl_mp3_estado.set_text("Reproducción completada") ui.notify('Reproducción finalizada', type='positive') elif estado['status'] == 'error': lbl_mp3_estado.set_text(f"Error: {estado['error']}") ui.notify(f"Error: {estado['error']}", type='negative') ui.timer(0.5, check_mp3_status) def play_mp3(): archivo = mp3_file_path['path'] print(f"[Play] Intentando reproducir: '{archivo}'") print(f"[Play] Estado mp3_file_path: {mp3_file_path}") if not archivo: print("[Play] No hay archivo en mp3_file_path") ui.notify('Por favor, sube un archivo MP3 primero', type='warning') return if not os.path.exists(archivo): print(f"[Play] Archivo no existe: {archivo}") ui.notify('Archivo no encontrado', type='negative') return print(f"[Play] Lanzando reproducción de: {archivo}") # Ejecutar en el pool de threads (no bloqueante) ejecutar_en_thread(tarea_reproducir_mp3_thread, archivo, mp3_estado) lbl_mp3_estado.set_text('Reproduciendo...') ui.notify('Reproducción iniciada...', type='info') ui.button('Reproducir', on_click=play_mp3).classes('w-full mt-2 bg-pink-600') # --- T3: SOCKETS --- ui.label('T3 · Sockets').classes('text-lg font-bold mt-4') with ui.row().classes('w-full gap-4 items-stretch'): # Chat with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Chat').classes('text-gray-400') # TCP Server with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('TCP Server').classes('text-gray-400') # Hueco with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Placeholder').classes('text-gray-400') # --- T4: SERVICIOS --- ui.label('T4 · Servicios').classes('text-lg font-bold mt-4') with ui.row().classes('w-full gap-4 items-stretch'): # HTTP Client with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('HTTP Client').classes('text-gray-400') # Hueco with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Placeholder').classes('text-gray-400') # Hueco with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Placeholder').classes('text-gray-400') # --- T5: SEGURIDAD --- ui.label('T5 · Seguridad').classes('text-lg font-bold mt-4') with ui.row().classes('w-full gap-4 items-stretch'): # Hash SHA256 with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Hash SHA256').classes('text-gray-400') # Hueco with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Placeholder').classes('text-gray-400') # Hueco with ui.card().classes('flex-1 min-w-0 bg-gray-50 flex flex-col'): ui.label('Placeholder').classes('text-gray-400') if __name__ in {"__main__", "__mp_main__"}: ui.run(title='PSP Dashboard', port=8080, reload=True)