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>
This commit is contained in:
ARCHITECT
2026-01-02 01:13:30 +00:00
parent acffd2a2a7
commit d35f11e2f7
7 changed files with 1796 additions and 38 deletions

165
apps/captain.py Normal file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
CAPTAIN CLAUDE - CLI Unificado para el Sistema TZZR
Punto de entrada principal para todas las apps
"""
import sys
import os
import subprocess
APPS_DIR = os.path.dirname(os.path.abspath(__file__))
APPS = {
"devops": {
"path": f"{APPS_DIR}/devops/app.py",
"desc": "Gestión de despliegues y construcción",
"alias": ["deploy", "build"]
},
"deck": {
"path": f"{APPS_DIR}/deck/app.py",
"desc": "Servidor DECK (72.62.1.113) - Agentes, Mail, Apps",
"alias": ["d"]
},
"corp": {
"path": f"{APPS_DIR}/corp/app.py",
"desc": "Servidor CORP (92.112.181.188) - Margaret, Jared, Mason, Feldman",
"alias": ["c"]
},
"hst": {
"path": f"{APPS_DIR}/hst/app.py",
"desc": "Servidor HST (72.62.2.84) - Directus, Imágenes",
"alias": ["h"]
},
"docker": {
"path": f"{APPS_DIR}/docker/app.py",
"desc": "Gestión Docker multi-servidor",
"alias": ["dk"]
}
}
def print_banner():
print("""
╔═══════════════════════════════════════════════════════════════╗
║ CAPTAIN CLAUDE ║
║ Sistema Multiagente TZZR ║
╠═══════════════════════════════════════════════════════════════╣
║ Servidor Central: 69.62.126.110 (Gitea, PostgreSQL) ║
║ DECK: 72.62.1.113 │ CORP: 92.112.181.188 ║
║ HST: 72.62.2.84 │ R2: Cloudflare Storage ║
╚═══════════════════════════════════════════════════════════════╝
""")
def print_help():
print_banner()
print("Uso: python captain.py <app> [comando] [argumentos]\n")
print("Apps disponibles:")
print("" * 60)
for name, info in APPS.items():
aliases = ", ".join(info["alias"]) if info["alias"] else ""
alias_str = f" (alias: {aliases})" if aliases else ""
print(f" {name}{alias_str}")
print(f" └─ {info['desc']}")
print()
print("Ejemplos:")
print(" python captain.py devops deploy clara deck")
print(" python captain.py deck agents")
print(" python captain.py corp pending")
print(" python captain.py docker ps all")
print(" python captain.py hst directus")
print()
print("Atajos rápidos:")
print(" python captain.py d agents # DECK agents")
print(" python captain.py c flows # CORP flows")
print(" python captain.py dk dashboard # Docker dashboard")
def resolve_app(name: str) -> str:
"""Resuelve nombre o alias a nombre de app"""
if name in APPS:
return name
for app_name, info in APPS.items():
if name in info.get("alias", []):
return app_name
return None
def run_app(app: str, args: list):
"""Ejecuta una app con los argumentos dados"""
app_name = resolve_app(app)
if not app_name:
print(f"❌ App no encontrada: {app}")
print(f" Apps disponibles: {', '.join(APPS.keys())}")
return 1
app_path = APPS[app_name]["path"]
if not os.path.exists(app_path):
print(f"❌ Archivo no encontrado: {app_path}")
return 1
cmd = ["python3", app_path] + args
try:
result = subprocess.run(cmd)
return result.returncode
except KeyboardInterrupt:
print("\n⚠️ Interrumpido")
return 130
except Exception as e:
print(f"❌ Error: {e}")
return 1
def quick_status():
"""Muestra estado rápido de todos los servidores"""
print_banner()
print("📊 Estado rápido del sistema:\n")
servers = [
("DECK", "root@72.62.1.113"),
("CORP", "root@92.112.181.188"),
("HST", "root@72.62.2.84")
]
for name, host in servers:
cmd = f"ssh -i ~/.ssh/tzzr -o ConnectTimeout=3 {host} 'docker ps -q | wc -l' 2>/dev/null"
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
if result.returncode == 0:
count = result.stdout.strip()
print(f"{name} ({host.split('@')[1]}): {count} contenedores")
else:
print(f"{name} ({host.split('@')[1]}): No responde")
except:
print(f"{name} ({host.split('@')[1]}): Timeout")
print()
def main():
if len(sys.argv) < 2:
print_help()
return 0
cmd = sys.argv[1]
# Comandos especiales
if cmd in ["-h", "--help", "help"]:
print_help()
return 0
if cmd in ["status", "s"]:
quick_status()
return 0
if cmd in ["version", "-v", "--version"]:
print("Captain Claude v1.0.0")
return 0
# Ejecutar app
args = sys.argv[2:] if len(sys.argv) > 2 else []
return run_app(cmd, args)
if __name__ == "__main__":
sys.exit(main())

343
apps/corp/app.py Normal file
View File

@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
TZZR CORP App - Gestión del servidor CORP (92.112.181.188)
Servicios: Agentes Margaret/Jared/Mason/Feldman, Context Manager, Apps
"""
import subprocess
import json
import sys
from dataclasses import dataclass
from typing import List, Dict, Optional
from datetime import datetime
SERVER = "root@92.112.181.188"
SSH_KEY = "~/.ssh/tzzr"
# Servicios conocidos en CORP
SERVICES = {
# Microservicios TZZR
"margaret": {"port": 5051, "type": "service", "desc": "Recepción de datos", "path": "/opt/margaret"},
"jared": {"port": 5052, "type": "service", "desc": "Automatización de flujos", "path": "/opt/jared"},
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento", "path": "/opt/mason"},
"feldman": {"port": 5054, "type": "service", "desc": "Registro final + Merkle", "path": "/opt/feldman"},
# Aplicaciones
"nextcloud": {"port": 8080, "type": "app", "desc": "Cloud storage", "url": "nextcloud.tzzrcorp.me"},
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "tzzrcorp.me"},
"vaultwarden": {"port": 8081, "type": "app", "desc": "Passwords", "url": "vault.tzzrcorp.me"},
"shlink": {"port": 8082, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrcorp.me"},
"addy": {"port": 8083, "type": "app", "desc": "Email aliases", "url": "addy.tzzrcorp.me"},
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "erp.tzzrcorp.me"},
"ntfy": {"port": 8880, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrcorp.me"},
# APIs y Automatización
"hsu": {"port": 5002, "type": "api", "desc": "HSU User Library", "url": "hsu.tzzrcorp.me"},
"windmill": {"port": 8000, "type": "automation", "desc": "Automatización de flujos", "url": "windmill.tzzrcorp.me"},
"context-manager": {"port": None, "type": "system", "desc": "Context Manager IA", "path": "/opt/context-manager"},
# Infraestructura
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL 16"},
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
}
@dataclass
class Result:
success: bool
data: any
error: str = ""
def ssh(cmd: str, timeout: int = 60) -> Result:
"""Ejecuta comando en CORP"""
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 = f"{service}-service" if info.get("type") == "agent" else service
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
health = "unknown"
if info.get("port"):
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 100")
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"),
"desc": info.get("desc")
}
def restart_service(service: str) -> Result:
"""Reinicia un servicio"""
info = SERVICES.get(service, {})
container = f"{service}-service" if info.get("type") == "agent" else service
return ssh(f"docker restart {container}")
def get_logs(service: str, lines: int = 100) -> str:
"""Obtiene logs de un servicio"""
info = SERVICES.get(service, {})
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 query_agent(agent: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
"""Hace petición a un agente TZZR"""
info = SERVICES.get(agent)
if not info or info.get("type") != "agent":
return Result(success=False, data="", error="Agente 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_system_stats() -> Dict:
"""Estadísticas del sistema"""
stats = {}
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]}
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]}
containers_result = ssh("docker ps -q | wc -l")
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
return stats
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 ingest_to_margaret(contenedor: dict, auth_key: str) -> Result:
"""Envía datos a Margaret para ingestión"""
json_data = json.dumps(contenedor)
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5051/ingest"
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 execute_flow(flow_id: int, data: dict, auth_key: str) -> Result:
"""Ejecuta un flujo en Jared"""
json_data = json.dumps(data)
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5052/ejecutar/{flow_id}"
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_pending_issues() -> Result:
"""Obtiene incidencias pendientes de Mason"""
result = ssh("curl -s http://localhost:5053/pendientes")
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 verify_merkle(hash: str) -> Result:
"""Verifica registro en Feldman"""
result = ssh(f"curl -s http://localhost:5054/verify/{hash}")
try:
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
except:
return Result(success=result.success, data=result.data)
# CLI
def main():
if len(sys.argv) < 2:
print("""
TZZR CORP App - Servidor 92.112.181.188
=======================================
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
logs <service> [lines] Ver logs
Agentes TZZR (Flujo de datos):
agents Estado de todos los agentes
agent <name> <endpoint> Query a agente (GET)
margaret Estado de Margaret (Secretaria)
jared Estado de Jared (Flujos)
mason Estado de Mason (Editor)
feldman Estado de Feldman (Contable)
flows Lista flujos predefinidos en Jared
pending Incidencias pendientes en Mason
verify <hash> Verificar hash en Feldman
Bases de Datos:
postgres Estadísticas PostgreSQL
Servicios disponibles:
Agentes: margaret, jared, mason, feldman
Apps: nextcloud, directus, vaultwarden, shlink, addy, odoo, ntfy
APIs: hsu, context-manager
DB: postgres, redis
""")
return
cmd = sys.argv[1]
if cmd == "status":
print("\n📊 CORP Status (92.112.181.188)")
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)}")
elif cmd == "containers":
containers = get_all_containers()
print(f"\n📦 Contenedores en CORP ({len(containers)} total):")
for c in containers:
icon = "" if "Up" in c["status"] else ""
print(f" {icon} {c['name']}: {c['status'][:30]}")
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']}")
print(f" Desc: {status.get('desc', '')}")
if status.get('url'):
print(f" URL: https://{status['url']}")
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 == "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 == "agents":
print("\n🤖 Agentes TZZR en CORP:")
print(" PACKET → MARGARET → JARED → MASON/FELDMAN")
print()
for agent in ["margaret", "jared", "mason", "feldman"]:
status = get_service_status(agent)
icon = "" if "Up" in status["status"] else ""
health = f"({status['health']})" if status['health'] != "unknown" else ""
print(f" {icon} {agent}: {status['status'][:20]} {health}")
print(f" └─ {SERVICES[agent]['desc']}")
elif cmd == "agent" and len(sys.argv) >= 4:
result = query_agent(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 in ["margaret", "jared", "mason", "feldman"]:
status = get_service_status(cmd)
print(f"\n🤖 {cmd.upper()} - {SERVICES[cmd]['desc']}")
print(f" Puerto: {SERVICES[cmd]['port']}")
print(f" Status: {status['status']}")
print(f" Health: {status['health']}")
# Endpoints específicos
contracts = query_agent(cmd, "/s-contract")
if contracts.success and isinstance(contracts.data, dict):
print(f" Versión: {contracts.data.get('version', 'N/A')}")
elif cmd == "flows":
result = query_agent("jared", "/flujos")
if result.success:
flows = result.data if isinstance(result.data, list) else []
print("\n📋 Flujos predefinidos en Jared:")
for f in flows:
print(f" • [{f.get('id')}] {f.get('nombre')}")
elif cmd == "pending":
result = get_pending_issues()
if result.success:
issues = result.data if isinstance(result.data, list) else []
print(f"\n⚠️ Incidencias pendientes en Mason: {len(issues)}")
for i in issues[:10]:
print(f"{i.get('id')}: {i.get('tipo', 'N/A')} - {i.get('descripcion', '')[:40]}")
elif cmd == "verify" and len(sys.argv) >= 3:
result = verify_merkle(sys.argv[2])
if result.success:
print(json.dumps(result.data, indent=2))
else:
print(f"❌ Error: {result.error}")
elif cmd == "postgres":
dbs = get_postgres_stats()
print("\n🐘 PostgreSQL en CORP:")
for db, size in dbs.items():
print(f"{db}: {size}")
else:
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
if __name__ == "__main__":
main()

317
apps/deck/app.py Normal file
View File

@@ -0,0 +1,317 @@
#!/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()

230
apps/devops/app.py Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
TZZR DevOps App - Gestión de despliegues y construcción
"""
import subprocess
import sys
from dataclasses import dataclass
from typing import Optional
import json
# Configuración de servidores
SERVERS = {
"deck": {"host": "root@72.62.1.113", "name": "DECK"},
"corp": {"host": "root@92.112.181.188", "name": "CORP"},
"hst": {"host": "root@72.62.2.84", "name": "HST"},
"local": {"host": None, "name": "LOCAL (69.62.126.110)"}
}
SSH_KEY = "~/.ssh/tzzr"
# Agentes conocidos
AGENTS = {
"deck": ["clara", "alfred", "mason", "feldman"],
"corp": ["margaret", "jared", "mason", "feldman"],
"hst": ["hst-api", "directus_hst", "directus_lumalia", "directus_personal"]
}
@dataclass
class CommandResult:
success: bool
output: str
error: str = ""
def ssh_cmd(server: str, command: str) -> CommandResult:
"""Ejecuta comando SSH en servidor remoto"""
if server == "local":
full_cmd = command
else:
host = SERVERS[server]["host"]
full_cmd = f'ssh -i {SSH_KEY} {host} "{command}"'
try:
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=60)
return CommandResult(
success=result.returncode == 0,
output=result.stdout.strip(),
error=result.stderr.strip()
)
except subprocess.TimeoutExpired:
return CommandResult(success=False, output="", error="Timeout")
except Exception as e:
return CommandResult(success=False, output="", error=str(e))
def deploy_agent(agent: str, server: str) -> CommandResult:
"""Despliega un agente en el servidor especificado"""
print(f"🚀 Desplegando {agent} en {server}...")
# Usar el script de deploy en DECK
cmd = f"/opt/scripts/deploy-agent.sh {agent} {server}"
result = ssh_cmd("deck", cmd)
if result.success:
print(f"{agent} desplegado exitosamente en {server}")
else:
print(f"❌ Error desplegando {agent}: {result.error}")
return result
def backup_postgres(server: str = "deck") -> CommandResult:
"""Ejecuta backup de PostgreSQL"""
print(f"💾 Ejecutando backup en {server}...")
result = ssh_cmd(server, "/opt/scripts/backup_postgres.sh")
if result.success:
print("✅ Backup completado")
else:
print(f"❌ Error: {result.error}")
return result
def sync_r2() -> CommandResult:
"""Sincroniza backups a R2"""
print("☁️ Sincronizando con R2...")
result = ssh_cmd("deck", "/opt/scripts/sync_backups_r2.sh")
if result.success:
print("✅ Sincronización completada")
else:
print(f"❌ Error: {result.error}")
return result
def onboard_user(email: str, username: str) -> CommandResult:
"""Da de alta un nuevo usuario"""
print(f"👤 Creando usuario {username} ({email})...")
result = ssh_cmd("deck", f"/opt/scripts/onboard-user.sh {email} {username}")
if result.success:
print(f"✅ Usuario {username} creado")
else:
print(f"❌ Error: {result.error}")
return result
def get_agent_status(server: str) -> dict:
"""Obtiene estado de agentes en un servidor"""
agents = AGENTS.get(server, [])
status = {}
for agent in agents:
result = ssh_cmd(server, f"docker ps --filter name={agent} --format '{{{{.Status}}}}'")
status[agent] = result.output if result.success else "unknown"
return status
def restart_agent(agent: str, server: str) -> CommandResult:
"""Reinicia un agente específico"""
print(f"🔄 Reiniciando {agent} en {server}...")
result = ssh_cmd(server, f"docker restart {agent}-service 2>/dev/null || docker restart {agent}")
if result.success:
print(f"{agent} reiniciado")
else:
print(f"❌ Error: {result.error}")
return result
def get_logs(agent: str, server: str, lines: int = 50) -> CommandResult:
"""Obtiene logs de un agente"""
container = f"{agent}-service" if agent in ["clara", "alfred", "mason", "feldman", "margaret", "jared"] else agent
return ssh_cmd(server, f"docker logs {container} --tail {lines} 2>&1")
def list_deployments() -> dict:
"""Lista todos los deployments activos"""
deployments = {}
for server in ["deck", "corp", "hst"]:
result = ssh_cmd(server, "docker ps --format '{{.Names}}|{{.Status}}|{{.Ports}}'")
if result.success:
containers = []
for line in result.output.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 ""
})
deployments[server] = containers
return deployments
def git_pull_all(server: str) -> dict:
"""Hace git pull en todos los proyectos de /opt"""
result = ssh_cmd(server, "for d in /opt/*/; do echo \"=== $d ===\"; cd $d && git pull 2>/dev/null || echo 'no git'; done")
return {"output": result.output, "success": result.success}
# CLI
def main():
if len(sys.argv) < 2:
print("""
TZZR DevOps App
===============
Uso: python app.py <comando> [argumentos]
Comandos:
deploy <agent> <server> Despliega un agente
backup [server] Ejecuta backup PostgreSQL
sync Sincroniza backups a R2
onboard <email> <username> Alta de usuario
status <server> Estado de agentes
restart <agent> <server> Reinicia agente
logs <agent> <server> Ver logs de agente
list Lista todos los deployments
pull <server> Git pull en todos los proyectos
Servidores: deck, corp, hst, local
Agentes DECK: clara, alfred, mason, feldman
Agentes CORP: margaret, jared, mason, feldman
""")
return
cmd = sys.argv[1]
if cmd == "deploy" and len(sys.argv) >= 4:
deploy_agent(sys.argv[2], sys.argv[3])
elif cmd == "backup":
server = sys.argv[2] if len(sys.argv) > 2 else "deck"
backup_postgres(server)
elif cmd == "sync":
sync_r2()
elif cmd == "onboard" and len(sys.argv) >= 4:
onboard_user(sys.argv[2], sys.argv[3])
elif cmd == "status" and len(sys.argv) >= 3:
status = get_agent_status(sys.argv[2])
print(f"\n📊 Estado de agentes en {sys.argv[2]}:")
for agent, st in status.items():
icon = "" if "Up" in st else ""
print(f" {icon} {agent}: {st}")
elif cmd == "restart" and len(sys.argv) >= 4:
restart_agent(sys.argv[2], sys.argv[3])
elif cmd == "logs" and len(sys.argv) >= 4:
result = get_logs(sys.argv[2], sys.argv[3])
print(result.output)
elif cmd == "list":
deployments = list_deployments()
for server, containers in deployments.items():
print(f"\n📦 {server.upper()}:")
for c in containers[:10]:
print(f"{c['name']}: {c['status']}")
if len(containers) > 10:
print(f" ... y {len(containers)-10} más")
elif cmd == "pull" and len(sys.argv) >= 3:
result = git_pull_all(sys.argv[2])
print(result["output"])
else:
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
if __name__ == "__main__":
main()

425
apps/docker/app.py Normal file
View File

@@ -0,0 +1,425 @@
#!/usr/bin/env python3
"""
TZZR Docker App - Gestión unificada de Docker en todos los servidores
Servidores: DECK (72.62.1.113), CORP (92.112.181.188), HST (72.62.2.84)
"""
import subprocess
import json
import sys
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime
# Configuración de servidores
SERVERS = {
"deck": {
"host": "root@72.62.1.113",
"name": "DECK",
"desc": "Producción - Agentes TZZR, Mail, Apps"
},
"corp": {
"host": "root@92.112.181.188",
"name": "CORP",
"desc": "Aplicaciones - Margaret, Jared, Mason, Feldman"
},
"hst": {
"host": "root@72.62.2.84",
"name": "HST",
"desc": "Contenido - Directus, Imágenes"
}
}
SSH_KEY = "~/.ssh/tzzr"
@dataclass
class Container:
name: str
status: str
image: str
ports: str = ""
created: str = ""
server: str = ""
@property
def is_running(self) -> bool:
return "Up" in self.status
@property
def icon(self) -> str:
return "" if self.is_running else ""
@dataclass
class Result:
success: bool
data: any
error: str = ""
def ssh(server: str, cmd: str, timeout: int = 60) -> Result:
"""Ejecuta comando SSH"""
host = SERVERS[server]["host"]
full_cmd = f'ssh -i {SSH_KEY} {host} "{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 subprocess.TimeoutExpired:
return Result(success=False, data="", error="Timeout")
except Exception as e:
return Result(success=False, data="", error=str(e))
def get_containers(server: str, all: bool = False) -> List[Container]:
"""Lista contenedores de un servidor"""
flag = "-a" if all else ""
result = ssh(server, f"docker ps {flag} --format '{{{{.Names}}}}|{{{{.Status}}}}|{{{{.Image}}}}|{{{{.Ports}}}}'")
if not result.success:
return []
containers = []
for line in result.data.split('\n'):
if line:
parts = line.split('|')
containers.append(Container(
name=parts[0],
status=parts[1] if len(parts) > 1 else "",
image=parts[2] if len(parts) > 2 else "",
ports=parts[3] if len(parts) > 3 else "",
server=server
))
return containers
def get_all_containers(all: bool = False) -> Dict[str, List[Container]]:
"""Lista contenedores de todos los servidores"""
result = {}
for server in SERVERS:
result[server] = get_containers(server, all)
return result
def container_action(server: str, container: str, action: str) -> Result:
"""Ejecuta acción en contenedor (start, stop, restart, rm)"""
return ssh(server, f"docker {action} {container}")
def get_logs(server: str, container: str, lines: int = 100, follow: bool = False) -> str:
"""Obtiene logs de contenedor"""
follow_flag = "-f" if follow else ""
result = ssh(server, f"docker logs {container} --tail {lines} {follow_flag} 2>&1", timeout=120 if follow else 60)
return result.data if result.success else result.error
def inspect_container(server: str, container: str) -> Dict:
"""Inspecciona contenedor"""
result = ssh(server, f"docker inspect {container}")
if result.success:
try:
return json.loads(result.data)[0]
except:
return {}
return {}
def get_images(server: str) -> List[Dict]:
"""Lista imágenes Docker"""
result = ssh(server, "docker images --format '{{.Repository}}|{{.Tag}}|{{.Size}}|{{.ID}}'")
if not result.success:
return []
images = []
for line in result.data.split('\n'):
if line:
parts = line.split('|')
images.append({
"repository": parts[0],
"tag": parts[1] if len(parts) > 1 else "",
"size": parts[2] if len(parts) > 2 else "",
"id": parts[3] if len(parts) > 3 else ""
})
return images
def get_networks(server: str) -> List[Dict]:
"""Lista redes Docker"""
result = ssh(server, "docker network ls --format '{{.Name}}|{{.Driver}}|{{.Scope}}'")
if not result.success:
return []
networks = []
for line in result.data.split('\n'):
if line:
parts = line.split('|')
networks.append({
"name": parts[0],
"driver": parts[1] if len(parts) > 1 else "",
"scope": parts[2] if len(parts) > 2 else ""
})
return networks
def get_volumes(server: str) -> List[Dict]:
"""Lista volúmenes Docker"""
result = ssh(server, "docker volume ls --format '{{.Name}}|{{.Driver}}'")
if not result.success:
return []
volumes = []
for line in result.data.split('\n'):
if line:
parts = line.split('|')
volumes.append({
"name": parts[0],
"driver": parts[1] if len(parts) > 1 else ""
})
return volumes
def docker_stats(server: str) -> List[Dict]:
"""Estadísticas de contenedores"""
result = ssh(server, "docker stats --no-stream --format '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}'")
if not result.success:
return []
stats = []
for line in result.data.split('\n'):
if line:
parts = line.split('|')
stats.append({
"name": parts[0],
"cpu": parts[1] if len(parts) > 1 else "",
"memory": parts[2] if len(parts) > 2 else "",
"network": parts[3] if len(parts) > 3 else ""
})
return stats
def prune(server: str, what: str = "all") -> Result:
"""Limpia recursos Docker no usados"""
if what == "all":
return ssh(server, "docker system prune -f")
elif what == "images":
return ssh(server, "docker image prune -f")
elif what == "volumes":
return ssh(server, "docker volume prune -f")
elif what == "networks":
return ssh(server, "docker network prune -f")
return Result(success=False, data="", error="Tipo no válido")
def compose_action(server: str, path: str, action: str) -> Result:
"""Ejecuta docker compose en un directorio"""
return ssh(server, f"cd {path} && docker compose {action}")
def exec_container(server: str, container: str, command: str) -> Result:
"""Ejecuta comando dentro de contenedor"""
return ssh(server, f"docker exec {container} {command}")
def find_container(name: str) -> List[tuple]:
"""Busca contenedor por nombre en todos los servidores"""
results = []
for server in SERVERS:
containers = get_containers(server, all=True)
for c in containers:
if name.lower() in c.name.lower():
results.append((server, c))
return results
def get_system_df(server: str) -> Dict:
"""Uso de disco de Docker"""
result = ssh(server, "docker system df --format '{{.Type}}|{{.Size}}|{{.Reclaimable}}'")
if not result.success:
return {}
df = {}
for line in result.data.split('\n'):
if line:
parts = line.split('|')
df[parts[0]] = {
"size": parts[1] if len(parts) > 1 else "",
"reclaimable": parts[2] if len(parts) > 2 else ""
}
return df
# CLI
def main():
if len(sys.argv) < 2:
print("""
TZZR Docker App - Gestión Multi-servidor
=========================================
Uso: python app.py <comando> [argumentos]
Servidores: deck, corp, hst (o 'all' para todos)
Contenedores:
ps [server] Lista contenedores activos
ps -a [server] Lista todos los contenedores
start <server> <name> Iniciar contenedor
stop <server> <name> Detener contenedor
restart <server> <name> Reiniciar contenedor
rm <server> <name> Eliminar contenedor
logs <server> <name> Ver logs
inspect <server> <name> Inspeccionar contenedor
exec <server> <name> <cmd> Ejecutar comando en contenedor
find <name> Buscar contenedor en todos los servidores
Recursos:
images <server> Lista imágenes
networks <server> Lista redes
volumes <server> Lista volúmenes
stats <server> Estadísticas de recursos
df <server> Uso de disco Docker
Mantenimiento:
prune <server> [type] Limpiar recursos (all/images/volumes/networks)
Compose:
up <server> <path> docker compose up -d
down <server> <path> docker compose down
build <server> <path> docker compose build
Dashboard:
dashboard Vista general de todos los servidores
""")
return
cmd = sys.argv[1]
# PS - Lista contenedores
if cmd == "ps":
show_all = "-a" in sys.argv
server = None
for arg in sys.argv[2:]:
if arg != "-a" and arg in SERVERS:
server = arg
if server:
containers = get_containers(server, show_all)
print(f"\n📦 {SERVERS[server]['name']} ({len(containers)} contenedores):")
for c in containers:
print(f" {c.icon} {c.name}: {c.status[:30]}")
else:
# Todos los servidores
all_containers = get_all_containers(show_all)
for srv, containers in all_containers.items():
print(f"\n📦 {SERVERS[srv]['name']} ({len(containers)}):")
for c in containers[:15]:
print(f" {c.icon} {c.name}: {c.status[:25]}")
if len(containers) > 15:
print(f" ... y {len(containers)-15} más")
# Acciones de contenedor
elif cmd in ["start", "stop", "restart", "rm"] and len(sys.argv) >= 4:
server, container = sys.argv[2], sys.argv[3]
result = container_action(server, container, cmd)
icon = "" if result.success else ""
action_name = {"start": "iniciado", "stop": "detenido", "restart": "reiniciado", "rm": "eliminado"}
print(f"{icon} {container} {action_name.get(cmd, cmd)}" if result.success else f"❌ Error: {result.error}")
# Logs
elif cmd == "logs" and len(sys.argv) >= 4:
server, container = sys.argv[2], sys.argv[3]
lines = int(sys.argv[4]) if len(sys.argv) > 4 else 50
print(get_logs(server, container, lines))
# Inspect
elif cmd == "inspect" and len(sys.argv) >= 4:
server, container = sys.argv[2], sys.argv[3]
info = inspect_container(server, container)
print(json.dumps(info, indent=2)[:3000])
# Exec
elif cmd == "exec" and len(sys.argv) >= 5:
server, container = sys.argv[2], sys.argv[3]
command = " ".join(sys.argv[4:])
result = exec_container(server, container, command)
print(result.data if result.success else f"Error: {result.error}")
# Find
elif cmd == "find" and len(sys.argv) >= 3:
name = sys.argv[2]
results = find_container(name)
if results:
print(f"\n🔍 Encontrados {len(results)} contenedores con '{name}':")
for server, c in results:
print(f" {c.icon} [{server}] {c.name}: {c.status[:25]}")
else:
print(f"❌ No se encontró '{name}'")
# Images
elif cmd == "images" and len(sys.argv) >= 3:
server = sys.argv[2]
images = get_images(server)
print(f"\n🖼️ Imágenes en {server}:")
for img in images[:20]:
print(f"{img['repository']}:{img['tag']} ({img['size']})")
# Networks
elif cmd == "networks" and len(sys.argv) >= 3:
server = sys.argv[2]
networks = get_networks(server)
print(f"\n🌐 Redes en {server}:")
for net in networks:
print(f"{net['name']} ({net['driver']})")
# Volumes
elif cmd == "volumes" and len(sys.argv) >= 3:
server = sys.argv[2]
volumes = get_volumes(server)
print(f"\n💾 Volúmenes en {server}:")
for vol in volumes:
print(f"{vol['name']}")
# Stats
elif cmd == "stats" and len(sys.argv) >= 3:
server = sys.argv[2]
stats = docker_stats(server)
print(f"\n📊 Estadísticas en {server}:")
for s in stats[:15]:
print(f" {s['name'][:20]}: CPU {s['cpu']} | MEM {s['memory']}")
# DF
elif cmd == "df" and len(sys.argv) >= 3:
server = sys.argv[2]
df = get_system_df(server)
print(f"\n💿 Uso de Docker en {server}:")
for type_, info in df.items():
print(f" {type_}: {info['size']} (recuperable: {info['reclaimable']})")
# Prune
elif cmd == "prune" and len(sys.argv) >= 3:
server = sys.argv[2]
what = sys.argv[3] if len(sys.argv) > 3 else "all"
result = prune(server, what)
print(f"{'' if result.success else ''} Limpieza completada" if result.success else f"Error: {result.error}")
# Compose
elif cmd in ["up", "down", "build"] and len(sys.argv) >= 4:
server, path = sys.argv[2], sys.argv[3]
action = f"{cmd} -d" if cmd == "up" else cmd
result = compose_action(server, path, action)
print(f"{'' if result.success else ''} compose {cmd}" if result.success else f"Error: {result.error}")
# Dashboard
elif cmd == "dashboard":
print("\n" + "=" * 60)
print("🐳 TZZR Docker Dashboard")
print("=" * 60)
for server, info in SERVERS.items():
containers = get_containers(server)
running = sum(1 for c in containers if c.is_running)
print(f"\n📦 {info['name']} ({info['host'].split('@')[1]})")
print(f" {info['desc']}")
print(f" Contenedores: {running}/{len(containers)} activos")
# Top 5 contenedores
for c in containers[:5]:
print(f" {c.icon} {c.name}")
else:
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
if __name__ == "__main__":
main()

273
apps/hst/app.py Normal file
View File

@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
TZZR HST App - Gestión del servidor HST (72.62.2.84)
Servicios: Directus (3 instancias), Servidor de imágenes, APIs
"""
import subprocess
import json
import sys
from dataclasses import dataclass
from typing import List, Dict
SERVER = "root@72.62.2.84"
SSH_KEY = "~/.ssh/tzzr"
# Servicios conocidos en HST
SERVICES = {
# Directus instances
"directus_hst": {"port": 8055, "type": "cms", "desc": "Directus HST principal", "url": "hst.tzrtech.org"},
"directus_lumalia": {"port": 8056, "type": "cms", "desc": "Directus Lumalia", "url": "lumalia.tzrtech.org"},
"directus_personal": {"port": 8057, "type": "cms", "desc": "Directus Personal", "url": "personal.tzrtech.org"},
# APIs
"hst-api": {"port": 5001, "type": "api", "desc": "HST Flask API"},
"hst-images": {"port": 80, "type": "web", "desc": "Servidor NGINX imágenes", "url": "tzrtech.org"},
# Infraestructura
"postgres_hst": {"port": 5432, "type": "db", "desc": "PostgreSQL 15"},
"filebrowser": {"port": 8081, "type": "app", "desc": "File Browser"},
}
@dataclass
class Result:
success: bool
data: any
error: str = ""
def ssh(cmd: str, timeout: int = 60) -> Result:
"""Ejecuta comando en HST"""
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, {})
status_result = ssh(f"docker ps --filter name={service} --format '{{{{.Status}}}}'")
health = "unknown"
if info.get("port") and info.get("type") in ["cms", "api"]:
if info.get("type") == "cms":
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/server/health 2>/dev/null | head -c 50")
else:
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 50")
health = "healthy" if health_result.success and health_result.data else "unhealthy"
return {
"service": service,
"status": status_result.data if status_result.success else "not found",
"health": health,
"port": info.get("port"),
"url": info.get("url"),
"desc": info.get("desc")
}
def restart_service(service: str) -> Result:
"""Reinicia un servicio"""
return ssh(f"docker restart {service}")
def get_logs(service: str, lines: int = 100) -> str:
"""Obtiene logs de un servicio"""
result = ssh(f"docker logs {service} --tail {lines} 2>&1")
return result.data if result.success else result.error
def get_system_stats() -> Dict:
"""Estadísticas del sistema"""
stats = {}
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]}
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]}
containers_result = ssh("docker ps -q | wc -l")
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
return stats
def query_directus(instance: str, endpoint: str, token: str = None) -> Result:
"""Hace petición a una instancia de Directus"""
info = SERVICES.get(instance)
if not info or info.get("type") != "cms":
return Result(success=False, data="", error="Instancia Directus no encontrada")
port = info["port"]
auth = f"-H 'Authorization: Bearer {token}'" if token else ""
cmd = f"curl -s {auth} 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_directus_collections(instance: str) -> List[str]:
"""Lista colecciones de una instancia Directus"""
result = query_directus(instance, "/collections")
if result.success and isinstance(result.data, dict):
collections = result.data.get("data", [])
return [c.get("collection") for c in collections if not c.get("collection", "").startswith("directus_")]
return []
def list_images(path: str = "/var/www/images") -> List[str]:
"""Lista imágenes en el servidor"""
result = ssh(f"ls -la {path} 2>/dev/null | head -20")
return result.data.split('\n') if result.success else []
def get_postgres_databases() -> List[str]:
"""Lista bases de datos PostgreSQL"""
result = ssh("docker exec postgres_hst psql -U postgres -c '\\l' -t 2>/dev/null")
if not result.success:
return []
databases = []
for line in result.data.split('\n'):
if '|' in line:
db = line.split('|')[0].strip()
if db and db not in ['template0', 'template1']:
databases.append(db)
return databases
# CLI
def main():
if len(sys.argv) < 2:
print("""
TZZR HST App - Servidor 72.62.2.84
==================================
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
logs <service> [lines] Ver logs
Directus (CMS):
directus Estado de las 3 instancias
collections <instance> Lista colecciones de instancia
query <instance> <path> Query a Directus API
Imágenes:
images [path] Lista imágenes
Bases de Datos:
postgres Lista bases de datos
Servicios disponibles:
CMS: directus_hst, directus_lumalia, directus_personal
API: hst-api
Web: hst-images
DB: postgres_hst
App: filebrowser
""")
return
cmd = sys.argv[1]
if cmd == "status":
print("\n📊 HST Status (72.62.2.84)")
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)}")
elif cmd == "containers":
containers = get_all_containers()
print(f"\n📦 Contenedores en HST ({len(containers)} total):")
for c in containers:
icon = "" if "Up" in c["status"] else ""
print(f" {icon} {c['name']}: {c['status'][:30]}")
elif cmd == "service" and len(sys.argv) >= 3:
status = get_service_status(sys.argv[2])
print(f"\n🔧 {status['service']}")
print(f" Status: {status['status']}")
print(f" Health: {status['health']}")
print(f" Desc: {status.get('desc', '')}")
if status.get('url'):
print(f" URL: https://{status['url']}")
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 == "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 == "directus":
print("\n📚 Instancias Directus en HST:")
for instance in ["directus_hst", "directus_lumalia", "directus_personal"]:
status = get_service_status(instance)
icon = "" if "Up" in status["status"] else ""
health = f"({status['health']})" if status['health'] != "unknown" else ""
url = f"https://{status['url']}" if status.get('url') else ""
print(f" {icon} {instance}: {status['status'][:20]} {health}")
print(f" └─ {url}")
elif cmd == "collections" and len(sys.argv) >= 3:
collections = get_directus_collections(sys.argv[2])
print(f"\n📋 Colecciones en {sys.argv[2]}:")
for c in collections:
print(f"{c}")
elif cmd == "query" and len(sys.argv) >= 4:
result = query_directus(sys.argv[2], sys.argv[3])
if result.success:
print(json.dumps(result.data, indent=2)[:2000])
else:
print(f"❌ Error: {result.error}")
elif cmd == "images":
path = sys.argv[2] if len(sys.argv) > 2 else "/var/www/images"
images = list_images(path)
print(f"\n🖼️ Imágenes en {path}:")
for img in images[:20]:
print(f" {img}")
elif cmd == "postgres":
dbs = get_postgres_databases()
print("\n🐘 Bases de datos PostgreSQL:")
for db in dbs:
print(f"{db}")
else:
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
if __name__ == "__main__":
main()

View File

@@ -98,6 +98,11 @@ class CaptainClaude:
self.cost_tracker = CostTracker() self.cost_tracker = CostTracker()
logger = logging.getLogger("captain-claude") logger = logging.getLogger("captain-claude")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
# Evitar handlers duplicados en sesiones largas
if not logger.handlers:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(message)s'))
logger.addHandler(handler)
self.logger = StructuredLogger(logger) self.logger = StructuredLogger(logger)
self.output_dir = Path(output_dir) if output_dir else Path.cwd() / "captain_output" self.output_dir = Path(output_dir) if output_dir else Path.cwd() / "captain_output"
self.output_dir.mkdir(exist_ok=True) self.output_dir.mkdir(exist_ok=True)
@@ -244,33 +249,33 @@ Continue and complete this work."""
"""Execute a task using intelligent agent orchestration.""" """Execute a task using intelligent agent orchestration."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
print(f"\n{'='*60}") print(f"\n{'='*60}", flush=True)
print("CAPTAIN CLAUDE by dadiaar") print("CAPTAIN CLAUDE by dadiaar", flush=True)
print(f"{'='*60}") print(f"{'='*60}", flush=True)
print(f"Task: {task[:100]}...") print(f"Task: {task[:100]}...", flush=True)
print(f"{'='*60}\n") print(f"{'='*60}\n", flush=True)
# Phase 1: Analyze task # Phase 1: Analyze task
print("[Captain] Analyzing task...") print("[Captain] Analyzing task...", flush=True)
plan = await self.analyze_task(task) plan = await self.analyze_task(task)
print(f"[Captain] Plan created: {len(plan.get('steps', []))} steps") print(f"[Captain] Plan created: {len(plan.get('steps', []))} steps", flush=True)
# Phase 2: Execute plan # Phase 2: Execute plan
results = [] results = []
if plan.get("parallel_possible") and len(plan.get("agents_needed", [])) > 1: if plan.get("parallel_possible") and len(plan.get("agents_needed", [])) > 1:
print("[Captain] Executing agents in parallel...") print("[Captain] Executing agents in parallel...", flush=True)
parallel_result = await self.run_parallel( parallel_result = await self.run_parallel(
task, task,
plan.get("agents_needed", ["coder"]) plan.get("agents_needed", ["coder"])
) )
results.append(parallel_result) results.append(parallel_result)
else: else:
print("[Captain] Executing agents sequentially...") print("[Captain] Executing agents sequentially...", flush=True)
sequential_results = await self.run_sequential(plan.get("steps", [])) sequential_results = await self.run_sequential(plan.get("steps", []))
results.extend(sequential_results) results.extend(sequential_results)
# Phase 3: Synthesize results # Phase 3: Synthesize results
print("[Captain] Synthesizing results...") print("[Captain] Synthesizing results...", flush=True)
synthesis_prompt = f"""Synthesize these results into a coherent final output: synthesis_prompt = f"""Synthesize these results into a coherent final output:
Original task: {task} Original task: {task}
@@ -298,12 +303,12 @@ Provide a clear, actionable final result."""
with open(output_file, "w") as f: with open(output_file, "w") as f:
json.dump(final_result, f, indent=2, default=str) json.dump(final_result, f, indent=2, default=str)
print(f"\n{'='*60}") print(f"\n{'='*60}", flush=True)
print("EXECUTION COMPLETE") print("EXECUTION COMPLETE", flush=True)
print(f"{'='*60}") print(f"{'='*60}", flush=True)
print(f"Output saved: {output_file}") print(f"Output saved: {output_file}", flush=True)
print(f"Cost: {self.cost_tracker.summary()}") print(f"Cost: {self.cost_tracker.summary()}", flush=True)
print(f"{'='*60}\n") print(f"{'='*60}\n", flush=True)
return final_result return final_result
@@ -322,18 +327,18 @@ async def main():
"""Interactive Captain Claude session.""" """Interactive Captain Claude session."""
captain = CaptainClaude() captain = CaptainClaude()
print("\n" + "="*60) print("\n" + "="*60, flush=True)
print("CAPTAIN CLAUDE by dadiaar") print("CAPTAIN CLAUDE by dadiaar", flush=True)
print("Multi-Agent Orchestration System") print("Multi-Agent Orchestration System", flush=True)
print("="*60) print("="*60, flush=True)
print("\nCommands:") print("\nCommands:", flush=True)
print(" /execute <task> - Full multi-agent execution") print(" /execute <task> - Full multi-agent execution", flush=True)
print(" /chat <message> - Chat with Captain") print(" /chat <message> - Chat with Captain", flush=True)
print(" /agent <name> <message> - Chat with specific agent") print(" /agent <name> <message> - Chat with specific agent", flush=True)
print(" /parallel <task> - Run all agents in parallel") print(" /parallel <task> - Run all agents in parallel", flush=True)
print(" /cost - Show cost summary") print(" /cost - Show cost summary", flush=True)
print(" /quit - Exit") print(" /quit - Exit", flush=True)
print("="*60 + "\n") print("="*60 + "\n", flush=True)
while True: while True:
try: try:
@@ -343,24 +348,24 @@ async def main():
continue continue
if user_input.lower() == "/quit": if user_input.lower() == "/quit":
print("Goodbye!") print("Goodbye!", flush=True)
break break
if user_input.lower() == "/cost": if user_input.lower() == "/cost":
print(f"Cost: {captain.cost_tracker.summary()}") print(f"Cost: {captain.cost_tracker.summary()}", flush=True)
continue continue
if user_input.startswith("/execute "): if user_input.startswith("/execute "):
task = user_input[9:] task = user_input[9:]
result = await captain.execute(task) result = await captain.execute(task)
print(f"\nFinal Output:\n{result['final_output']}\n") print(f"\nFinal Output:\n{result['final_output']}\n", flush=True)
continue continue
if user_input.startswith("/parallel "): if user_input.startswith("/parallel "):
task = user_input[10:] task = user_input[10:]
agents = ["coder", "reviewer", "researcher"] agents = ["coder", "reviewer", "researcher"]
result = await captain.run_parallel(task, agents) result = await captain.run_parallel(task, agents)
print(f"\nParallel Results:\n{result}\n") print(f"\nParallel Results:\n{result}\n", flush=True)
continue continue
if user_input.startswith("/agent "): if user_input.startswith("/agent "):
@@ -368,24 +373,24 @@ async def main():
if len(parts) == 2: if len(parts) == 2:
agent, message = parts agent, message = parts
response = await captain.chat(message, agent) response = await captain.chat(message, agent)
print(f"\n[{agent}]: {response}\n") print(f"\n[{agent}]: {response}\n", flush=True)
else: else:
print("Usage: /agent <name> <message>") print("Usage: /agent <name> <message>", flush=True)
continue continue
if user_input.startswith("/chat ") or not user_input.startswith("/"): if user_input.startswith("/chat ") or not user_input.startswith("/"):
message = user_input[6:] if user_input.startswith("/chat ") else user_input message = user_input[6:] if user_input.startswith("/chat ") else user_input
response = await captain.chat(message) response = await captain.chat(message)
print(f"\n[Captain]: {response}\n") print(f"\n[Captain]: {response}\n", flush=True)
continue continue
print("Unknown command. Use /quit to exit.") print("Unknown command. Use /quit to exit.", flush=True)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nGoodbye!") print("\nGoodbye!", flush=True)
break break
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Error: {e}", flush=True)
if __name__ == "__main__": if __name__ == "__main__":