phone-health.sh
#!/usr/bin/env bash
# ==============================================================================
# phone-health.sh — S21 Phone 건강 검진 스크립트 v3
# ==============================================================================
# 사용법:
# bash phone-health.sh # 기본 검진 (비파괴)
# bash phone-health.sh --full # 전체 검진 (사진/녹음 포함)
# bash phone-health.sh --telegram # 검진 + 텔레그램 보고
#
# 결과: _notebook/health/YYYY-MM-DD_HHMM.json 자동 저장
# ==============================================================================
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
HEALTH_DIR="${BASE_DIR}/_notebook/health"
MCP_PORT="${MCP_PORT:-3456}"
NOW="$(date +%Y-%m-%d_%H%M)"
TS="$(date '+%Y-%m-%d %H:%M:%S %Z')"
REPORT_FILE="${HEALTH_DIR}/${NOW}.json"
mkdir -p "$HEALTH_DIR"
# Termux PATH 반드시 선두에
export PATH="/data/data/com.termux/files/usr/bin:$PATH"
PASS_CNT=0; WARN_CNT=0; FAIL_CNT=0; SKIP_CNT=0
# ============================================================
# Helpers
# ============================================================
OK="✅"; WARN="⚠️"; FAIL="❌"; SKIP="⏭️"; INFO="ℹ️"
info() { echo " $INFO $1"; }
ok() { echo " $OK $1"; PASS_CNT=$((PASS_CNT+1)); }
warn() { echo " $WARN $1"; WARN_CNT=$((WARN_CNT+1)); }
fail() { echo " $FAIL $1"; FAIL_CNT=$((FAIL_CNT+1)); }
skip() { echo " $SKIP $1"; SKIP_CNT=$((SKIP_CNT+1)); }
sect() { echo ""; echo "━━━ $* ━━━"; }
# 핵심: Termux 명령은 반드시 직접 실행 (bash -c 말고)
tx() { "$@" 2>/dev/null || true; }
# JSON 필드 추출 (multiline JSON 대비, 따옴표/공백 제거)
jget() {
echo "$1" | sed -n "s/.*\"$2\": *\"\{0,1\}\([^\",}]*\)\"\{0,1\}.*/\1/p" | head -1
}
# ============================================================
# 1. 시스템 기본
# ============================================================
sect "1. 시스템 기본"
info "Kernel: $(uname -r 2>/dev/null || echo '?')"
info "Host: $(uname -n 2>/dev/null || echo '?')"
MCP_HTTP=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${MCP_PORT}/health" 2>/dev/null || echo "000")
MCP_OKAY=$(curl -s "http://localhost:${MCP_PORT}/health" 2>/dev/null | grep -c '"status":"ok"' || true)
[ "$MCP_HTTP" = "200" ] && [ "$MCP_OKAY" -ge 1 ] && ok "phone-mcp-server (port ${MCP_PORT})" || fail "phone-mcp-server (HTTP ${MCP_HTTP})"
info "MCP tools: 18 (phone-mcp-server)"
info "Disk: $(df -h /data/data/com.termux/files 2>/dev/null | awk 'NR==2{print $4" free / "$2" total"}')"
GH_HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "https://helena751107.github.io/helena_phone/" 2>/dev/null || echo "000")
[ "$GH_HTTP" = "200" ] && ok "GitHub Pages (HTTP ${GH_HTTP})" || warn "GitHub Pages (HTTP ${GH_HTTP})"
# ============================================================
# 1.5 메모리 / 스왑 (OOM 방지)
# ============================================================
sect "1.5 메모리 / 스왑"
MEM_INFO=$(cat /proc/meminfo 2>/dev/null)
MEM_TOTAL=$(echo "$MEM_INFO" | awk '/^MemTotal:/{print $2}')
MEM_AVAIL=$(echo "$MEM_INFO" | awk '/^MemAvailable:/{print $2}')
MEM_FREE=$(echo "$MEM_INFO" | awk '/^MemFree:/{print $2}')
SWAP_TOTAL=$(echo "$MEM_INFO" | awk '/^SwapTotal:/{print $2}')
SWAP_FREE=$(echo "$MEM_INFO" | awk '/^SwapFree:/{print $2}')
MEM_AVAIL_MB=$((MEM_AVAIL / 1024))
MEM_TOTAL_MB=$((MEM_TOTAL / 1024))
SWAP_USED_MB=$(( (SWAP_TOTAL - SWAP_FREE) / 1024 ))
SWAP_TOTAL_MB=$((SWAP_TOTAL / 1024))
SWAP_PCT=$(( (SWAP_TOTAL - SWAP_FREE) * 100 / (SWAP_TOTAL + 1) ))
info "Available: ${MEM_AVAIL_MB}MB / ${MEM_TOTAL_MB}MB total"
info "Swap: ${SWAP_USED_MB}MB used / ${SWAP_TOTAL_MB}MB total (${SWAP_PCT}%)"
if [ "$MEM_AVAIL_MB" -lt 500 ] 2>/dev/null; then
fail "Available memory below 500MB (OOM risk: ${MEM_AVAIL_MB}MB)"
elif [ "$MEM_AVAIL_MB" -lt 1024 ] 2>/dev/null; then
warn "Available memory below 1GB (${MEM_AVAIL_MB}MB)"
else
ok "Memory OK (${MEM_AVAIL_MB}MB available)"
fi
if [ "$SWAP_PCT" -gt 80 ] 2>/dev/null; then
fail "Swap > 80% (${SWAP_PCT}% — OOM imminent)"
elif [ "$SWAP_PCT" -gt 60 ] 2>/dev/null; then
warn "Swap > 60% (${SWAP_PCT}%)"
else
ok "Swap OK (${SWAP_PCT}% used)"
fi
# 고아 프로세스 경고
ORPHAN_COUNT=$(ps aux 2>/dev/null | grep -cE '([b]fs|[f]ind) /' || true)
if [ "$ORPHAN_COUNT" -gt 0 ] 2>/dev/null; then
warn "Orphan bfs scanners: ${ORPHAN_COUNT} (kill -9 them)"
fi
CLAUDE_COUNT=$(ps aux 2>/dev/null | grep '[c]laude --dangerously-skip-permissions' | grep -vc '[p]root' || true)
if [ "$CLAUDE_COUNT" -gt 1 ] 2>/dev/null; then
warn "Multiple Claude sessions: ${CLAUDE_COUNT} (should be 1)"
fi
# ============================================================
# 2. 전원 / 배터리 — 가장 중요
# ============================================================
sect "2. 전원 / 배터리"
BAT=$(termux-battery-status 2>/dev/null || echo "")
if [ -n "$BAT" ] && [ "$BAT" != "{}" ]; then
ok "battery-status sensor"
BAT_PCT=$(jget "$BAT" "percentage")
BAT_TMP=$(jget "$BAT" "temperature")
BAT_STS=$(jget "$BAT" "status")
BAT_HTH=$(jget "$BAT" "health")
BAT_PLG=$(jget "$BAT" "plugged")
BAT_VLT=$(jget "$BAT" "voltage")
BAT_CUR=$(jget "$BAT" "current_average")
info "${BAT_PCT}% / ${BAT_TMP}°C / ${BAT_STS} / ${BAT_HTH} / plugged=${BAT_PLG}"
info "Voltage: ${BAT_VLT}mV | Current avg: ${BAT_CUR}mA"
[ -n "$BAT_PCT" ] && [ "$BAT_PCT" -lt 15 ] 2>/dev/null && warn "배터리 임계 (${BAT_PCT}% < 15%)"
[ -n "$BAT_TMP" ] && [ "${BAT_TMP%.*}" -gt 45 ] 2>/dev/null && warn "온도 과열 (${BAT_TMP}°C > 45°C)"
else
fail "battery-status sensor (no data)"
BAT_PCT=""; BAT_TMP=""; BAT_STS="?"; BAT_HTH="?"; BAT_PLG="?"
fi
# ============================================================
# 3. WiFi / 네트워크
# ============================================================
sect "3. WiFi / 네트워크"
WIFI=$(termux-wifi-connectioninfo 2>/dev/null || echo "")
if [ -n "$WIFI" ] && [ "$WIFI" != "{}" ]; then
ok "WiFi connection info"
WIFI_SSID=$(jget "$WIFI" "ssid")
WIFI_IP=$(jget "$WIFI" "ip")
WIFI_RSSI=$(jget "$WIFI" "rssi")
WIFI_LINK=$(jget "$WIFI" "link_speed_mbps")
info "SSID: ${WIFI_SSID:-?} | IP: ${WIFI_IP:-?} | RSSI: ${WIFI_RSSI:-?}dBm | Link: ${WIFI_LINK:-?}Mbps"
[ -n "$WIFI_RSSI" ] && [ "$WIFI_RSSI" -lt -80 ] 2>/dev/null && warn "WiFi 신호 약함 (RSSI ${WIFI_RSSI}dBm)"
else
fail "WiFi connection info"
WIFI_SSID="?"; WIFI_IP="?"; WIFI_RSSI=""
fi
EXT_G=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "https://www.google.com" 2>/dev/null || echo "000")
EXT_H=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "https://github.com" 2>/dev/null || echo "000")
info "Google: HTTP ${EXT_G} | GitHub: HTTP ${EXT_H}"
# ============================================================
# 4. 하드웨어 센서
# ============================================================
sect "4. 하드웨어 센서"
tx termux-torch on
sleep 1
tx termux-torch off
echo " $OK Flashlight (ON→1s→OFF)"; PASS_CNT=$((PASS_CNT+1))
tx termux-vibrate -d 100
echo " $OK Vibrate (100ms)"; PASS_CNT=$((PASS_CNT+1))
VOL=$(termux-volume 2>/dev/null || echo "")
if [ -n "$VOL" ] && [ "$VOL" != "[]" ]; then
ok "Volume sensor"
VM=$(echo "$VOL" | grep -oP '"stream":"music","volume":\K[0-9]+' || echo "?")
VR=$(echo "$VOL" | grep -oP '"stream":"ring","volume":\K[0-9]+' || echo "?")
VN=$(echo "$VOL" | grep -oP '"stream":"notification","volume":\K[0-9]+' || echo "?")
info "Music: ${VM} | Ring: ${VR} | Notification: ${VN}"
else
warn "Volume sensor"
fi
AI=$(termux-audio-info 2>/dev/null || echo "")
[ -n "$AI" ] && [ "$AI" != "{}" ] && ok "Audio info" || warn "Audio info"
SN=$(termux-sensor -l 2>/dev/null || echo "")
SC=$(echo "$SN" | wc -l)
[ -n "$SN" ] && ok "Sensors (${SC} available)" || warn "Sensors (unavailable)"
# ============================================================
# 5. GPS / 위치
# ============================================================
sect "5. 위치 / GPS"
GPS=$(timeout 8 termux-location -p gps 2>/dev/null || echo "")
if [ -n "$GPS" ] && echo "$GPS" | grep -q "latitude"; then
ok "GPS location"
GLAT=$(jget "$GPS" "latitude"); GLON=$(jget "$GPS" "longitude")
GACC=$(jget "$GPS" "accuracy"); GALT=$(jget "$GPS" "altitude")
info "(${GLAT}, ${GLON}) ±${GACC}m alt=${GALT}m"
else
GPS2=$(timeout 5 termux-location -p network 2>/dev/null || echo "")
if [ -n "$GPS2" ] && echo "$GPS2" | grep -q "latitude"; then
GLAT=$(jget "$GPS2" "latitude"); GLON=$(jget "$GPS2" "longitude")
warn "GPS (network fallback: ${GLAT}, ${GLON})"
else
warn "GPS (both providers failed)"
fi
fi
# ============================================================
# 6. 카메라 / 미디어
# ============================================================
sect "6. 카메라 / 미디어"
CI=$(termux-camera-info 2>/dev/null || echo "")
if [ -n "$CI" ] && [ "$CI" != "[]" ]; then
ok "Camera info"
echo " ${INFO} $(echo "$CI" | grep -oP '"facing":"[^"]*"' | tr '\n' ' ')"
else
warn "Camera info"
fi
if [ "${1:-}" = "--full" ]; then
tx termux-camera-photo -c 1 /tmp/hc_${NOW}.jpg
PS=$(ls -la /tmp/hc_${NOW}.jpg 2>/dev/null | awk '{print $5}' || echo "0")
[ "$PS" -gt 1000 ] 2>/dev/null && ok "Camera photo (${PS}B)" || fail "Camera photo"
rm -f /tmp/hc_${NOW}.jpg 2>/dev/null
else
skip "Camera photo (--full)"
fi
tx termux-microphone-record -d 1 -l 1 -f /tmp/hc_mic.m4a 2>/dev/null
sleep 1
tx termux-microphone-record -q 2>/dev/null
MS=$(ls -la /tmp/hc_mic.m4a 2>/dev/null | awk '{print $5}' || echo "0")
rm -f /tmp/hc_mic.m4a 2>/dev/null
[ "$MS" -gt 100 ] 2>/dev/null && ok "Microphone (${MS}B)" || warn "Microphone"
TTS=$(termux-tts-speak "health check ok" 2>/dev/null && echo "ok" || echo "fail")
[ "$TTS" = "ok" ] && ok "TTS" || warn "TTS"
# ============================================================
# 7. 클립보드
# ============================================================
sect "7. 클립보드"
echo "S21HC_${NOW}" | termux-clipboard-set 2>/dev/null
sleep 0.3
CG=$(termux-clipboard-get 2>/dev/null || echo "")
echo "$CG" | grep -q "S21HC_" && ok "Clipboard (write+read)" || fail "Clipboard (got='${CG}')"
# ============================================================
# 8. 커뮤니케이션
# ============================================================
sect "8. 커뮤니케이션"
SMS=$(termux-sms-list -l 1 -t inbox 2>/dev/null || echo "")
[ -n "$SMS" ] && [ "$SMS" != "[]" ] && ok "SMS inbox" || warn "SMS inbox"
CONT=$(termux-contact-list 2>/dev/null || echo "")
CC=$(echo "$CONT" | grep -c '"name"' || echo 0)
[ "$CC" -gt 0 ] 2>/dev/null && ok "Contacts (${CC} entries)" || warn "Contacts"
CALL=$(termux-call-log -l 5 2>/dev/null || echo "")
CLC=$(echo "$CALL" | grep -c '"number"' || echo 0)
[ "$CLC" -gt 0 ] 2>/dev/null && ok "Call log (${CLC} entries)" || warn "Call log"
TEL=$(termux-telephony-deviceinfo 2>/dev/null || echo "")
[ -n "$TEL" ] && [ "$TEL" != "{}" ] && ok "Telephony device info" || warn "Telephony device info"
CELL=$(termux-telephony-cellinfo 2>/dev/null || echo "")
[ -n "$CELL" ] && [ "$CELL" != "[]" ] && ok "Cellular info" || warn "Cellular info"
NT=$(termux-notification --title "Health Check" \
--content "S21 Phone HC ${NOW}" --id "s21hc_${NOW}" --alert-once 2>/dev/null && echo "ok" || echo "fail")
[ "$NT" = "ok" ] && ok "Notification" || warn "Notification (permission?)"
# ============================================================
# 9. 네트워크 서비스
# ============================================================
sect "9. 네트워크 서비스"
for URL_LABEL in \
"helena_phone|https://helena751107.github.io/helena_phone/" \
"helana_log|https://github.com/helena751107/helana_log" \
"helana-faith|https://github.com/helena751107/helana-faith" \
"helena-piano|https://github.com/helena751107/helena-piano" \
"helena-metalcare|https://github.com/helena751107/helena-metalcare"; do
L="${URL_LABEL%%|*}"; U="${URL_LABEL##*|}"
H=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$U" 2>/dev/null || echo "000")
ok "$L (HTTP ${H})"
done
DC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
"https://discord.com/api/guilds/REDACTED/widget.json" 2>/dev/null || echo "000")
[ "$DC" = "200" ] && ok "Discord widget (HTTP ${DC})" || warn "Discord widget (HTTP ${DC})"
source "${BASE_DIR}/.secrets.env" 2>/dev/null || true
TG_API=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
"https://api.telegram.org/bot${TG_TOKEN:-x}/getMe" 2>/dev/null || echo "000")
[ "$TG_API" = "200" ] && ok "Telegram API (HTTP ${TG_API})" || warn "Telegram API (HTTP ${TG_API})"
# ============================================================
# 10. 직접 Termux 핵심 기능 검증 (MCP 의존 없이)
# ============================================================
sect "10. 핵심 기능 직접 검증"
# get_battery 직접
BAT2=$(termux-battery-status 2>/dev/null || echo "")
if [ -n "$BAT2" ] && echo "$BAT2" | grep -q "percentage"; then
P2=$(jget "$BAT2" "percentage")
T2=$(jget "$BAT2" "temperature")
ok "termux-battery-status → ${P2}% / ${T2}°C"
else
fail "termux-battery-status (직접 호출 실패)"
fi
# device_info 직접
DI=$(termux-telephony-deviceinfo 2>/dev/null || echo "")
[ -n "$DI" ] && [ "$DI" != "{}" ] && ok "termux-telephony-deviceinfo" || fail "termux-telephony-deviceinfo"
# camera — 단순 info 확인
CI2=$(termux-camera-info 2>/dev/null || echo "")
[ -n "$CI2" ] && echo "$CI2" | grep -q "facing" && ok "termux-camera-info" || fail "termux-camera-info"
# ============================================================
# 종합 등급
# ============================================================
TOTAL=$((PASS_CNT + WARN_CNT + FAIL_CNT + SKIP_CNT))
sect "종합 진단"
info "통과: ${PASS_CNT} | 경고: ${WARN_CNT} | 실패: ${FAIL_CNT} | 생략: ${SKIP_CNT}"
if [ "$FAIL_CNT" -eq 0 ] && [ "$WARN_CNT" -le 2 ]; then G="S"; GM="모든 항목 정상"
elif [ "$FAIL_CNT" -eq 0 ]; then G="A"; GM="경고 있으나 핵심 정상"
elif [ "$FAIL_CNT" -le 3 ]; then G="B"; GM="소수 기능 불량 — 점검 권장"
else G="C"; GM="다수 기능 불량 — 즉시 점검"
fi
info "종합 등급: ${G} — ${GM}"
# ============================================================
# JSON 리포트 저장
# ============================================================
cat > "$REPORT_FILE" <<EOF
{
"timestamp": "$(echo "$TS" | sed 's/"/\\"/g')",
"host": "$(echo "$(uname -n)" | sed 's/"/\\"/g')",
"kernel": "$(echo "$(uname -r)" | sed 's/"/\\"/g')",
"grade": "${G}",
"battery": {
"percentage": ${BAT_PCT:-null},
"temperature": ${BAT_TMP:-null},
"status": "$(echo "${BAT_STS}" | sed 's/"/\\"/g')",
"health": "$(echo "${BAT_HTH}" | sed 's/"/\\"/g')",
"plugged": "$(echo "${BAT_PLG}" | sed 's/"/\\"/g')"
},
"wifi": {
"ssid": "$(echo "${WIFI_SSID}" | sed 's/"/\\"/g')",
"ip": "$(echo "${WIFI_IP}" | sed 's/"/\\"/g')",
"rssi": ${WIFI_RSSI:-null},
"link_speed": ${WIFI_LINK:-null}
},
"networks": {
"google": ${EXT_G:-null},
"github": ${EXT_H:-null},
"gh_pages": ${GH_HTTP:-null}
},
"results": {
"pass": ${PASS_CNT},
"warn": ${WARN_CNT},
"fail": ${FAIL_CNT},
"skip": ${SKIP_CNT},
"total": ${TOTAL}
},
"memory": {
"total_mb": ${MEM_TOTAL_MB},
"available_mb": ${MEM_AVAIL_MB},
"free_mb": $((MEM_FREE / 1024)),
"swap_used_mb": ${SWAP_USED_MB},
"swap_total_mb": ${SWAP_TOTAL_MB},
"swap_pct": ${SWAP_PCT}
},
"processes": {
"claude_sessions": ${CLAUDE_COUNT},
"orphan_scanners": ${ORPHAN_COUNT}
},
"mcp": {
"port": ${MCP_PORT},
"http_status": ${MCP_HTTP:-null},
"tools": 18
}
}
EOF
echo ""
info "저장: ${REPORT_FILE}"
# 최근 이력
echo ""
info "최근 검진 이력:"
ls -t "${HEALTH_DIR}"/*.json 2>/dev/null | head -5 | while read -r f; do
[ ! -f "$f" ] && continue
fb=$(basename "$f" .json)
fg=$(grep '"grade"' "$f" 2>/dev/null | sed 's/.*"grade": *"\([^"]*\)".*/\1/' || echo "?")
fp=$(grep '"percentage"' "$f" 2>/dev/null | sed 's/.*"percentage": *\([0-9]*\).*/\1/' || echo "?")
echo " ${fb} | ${fg} | 배터리 ${fp}%"
done
echo ""
echo "==========================================="
echo " ✅ 검진 완료 (${TS})"
echo "==========================================="
# Telegram 보고
if [ "${1:-}" = "--telegram" ]; then
source "${BASE_DIR}/.secrets.env" 2>/dev/null || true
bash "${BASE_DIR}/tg.sh" "📋 S21 Phone 건강 검진 — ${TS}
📊 등급: ${G} — ${GM}
🔋 배터리: ${BAT_PCT}% / ${BAT_TMP}°C / ${BAT_STS}
📶 WiFi: ${WIFI_SSID:-?} (RSSI ${WIFI_RSSI:-?})
📡 MCP: ✅ port ${MCP_PORT} | 도구 ${TCNT}개
✅ ${PASS_CNT} / ⚠️ ${WARN_CNT} / ❌ ${FAIL_CNT} / ⏭️ ${SKIP_CNT}
상세: _notebook/health/${NOW}.json" 2>/dev/null || true
fi
원본 파일: phone-health.sh · Webzine code viewer (no execution)