Files
captain-claude/apps/deck/app.py
ARCHITECT d35f11e2f7 Add apps modules and improve captain_claude logging
- Add apps/ directory with modular components:
  - captain.py: Main orchestrator
  - corp/, deck/, devops/, docker/, hst/: Domain-specific apps
- Fix duplicate logger handlers in long sessions
- Add flush=True to print statements for real-time output

Note: flow-ui, mindlink, tzzr-cli are separate repos (not included)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 01:13:30 +00:00

318 lines
12 KiB
Python

#!/usr/bin/env python3
"""
TZZR DECK App - Gestión del servidor DECK (72.62.1.113)
Servicios: Agentes TZZR, Mailcow, Nextcloud, Odoo, Vaultwarden, etc.
"""
import subprocess
import json
import sys
from dataclasses import dataclass
from typing import Optional, List, Dict
from datetime import datetime
SERVER = "root@72.62.1.113"
SSH_KEY = "~/.ssh/tzzr"
# Servicios conocidos en DECK
SERVICES = {
# Microservicios TZZR
"clara": {"port": 5051, "type": "service", "desc": "Log inmutable"},
"alfred": {"port": 5052, "type": "service", "desc": "Automatización de flujos"},
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento"},
"feldman": {"port": 5054, "type": "service", "desc": "Validador Merkle"},
# Aplicaciones
"nextcloud": {"port": 8084, "type": "app", "desc": "Cloud storage", "url": "cloud.tzzrdeck.me"},
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "odoo.tzzrdeck.me", "container": "deck-odoo"},
"vaultwarden": {"port": 8085, "type": "app", "desc": "Passwords", "url": "vault.tzzrdeck.me"},
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "directus.tzzrdeck.me"},
"shlink": {"port": 8083, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrdeck.me"},
"ntfy": {"port": 8080, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrdeck.me"},
"filebrowser": {"port": 8082, "type": "app", "desc": "File manager", "url": "files.tzzrdeck.me"},
# Infraestructura
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL con pgvector"},
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
"mailcow": {"port": 8180, "type": "mail", "desc": "Servidor correo", "url": "mail.tzzr.net"},
}
@dataclass
class Result:
success: bool
data: any
error: str = ""
def ssh(cmd: str, timeout: int = 60) -> Result:
"""Ejecuta comando en DECK"""
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{cmd}"'
try:
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return Result(success=result.returncode == 0, data=result.stdout.strip(), error=result.stderr.strip())
except Exception as e:
return Result(success=False, data="", error=str(e))
def get_all_containers() -> List[Dict]:
"""Lista todos los contenedores Docker"""
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
if not result.success:
return []
containers = []
for line in result.data.split('\n'):
if line:
parts = line.split('|')
containers.append({
"name": parts[0],
"status": parts[1] if len(parts) > 1 else "",
"ports": parts[2] if len(parts) > 2 else "",
"image": parts[3] if len(parts) > 3 else ""
})
return containers
def get_service_status(service: str) -> Dict:
"""Estado detallado de un servicio"""
info = SERVICES.get(service, {})
container = info.get("container", f"{service}-service" if info.get("type") == "service" else service)
# Estado del contenedor
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
# Logs recientes
logs_result = ssh(f"docker logs {container} --tail 5 2>&1")
# Health check si tiene endpoint
health = "unknown"
if info.get("type") == "service":
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null")
health = "healthy" if health_result.success and health_result.data else "unhealthy"
return {
"service": service,
"container": container,
"status": status_result.data if status_result.success else "not found",
"health": health,
"port": info.get("port"),
"url": info.get("url"),
"logs": logs_result.data[:500] if logs_result.success else ""
}
def restart_service(service: str) -> Result:
"""Reinicia un servicio"""
info = SERVICES.get(service, {})
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
return ssh(f"docker restart {container}")
def stop_service(service: str) -> Result:
"""Detiene un servicio"""
info = SERVICES.get(service, {})
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
return ssh(f"docker stop {container}")
def start_service(service: str) -> Result:
"""Inicia un servicio"""
info = SERVICES.get(service, {})
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
return ssh(f"docker start {container}")
def get_logs(service: str, lines: int = 100) -> str:
"""Obtiene logs de un servicio"""
info = SERVICES.get(service, {})
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
result = ssh(f"docker logs {container} --tail {lines} 2>&1")
return result.data if result.success else result.error
def get_system_stats() -> Dict:
"""Estadísticas del sistema"""
stats = {}
# CPU y memoria
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
if mem_result.success:
parts = mem_result.data.split()
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
# Disco
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
if disk_result.success:
parts = disk_result.data.split()
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
# Contenedores
containers_result = ssh("docker ps -q | wc -l")
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
# Load average
load_result = ssh("cat /proc/loadavg | awk '{print $1,$2,$3}'")
if load_result.success:
parts = load_result.data.split()
stats["load"] = {"1m": parts[0], "5m": parts[1], "15m": parts[2]}
return stats
def query_service(service: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
"""Hace petición a un servicio TZZR"""
info = SERVICES.get(service)
if not info or info.get("type") != "service":
return Result(success=False, data="", error="Servicio no encontrado")
port = info["port"]
if method == "GET":
cmd = f"curl -s http://localhost:{port}{endpoint}"
else:
json_data = json.dumps(data) if data else "{}"
cmd = f"curl -s -X {method} -H 'Content-Type: application/json' -d '{json_data}' http://localhost:{port}{endpoint}"
result = ssh(cmd)
try:
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
except:
return Result(success=result.success, data=result.data)
def get_postgres_stats() -> Dict:
"""Estadísticas de PostgreSQL"""
result = ssh("sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datistemplate = false;\" -t")
databases = {}
if result.success:
for line in result.data.split('\n'):
if '|' in line:
parts = line.split('|')
databases[parts[0].strip()] = parts[1].strip()
return databases
def get_mailcow_status() -> Dict:
"""Estado de Mailcow"""
containers = ["postfix-mailcow", "dovecot-mailcow", "nginx-mailcow", "mysql-mailcow", "redis-mailcow"]
status = {}
for c in containers:
result = ssh(f"docker ps --filter name={c} --format '{{{{.Status}}}}'")
status[c.replace("-mailcow", "")] = result.data if result.success and result.data else "stopped"
return status
# CLI
def main():
if len(sys.argv) < 2:
print("""
TZZR DECK App - Servidor 72.62.1.113
====================================
Uso: python app.py <comando> [argumentos]
Comandos Generales:
status Estado general del sistema
containers Lista todos los contenedores
stats Estadísticas del sistema
Gestión de Servicios:
service <name> Estado detallado de servicio
restart <service> Reiniciar servicio
stop <service> Detener servicio
start <service> Iniciar servicio
logs <service> [lines] Ver logs
Servicios TZZR:
services Estado de todos los servicios
query <name> <endpoint> Query a servicio (GET)
Bases de Datos:
postgres Estadísticas PostgreSQL
Mail:
mailcow Estado de Mailcow
Servicios disponibles:
TZZR: clara, alfred, mason, feldman
Apps: nextcloud, odoo, vaultwarden, directus, shlink, ntfy, filebrowser
DB: postgres, redis
Mail: mailcow
""")
return
cmd = sys.argv[1]
if cmd == "status":
print("\n📊 DECK Status (72.62.1.113)")
print("=" * 40)
stats = get_system_stats()
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
print(f"⚡ Load: {stats.get('load', {}).get('1m', '?')}")
elif cmd == "containers":
containers = get_all_containers()
print(f"\n📦 Contenedores en DECK ({len(containers)} total):")
for c in containers:
icon = "" if "Up" in c["status"] else ""
print(f" {icon} {c['name']}: {c['status'][:30]}")
elif cmd == "stats":
stats = get_system_stats()
print(json.dumps(stats, indent=2))
elif cmd == "service" and len(sys.argv) >= 3:
status = get_service_status(sys.argv[2])
print(f"\n🔧 {status['service']}")
print(f" Container: {status['container']}")
print(f" Status: {status['status']}")
print(f" Health: {status['health']}")
if status.get('url'):
print(f" URL: https://{status['url']}")
if status.get('port'):
print(f" Puerto: {status['port']}")
elif cmd == "restart" and len(sys.argv) >= 3:
result = restart_service(sys.argv[2])
print(f"{'' if result.success else ''} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
elif cmd == "stop" and len(sys.argv) >= 3:
result = stop_service(sys.argv[2])
print(f"{'' if result.success else ''} {sys.argv[2]}: {'detenido' if result.success else result.error}")
elif cmd == "start" and len(sys.argv) >= 3:
result = start_service(sys.argv[2])
print(f"{'' if result.success else ''} {sys.argv[2]}: {'iniciado' if result.success else result.error}")
elif cmd == "logs" and len(sys.argv) >= 3:
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
print(get_logs(sys.argv[2], lines))
elif cmd == "services":
print("\n⚙️ Servicios TZZR en DECK:")
for svc in ["clara", "alfred", "mason", "feldman"]:
status = get_service_status(svc)
icon = "" if "Up" in status["status"] else ""
health = f"({status['health']})" if status['health'] != "unknown" else ""
print(f" {icon} {svc}: {status['status'][:25]} {health} - {SERVICES[svc]['desc']}")
elif cmd == "query" and len(sys.argv) >= 4:
result = query_service(sys.argv[2], sys.argv[3])
if result.success:
print(json.dumps(result.data, indent=2) if isinstance(result.data, dict) else result.data)
else:
print(f"❌ Error: {result.error}")
elif cmd == "postgres":
dbs = get_postgres_stats()
print("\n🐘 PostgreSQL en DECK:")
for db, size in dbs.items():
print(f"{db}: {size}")
elif cmd == "mailcow":
status = get_mailcow_status()
print("\n📧 Mailcow Status:")
for service, st in status.items():
icon = "" if "Up" in st else ""
print(f" {icon} {service}: {st[:30]}")
else:
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
if __name__ == "__main__":
main()