Files
captain-claude/apps/corp/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

344 lines
14 KiB
Python

#!/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()