FastAPI로 AI 봇 모니터링 대시보드 만들기 — JARVIS Dashboard 구축
7개 봇이 돌아가는데 상태를 한눈에 보고 싶었다. FastAPI + Jinja2 + 폴링으로 실시간 대시보드 만든 과정.
왜 만들었나
봇이 7개 돌아갑니다. 각각 Telegram 메시지를 보내줘서 어느 정도 파악은 되지만:
- 지금 어떤 봇이 살아있는지 한눈에 못 봄
- 봇회의 결과를 PC에서 보려면 Telegram 열어야 함
- 인텔리전스 피드(수집된 뉴스 요약)를 브라우저에서 보고 싶음
FastAPI로 간단한 대시보드를 만들었습니다.
구조
jarvis_dashboard.py (FastAPI, 포트 8902)
├── GET / → 메인 대시보드 (HTML)
├── GET /api/status → 봇 상태 JSON
├── GET /api/feed → 인텔리전스 피드
├── GET /api/meeting → 봇회의 결과
└── GET /api/goals → goal_state.json
HTML은 Python에서 직접 반환하거나 templates/ 폴더에서 Jinja2로 렌더링.
핵심 코드
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import json
import os
from pathlib import Path
app = FastAPI()
@app.get("/api/status")
def get_bot_status():
"""각 봇 프로세스 생존 여부 확인"""
import psutil
bots = {
"nim_bot": False,
"qwen36_bot": False,
"helper_bot": False,
"gemini_bot": False,
}
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
cmdline = ' '.join(proc.info['cmdline'] or [])
for bot_name in bots:
if bot_name in cmdline:
bots[bot_name] = True
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return bots
@app.get("/api/meeting")
def get_latest_meeting():
"""최신 봇회의 기록 반환"""
debates_dir = Path("D:/knowledge/debates")
files = sorted(debates_dir.glob("*.md"), reverse=True)
if not files:
return {"content": "회의 기록 없음"}
latest = files[0]
return {
"date": latest.stem,
"content": latest.read_text(encoding="utf-8")
}
@app.get("/api/goals")
def get_goals():
goal_file = Path("D:/bots/goal_state.json")
if goal_file.exists():
return json.loads(goal_file.read_text(encoding="utf-8"))
return {}
프론트엔드
Jinja2 템플릿 대신 FastAPI에서 HTML 문자열 직접 반환하는 방식으로 시작했습니다. 간단해서 빠르게 만들 수 있었습니다.
@app.get("/", response_class=HTMLResponse)
def dashboard():
return """
<!DOCTYPE html>
<html>
<head>
<title>JARVIS Dashboard</title>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; background: #0f172a; color: #e2e8f0; }
.status-ok { color: #4ade80; }
.status-fail { color: #f87171; }
</style>
</head>
<body>
<h1>JARVIS</h1>
<div id="status">로딩 중...</div>
<script>
async function refresh() {
const res = await fetch('/api/status');
const data = await res.json();
let html = '<table>';
for (const [bot, alive] of Object.entries(data)) {
const cls = alive ? 'status-ok' : 'status-fail';
const icon = alive ? '✅' : '❌';
html += `<tr><td>${bot}</td><td class="${cls}">${icon}</td></tr>`;
}
html += '</table>';
document.getElementById('status').innerHTML = html;
}
refresh();
setInterval(refresh, 10000); // 10초마다 폴링
</script>
</body>
</html>
"""
10초마다 /api/status를 폴링해서 봇 상태를 갱신합니다.
탭 구조 (나중에 추가)
처음엔 상태만 봤습니다. 나중에 탭으로 나눴습니다:
- tab1: 봇 상태 (프로세스 생존 여부)
- tab2: 인텔리전스 피드 (수집된 뉴스)
- tab3: goal_state.json (수익/포스트 현황)
- tab4: Dream Reports (야간 합성 결과)
- tab5: 봇회의 기록 (bot_meeting 결과)
Task Scheduler로 자동 시작
$trigger = New-ScheduledTaskTrigger -AtStartup
$action = New-ScheduledTaskAction `
-Execute "python" `
-Argument "D:\bots\jarvis_dashboard.py" `
-WorkingDirectory "D:\bots"
Register-ScheduledTask `
-TaskName "JARVIS\Dashboard" `
-Trigger $trigger `
-Action $action `
-RunLevel Highest
겪었던 문제
psutil로 프로세스 이름 매칭 문제
Windows에서 Python 프로세스는 전부 python.exe로 나옵니다. 스크립트 이름은 cmdline에서 찾아야 합니다.
cmdline = ' '.join(proc.info['cmdline'] or [])
if 'nim_bot.py' in cmdline:
...
Cloudflare Tunnel 연동
외부에서 접속하려면 Cloudflare Tunnel을 씁니다. Tunnel이 먼저 시작되고 Dashboard가 나중에 올라오면 502 오류가 납니다.
Task Scheduler에서 Dashboard 시작 지연(30초)을 줬습니다.
현재
localhost:8902에서 접속, Cloudflare Tunnel로 외부 접속도 됩니다.
봇 상태를 한눈에 확인하고, 봇회의 결과를 브라우저에서 읽습니다.