# Multi-Server Health Check API Pattern

Added Jul 10, 2026 to ops portal. Replaced the single-server scraped-API health with ping-based live health for all 7 ITPP servers.

## Backend (FastAPI endpoint)

```python
SERVERS = [
    {"name": "Core", "ip": "152.53.192.33", "role": "Hermes, Docker, OSINT, Caddy", "tier": "Primary"},
    {"name": "app1", "ip": "152.53.36.131", "role": "AI Stack", "tier": "Standard"},
    {"name": "app2", "ip": "152.53.39.202", "role": "UNMS, UniFi, Traccar", "tier": "Standard"},
    {"name": "app3", "ip": "152.53.241.111", "role": "WordPress", "tier": "Standard"},
    {"name": "app1-bu", "ip": "5.161.114.8", "role": "Warm Standby", "tier": "Standby"},
    {"name": "AI Box (legacy)", "ip": "178.156.167.181", "role": "LiteLLM (migrating)", "tier": "Legacy"},
    {"name": "Docker Box (legacy)", "ip": "178.156.168.35", "role": "Uptime Kuma", "tier": "Legacy"},
]

@app.get("/api/servers/health")
async def servers_health(user: str = Depends(get_current_user)):
    results = []
    for s in SERVERS:
        try:
            start = time.time()
            result = subprocess.run(["ping", "-c", "1", "-W", "2", s["ip"]],
                                    capture_output=True, text=True, timeout=5)
            latency = round((time.time() - start) * 1000)
            alive = result.returncode == 0
        except:
            alive = False
            latency = None
        results.append({...})
    return {"servers": results, "count": len(results), "live": sum(...)}
```

## Frontend (vanilla JS)

Page: `/opt/ops-portal/static/servers.html`
Uses `Ops.apiFetch('/api/servers/health')` to get all 7 servers.
Renders as card grid with border-left color (green=LIVE, red=OFFLINE).
Shows: server name, status badge, role, IP, latency, tier.

## Pitfalls

- Ping requires root or CAP_NET_RAW. The ops-portal service runs as root, so ping works.
- Some servers block ICMP (firewalls). If a server shows OFFLINE but is actually up, check UFW rules.
- Legacy servers in migration status should still be monitored — they're running production services.
- The API endpoint requires JWT auth (like all /api/* routes).
