feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 648fea8dcb
783 changed files with 221554 additions and 0 deletions
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# health-check.sh Comprehensive health check for Claude Code Agent Monitor
#
# Usage:
# ./health-check.sh --url http://localhost:4820
# ./health-check.sh --url http://host:port --retries 30 --interval 5
# ./health-check.sh --url http://host:port --json
# ./health-check.sh --help
# ─────────────────────────────────────────────────────────────────────────────
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
# ── Colors & logging ───────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; }
info() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${BLUE}${NC} $*"; }
ok() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${GREEN}${NC} $*"; }
warn() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${YELLOW}${NC} $*" >&2; }
err() { [[ "$JSON_OUTPUT" == true ]] && return; echo -e "${CYAN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} ${RED}${NC} $*" >&2; }
# ── Defaults ────────────────────────────────────────────────────────────────
BASE_URL=""
RETRIES=30
INTERVAL=5
TIMEOUT=5
RESPONSE_THRESHOLD=2000 # milliseconds
JSON_OUTPUT=false
CHECK_WEBSOCKET=true
HEALTH_PATH="/api/health"
# ── Usage ───────────────────────────────────────────────────────────────────
usage() {
cat <<HELP
${BOLD}Usage:${NC}
$(basename "$0") --url <base-url> [options]
${BOLD}Required:${NC}
--url, -u Base URL (e.g., http://localhost:4820)
${BOLD}Options:${NC}
--retries, -r Max retry attempts (default: 30)
--interval, -i Seconds between retries (default: 5)
--timeout HTTP request timeout in seconds (default: 5)
--threshold Max response time in ms (default: 2000)
--path Health endpoint path (default: /api/health)
--no-websocket Skip WebSocket connectivity check
--json Output results as JSON
--help, -h Show this help message
${BOLD}Exit codes:${NC}
0 All checks passed
1 One or more checks failed
${BOLD}Examples:${NC}
$(basename "$0") --url http://localhost:4820
$(basename "$0") --url https://monitor.example.com --retries 10 --json
$(basename "$0") --url http://10.0.1.5:4820 --threshold 500 --no-websocket
HELP
exit 0
}
# ── Argument parsing ────────────────────────────────────────────────────────
parse_args() {
[[ $# -eq 0 ]] && usage
while [[ $# -gt 0 ]]; do
case "$1" in
--url|-u) BASE_URL="$2"; shift 2 ;;
--retries|-r) RETRIES="$2"; shift 2 ;;
--interval|-i) INTERVAL="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--threshold) RESPONSE_THRESHOLD="$2"; shift 2 ;;
--path) HEALTH_PATH="$2"; shift 2 ;;
--no-websocket) CHECK_WEBSOCKET=false; shift ;;
--json) JSON_OUTPUT=true; shift ;;
--help|-h) usage ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
[[ -z "$BASE_URL" ]] && { echo "Missing required argument: --url" >&2; exit 1; }
# Strip trailing slash
BASE_URL="${BASE_URL%/}"
}
# ── HTTP health check ──────────────────────────────────────────────────────
check_http_health() {
local url="${BASE_URL}${HEALTH_PATH}"
local attempt=0
local http_ok=false
local status_code=""
local response_body=""
local response_time_ms=0
info "Checking HTTP health: ${url}"
while [[ $attempt -lt $RETRIES ]]; do
attempt=$((attempt + 1))
# Measure response time and capture output
local start_ns
start_ns=$(date +%s%N 2>/dev/null || echo "0")
local http_response
http_response=$(curl -sf \
--max-time "${TIMEOUT}" \
--write-out "\n%{http_code}\n%{time_total}" \
"${url}" 2>/dev/null) || true
local end_ns
end_ns=$(date +%s%N 2>/dev/null || echo "0")
if [[ -n "$http_response" ]]; then
response_body=$(echo "$http_response" | head -n -2)
status_code=$(echo "$http_response" | tail -2 | head -1)
local time_total
time_total=$(echo "$http_response" | tail -1)
# Convert seconds to milliseconds
response_time_ms=$(echo "$time_total" | awk '{printf "%.0f", $1 * 1000}' 2>/dev/null || echo "0")
if [[ "$status_code" == "200" ]] && echo "$response_body" | grep -q '"status":"ok"'; then
http_ok=true
break
fi
fi
if [[ $attempt -lt $RETRIES ]]; then
info "Attempt ${attempt}/${RETRIES} waiting ${INTERVAL}s..."
sleep "${INTERVAL}"
fi
done
# Results
HTTP_OK="$http_ok"
HTTP_STATUS="$status_code"
HTTP_BODY="$response_body"
HTTP_RESPONSE_TIME_MS="$response_time_ms"
HTTP_ATTEMPTS="$attempt"
if [[ "$http_ok" == true ]]; then
ok "HTTP health check passed (${response_time_ms}ms, ${attempt} attempt(s))"
else
err "HTTP health check failed after ${attempt} attempts"
fi
}
# ── Response time check ────────────────────────────────────────────────────
check_response_time() {
if [[ "$HTTP_OK" != true ]]; then
LATENCY_OK=false
return
fi
if [[ "$HTTP_RESPONSE_TIME_MS" -le "$RESPONSE_THRESHOLD" ]]; then
LATENCY_OK=true
ok "Response time ${HTTP_RESPONSE_TIME_MS}ms within threshold (${RESPONSE_THRESHOLD}ms)"
else
LATENCY_OK=false
warn "Response time ${HTTP_RESPONSE_TIME_MS}ms exceeds threshold (${RESPONSE_THRESHOLD}ms)"
fi
}
# ── WebSocket connectivity check ───────────────────────────────────────────
check_websocket() {
WS_OK=false
if [[ "$CHECK_WEBSOCKET" == false ]]; then
info "WebSocket check skipped"
WS_OK=true # treat as pass when skipped
return
fi
# Construct WebSocket URL
local ws_url="${BASE_URL}"
ws_url="${ws_url/http:/ws:}"
ws_url="${ws_url/https:/wss:}"
ws_url="${ws_url}/ws"
info "Checking WebSocket: ${ws_url}"
# Check if we have a WebSocket testing tool
if command -v websocat &>/dev/null; then
if echo "" | websocat --one-message -t "${ws_url}" 2>/dev/null; then
WS_OK=true
ok "WebSocket connection succeeded (websocat)"
return
fi
fi
# Fallback: use curl with upgrade headers to test the handshake
local ws_status
ws_status=$(curl -sf \
--max-time "${TIMEOUT}" \
-o /dev/null \
-w "%{http_code}" \
-H "Upgrade: websocket" \
-H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" \
"${BASE_URL}/ws" 2>/dev/null) || ws_status="000"
# 101 = Switching Protocols (WebSocket upgrade success)
# 400 = Bad Request (server recognized WS but rejected still proves WS is available)
case "$ws_status" in
101)
WS_OK=true
ok "WebSocket handshake succeeded (HTTP 101)"
;;
400|426)
WS_OK=true
ok "WebSocket endpoint reachable (HTTP ${ws_status} server recognized upgrade)"
;;
*)
# Try Node.js one-liner as last resort
if command -v node &>/dev/null; then
local node_result
node_result=$(node -e "
const ws = new (require('ws'))('${ws_url}');
const t = setTimeout(() => { process.stdout.write('timeout'); process.exit(1); }, 5000);
ws.on('open', () => { clearTimeout(t); process.stdout.write('ok'); ws.close(); process.exit(0); });
ws.on('error', (e) => { clearTimeout(t); process.stdout.write('error:' + e.message); process.exit(1); });
" 2>/dev/null) || node_result="error"
if [[ "$node_result" == "ok" ]]; then
WS_OK=true
ok "WebSocket connection verified (node)"
else
WS_OK=false
warn "WebSocket check failed: ${node_result}"
fi
else
warn "WebSocket check inconclusive (no ws testing tool available, HTTP status: ${ws_status})"
WS_OK=true # Don't fail the whole check for this
fi
;;
esac
}
# ── Output results ──────────────────────────────────────────────────────────
output_results() {
local overall_healthy=true
[[ "$HTTP_OK" != true ]] && overall_healthy=false
[[ "$LATENCY_OK" != true ]] && overall_healthy=false
[[ "$WS_OK" != true ]] && overall_healthy=false
if [[ "$JSON_OUTPUT" == true ]]; then
cat <<JSON
{
"healthy": ${overall_healthy},
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"url": "${BASE_URL}",
"checks": {
"http": {
"passed": ${HTTP_OK},
"status_code": "${HTTP_STATUS:-null}",
"response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0},
"attempts": ${HTTP_ATTEMPTS:-0},
"body": $(echo "${HTTP_BODY:-null}" | head -c 500 | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null || echo '"null"')
},
"latency": {
"passed": ${LATENCY_OK},
"response_time_ms": ${HTTP_RESPONSE_TIME_MS:-0},
"threshold_ms": ${RESPONSE_THRESHOLD}
},
"websocket": {
"passed": ${WS_OK},
"checked": ${CHECK_WEBSOCKET}
}
}
}
JSON
else
echo ""
echo -e "${BOLD}Health Check Summary${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local http_icon=$([[ "$HTTP_OK" == true ]] && echo "${GREEN}${NC}" || echo "${RED}${NC}")
local latency_icon=$([[ "$LATENCY_OK" == true ]] && echo "${GREEN}${NC}" || echo "${YELLOW}${NC}")
local ws_icon=$([[ "$WS_OK" == true ]] && echo "${GREEN}${NC}" || echo "${RED}${NC}")
echo -e " ${http_icon} HTTP /api/health (${HTTP_STATUS:-???}, ${HTTP_RESPONSE_TIME_MS:-?}ms, ${HTTP_ATTEMPTS:-?} attempts)"
echo -e " ${latency_icon} Response time (${HTTP_RESPONSE_TIME_MS:-?}ms / ${RESPONSE_THRESHOLD}ms threshold)"
echo -e " ${ws_icon} WebSocket (checked: ${CHECK_WEBSOCKET})"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "$overall_healthy" == true ]]; then
echo -e " ${GREEN}${BOLD}Overall: HEALTHY${NC}"
else
echo -e " ${RED}${BOLD}Overall: UNHEALTHY${NC}"
fi
echo ""
fi
if [[ "$overall_healthy" == true ]]; then
return 0
else
return 1
fi
}
# ── Main ────────────────────────────────────────────────────────────────────
main() {
parse_args "$@"
# Initialize result variables
HTTP_OK=false
HTTP_STATUS=""
HTTP_BODY=""
HTTP_RESPONSE_TIME_MS=0
HTTP_ATTEMPTS=0
LATENCY_OK=false
WS_OK=false
[[ "$JSON_OUTPUT" != true ]] && {
echo ""
echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}${GREEN}║ Claude Code Agent Monitor Health Check ║${NC}"
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════╝${NC}"
echo ""
info "Target: ${BASE_URL}"
info "Config: retries=${RETRIES}, interval=${INTERVAL}s, threshold=${RESPONSE_THRESHOLD}ms"
echo ""
}
check_http_health
check_response_time
check_websocket
output_results
}
main "$@"