# Service Health Check Cron

## Pattern

A no_agent cron that checks all critical services every 5 minutes. Silent on success. Reports only when something is down. Implemented at `~/.hermes/scripts/service-health-check.sh`.

## What it checks

| # | Check | Method |
|---|-------|--------|
| 1 | Caddy (sign.itpropartner.com) | `systemctl is-active` |
| 2 | MySQL tunnel (wphost02) | `systemctl is-active` |
| 3 | SearXNG container | `docker ps --format` |
| 4 | DocuSeal container | `docker ps --format` |
| 5 | Portal mockup (port 8081) | `curl -sf` |
| 6 | DocuSeal web (port 3000) | `curl -sf` |
| 7 | SearXNG search (port 8888) | `curl -sf` with search query |
| 8 | WireGuard (home router) | `ping 10.77.0.2` |
| 9 | MySQL through tunnel | `mysql --skip-ssl -e "SELECT 1"` |
| 10 | wphost02 SSH | `ssh -o ConnectTimeout=5 hostname` |
| 11 | Hermes gateway process | `ps aux | grep` |
| 12 | Cloudflare API token | `curl -X GET /tokens/verify` |

## Cron setup

```bash
hermes cron create \
  --name "service-health-check" \
  --schedule "every 5m" \
  --no_agent \
  --script service-health-check.sh
```

## Script design

- `exit 0` when all services healthy — no output, no delivery
- `exit 1` when any service fails — prints a bullet list of failed checks
- Failed services array tracks which checks failed
- Each check is independent — one failure doesn't abort the script
- Check count shown in alert message

## Pitfalls

- **Sensitive info in error output:** The MySQL connection string appears in the script. If a service fails, the error output may include the full password. The cron delivery is Telegram (visible only to user), not email.
- **False negatives on restart:** During a container restart, the health check may fire before the service comes back up. The 5-minute interval means this resolves within one cycle.
- **Token expiry:** The Cloudflare API token check is a direct API call. If the token expires, ALL Cloudflare checks fail. The script does not differentiate between expired token and Cloudflare outage.
- **MySQL credential in plaintext:** The password is in the script file and .env. If the cron output is ever delivered to the wrong channel, credentials leak. Consider moving MySQL check to a separate script that sources credentials from .env with tighter permissions.

## ⚠️ Fixed Bug: Bearer token sent as literal asterisks

**Root cause discovered Jul 7, 2026:** Line 38 of the script reads the token from `.env` into a variable, but line 39 sends `Authorization: Bearer ***` (literal asterisks) instead of `Authorization: Bearer $TOKEN`. This caused the health check to report the Cloudflare API token as "unhealthy" every 5 minutes, even though the actual token was valid.

**Before (broken):**
```bash
TOKEN=$(grep CLOUDFLARE_API_TOKEN ~/.hermes/.env | cut -d= -f2-)
curl -sf -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
  -H "Authorization: Bearer ***" | ...
```

**After (fixed):**
```bash
CLOUD_TOKEN=$(grep CLOUDFLARE_API_TOKEN ~/.hermes/.env | cut -d= -f2-)
curl -sf -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
  -H "Authorization: Bearer $CLOUD_TOKEN" | ...
```

Variable name was changed from `$TOKEN` to `$CLOUD_TOKEN` to avoid any confusion with other use of `TOKEN` in the script or environment.
