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

274 lines
9.9 KiB
Python

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