개발 블로그
회고 4 분 소요

텔레그램 봇 개발 일지 #6 — 로컬 dashboard와 /restart

telegram claude-code observability

결론부터

봇은 백그라운드 process라 평소엔 보이지 않는다. 옆에 작은 HTTP 서버(DASHBOARD_PORT=8765, 기본 127.0.0.1)를 띄워서 현재 작업, 최근 도구 호출, 로그 tail을 브라우저로 실시간 볼 수 있게 했다. 봇 자체 상태가 망가졌을 때를 위한 /restart 슬래시 명령도 같이.

왜 dashboard가 필요한가

5편의 라이브 메시지는 사용자에게 응답 흐름을 보여준다. 하지만 그건 한 번에 한 turn만이다. 다음 같은 의문은 라이브 메시지로 답이 안 된다.

  • 지금 어떤 토픽이 작업 중인가?
  • 봇 process가 살아있는가, polling이 도는가?
  • 최근 5분간 무슨 일이 있었나?
  • 봇 시작 시점의 환경변수와 MCP 서버 목록은?

텔레그램 채팅으로 이걸 일일이 묻는 건 노이즈다. 브라우저에서 한 페이지로 다 보는 게 자연스럽다.

구조

작은 HTTP 서버(stdlib http.server 한 줄짜리 + asyncio task)로 띄운다. 별도 패키지 의존성 없음.

  • GET / — HTML 단일 페이지. 현재 작업, 토픽별 cwd, 최근 도구 호출 라인, MCP 서버 헬스, 환경변수 요약.
  • GET /events — SSE(Server-Sent Events). 봇이 새 메시지를 받거나 도구를 호출할 때마다 푸시.
  • GET /log — 로그 파일 tail. ?lines=200으로 라인 수 조정.
async def emit(event: str, data: dict):
    payload = f"event: {event}\ndata: {json.dumps(data)}\n\n"
    for queue in subscribers:
        await queue.put(payload)

봇 메인 루프에서 메시지·도구 호출·세션 시작 같은 이벤트를 emit()으로 흘려보내면 브라우저가 즉시 업데이트된다.

/restart — 자가 재시작

봇이 자기 자신을 재시작해야 할 때(.env 변경, 코드 hotpath 수정, MCP 서버 추가)가 있다. 외부 터미널을 거치지 않고 텔레그램에서 /restart 한 줄로 끝내는 게 모바일에서 절실했다.

@whitelist_only
async def cmd_restart(update, context):
    await update.message.reply_text("재시작합니다…")
    # subprocess로 start.bat을 띄우고 현재 process는 종료
    subprocess.Popen(["start.bat"], cwd=BOT_DIR, creationflags=DETACHED_PROCESS)
    asyncio.get_event_loop().call_later(0.5, lambda: os._exit(0))

자식 process를 detached로 띄우는 게 핵심이다. 부모(현재 봇) 종료 시 자식이 같이 죽지 않게 해야, 새 봇이 정상 부팅된 뒤 옛 봇이 깔끔하게 빠진다.

start.bat 경로

cwd=BOT_DIR을 명시 안 하면 텔레그램이 띄운 process의 cwd가 어디인지에 따라 start.bat을 못 찾는다. 절대 경로로 박는 게 안전.

LAN 노출과 토큰 가드

기본은 127.0.0.1에 묶어둔다 — 로컬 RDP 세션에서만 보면 충분한 경우. 핸드폰이나 LAN 안 다른 기기에서 보고 싶다면 DASHBOARD_HOST=0.0.0.0로 풀고, DASHBOARD_TOKEN을 32자 이상 시크릿으로 설정한다.

토큰이 있으면 모든 요청에 ?token=<value> 쿼리 또는 Authorization: Bearer <value> 헤더가 필요하다. 없으면 401. 토큰 미설정 + LAN 노출 조합은 봇의 모든 작업 로그가 동네 와이파이에 공개되는 것이라 위험.

DASHBOARD_DISABLED 가끔 dashboard 자체를 끄고 싶을 땐(포트 충돌, 보안 점검)

DASHBOARD_PORT=0 또는 DASHBOARD_DISABLED=1로 토글한다. 코드 수정 없이 환경변수 한 줄.

다음 편 예고

7편은 마무리 회고 — 6일에 걸쳐 만든 봇을 실제로 써보면서 느낀 점, 절약된 시간, 다시 만든다면 다르게 했을 결정들을 정리한다.