Clean up: remove backups, cache, logs; add .gitignore
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Backup files
|
||||||
|
*.backup.*
|
||||||
|
*.v?.*
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Local data (auto-generated)
|
||||||
|
health.json
|
||||||
Binary file not shown.
-394
@@ -1,394 +0,0 @@
|
|||||||
{
|
|
||||||
"subdomains": [
|
|
||||||
{
|
|
||||||
"name": "wiki.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "MiniCITY 지식정원",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 21.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "openclaw.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "OpenClaw 대시보드",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 18.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "agent.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "인프라 현황판",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 13.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "SearXNG 메타서치",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 824.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "erp.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "공사관리 ERP",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 20.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dify.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "Dify 워크플로우",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 307",
|
|
||||||
"latency_ms": 17.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "photo.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "PhotoVault 자료정리",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 46.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "git.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "Gitea Git 서버",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 18.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "find.pinksky.kr",
|
|
||||||
"group": "pinksky.kr",
|
|
||||||
"desc": "Everything 검색",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 401",
|
|
||||||
"latency_ms": 23.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gwenc.kr",
|
|
||||||
"group": "gwenc.kr",
|
|
||||||
"desc": "회사 메인 사이트",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 28.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "minicity.kr",
|
|
||||||
"group": "minicity.kr",
|
|
||||||
"desc": "집 NAS 웹 UI",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "HTTP 200",
|
|
||||||
"latency_ms": 65.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"domains": {
|
|
||||||
"pinksky.kr": {
|
|
||||||
"domain": {
|
|
||||||
"status": "online",
|
|
||||||
"detail": "100.70.47.91"
|
|
||||||
},
|
|
||||||
"pcs": {
|
|
||||||
"macmini": {
|
|
||||||
"name": "맥미니 M4 Pro",
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"spec": "Apple M4 Pro · 64GB · 통합 GPU",
|
|
||||||
"dns": "pinksky.kr",
|
|
||||||
"tailscale_ip": "100.70.47.91",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "",
|
|
||||||
"agents": {
|
|
||||||
"mimi": {
|
|
||||||
"status": "online",
|
|
||||||
"version": "2026.5.2",
|
|
||||||
"uptime": "02-05:04:59",
|
|
||||||
"proc_count": 1,
|
|
||||||
"engine": "openclaw",
|
|
||||||
"detail": "02-05:04:59 · 1프로세스",
|
|
||||||
"name": "미미",
|
|
||||||
"emoji": "🦞"
|
|
||||||
},
|
|
||||||
"ruki": {
|
|
||||||
"status": "online",
|
|
||||||
"version": "v0.11.0",
|
|
||||||
"uptime": "01-17:52:56",
|
|
||||||
"proc_count": 2,
|
|
||||||
"engine": "hermes",
|
|
||||||
"detail": "01-17:52:56 · 2프로세스",
|
|
||||||
"name": "루키",
|
|
||||||
"emoji": "🌱"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mainpc": {
|
|
||||||
"name": "메인컴 (PS-i14700K)",
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"spec": "Intel Core i7-14700K · 96GB · RTX 3090 24GB",
|
|
||||||
"dns": "mainpc-wsl.pinksky.kr",
|
|
||||||
"tailscale_ip": "100.105.122.120",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "alive",
|
|
||||||
"agents": {
|
|
||||||
"pink": {
|
|
||||||
"status": "online",
|
|
||||||
"version": "v0.11.0",
|
|
||||||
"uptime": "?",
|
|
||||||
"proc_count": 2,
|
|
||||||
"engine": "hermes",
|
|
||||||
"detail": "2프로세스",
|
|
||||||
"name": "분홍",
|
|
||||||
"emoji": "🤖"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"subpc": {
|
|
||||||
"name": "보조컴 (MH-3900x)",
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"spec": "AMD Ryzen 9 3900X · 32GB · RTX 3080 12GB",
|
|
||||||
"dns": "subpc-wsl.pinksky.kr",
|
|
||||||
"tailscale_ip": "100.124.61.85",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "alive",
|
|
||||||
"agents": {
|
|
||||||
"sky": {
|
|
||||||
"status": "online",
|
|
||||||
"version": "v0.11.0",
|
|
||||||
"uptime": "?",
|
|
||||||
"proc_count": 1,
|
|
||||||
"engine": "hermes",
|
|
||||||
"detail": "1프로세스",
|
|
||||||
"name": "하늘",
|
|
||||||
"emoji": "🤖"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minicity.kr": {
|
|
||||||
"domain": {
|
|
||||||
"status": "online",
|
|
||||||
"detail": "집 NAS/공유기"
|
|
||||||
},
|
|
||||||
"pcs": {
|
|
||||||
"nas_home": {
|
|
||||||
"name": "집 NAS (mh-nas)",
|
|
||||||
"emoji": "🗄️",
|
|
||||||
"spec": "Synology DS418+",
|
|
||||||
"dns": "minicity.kr",
|
|
||||||
"tailscale_ip": "100.69.107.65",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "alive",
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"router_home": {
|
|
||||||
"name": "집 공유기",
|
|
||||||
"emoji": "📡",
|
|
||||||
"spec": "아이피타임 AX8004BCM",
|
|
||||||
"dns": "pinksky.iptime.org",
|
|
||||||
"tailscale_ip": "",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "DDNS · 원격관리 9090",
|
|
||||||
"agents": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gwenc.kr": {
|
|
||||||
"domain": {
|
|
||||||
"status": "online",
|
|
||||||
"detail": "tcp open"
|
|
||||||
},
|
|
||||||
"pcs": {
|
|
||||||
"officepc": {
|
|
||||||
"name": "서버컴 (gw-ps-5600)",
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"spec": "Intel Core i7-14700K · 96GB · RTX 3090",
|
|
||||||
"dns": "gwenc.kr",
|
|
||||||
"tailscale_ip": "",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "tcp open",
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"nas_office": {
|
|
||||||
"name": "회사 NAS (gwenc-nas2)",
|
|
||||||
"emoji": "🗄️",
|
|
||||||
"spec": "Synology",
|
|
||||||
"dns": "",
|
|
||||||
"tailscale_ip": "100.105.95.19",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "",
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"nas_backup": {
|
|
||||||
"name": "백업 NAS (ps-bk-nas)",
|
|
||||||
"emoji": "🗄️",
|
|
||||||
"spec": "14TB HDD",
|
|
||||||
"dns": "",
|
|
||||||
"tailscale_ip": "100.83.176.55",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "",
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"router_office": {
|
|
||||||
"name": "회사 공유기",
|
|
||||||
"emoji": "📡",
|
|
||||||
"spec": "TP-LINK AX18000",
|
|
||||||
"dns": "gwenc.kr",
|
|
||||||
"tailscale_ip": "",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "원격관리 9000/9443",
|
|
||||||
"agents": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nas_storage": {
|
|
||||||
"nas_home": {
|
|
||||||
"name": "집 NAS (mh-nas)",
|
|
||||||
"model": "Synology DS418+",
|
|
||||||
"os": "DSM 7.2",
|
|
||||||
"status": "online",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"mount": "/volume1",
|
|
||||||
"label": "데이터",
|
|
||||||
"total": "1.8T",
|
|
||||||
"used": "833G",
|
|
||||||
"free": "952G",
|
|
||||||
"pct": "47%"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mount": "/volume2",
|
|
||||||
"label": "백업",
|
|
||||||
"total": "7.0T",
|
|
||||||
"used": "6.0T",
|
|
||||||
"free": "1020G",
|
|
||||||
"pct": "86%"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"detail": "alive"
|
|
||||||
},
|
|
||||||
"nas_office": {
|
|
||||||
"name": "회사 NAS (gwenc-nas2)",
|
|
||||||
"model": "Synology DS923+",
|
|
||||||
"os": "DSM 7.2",
|
|
||||||
"status": "online",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"mount": "/volume1",
|
|
||||||
"label": "메인",
|
|
||||||
"total": "21G",
|
|
||||||
"used": "2.2G",
|
|
||||||
"free": "19G",
|
|
||||||
"pct": "11%"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mount": "/volume3",
|
|
||||||
"label": "SSD캐시",
|
|
||||||
"total": "493G",
|
|
||||||
"used": "211G",
|
|
||||||
"free": "283G",
|
|
||||||
"pct": "43%"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mount": "/volume2",
|
|
||||||
"label": "대용량스토리지",
|
|
||||||
"total": "7.3T",
|
|
||||||
"used": "250G",
|
|
||||||
"free": "7.0T",
|
|
||||||
"pct": "4%"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"detail": "alive"
|
|
||||||
},
|
|
||||||
"nas_backup": {
|
|
||||||
"name": "백업 NAS (ps-bk-nas)",
|
|
||||||
"model": "Synology",
|
|
||||||
"os": "DSM 7.x",
|
|
||||||
"status": "online",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"mount": "/volume1",
|
|
||||||
"label": "메인백업",
|
|
||||||
"total": "52G",
|
|
||||||
"used": "1.4G",
|
|
||||||
"free": "51G",
|
|
||||||
"pct": "3%"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mount": "/volume2",
|
|
||||||
"label": "대용량백업",
|
|
||||||
"total": "15T",
|
|
||||||
"used": "5.3T",
|
|
||||||
"free": "9.2T",
|
|
||||||
"pct": "37%"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"detail": "alive"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"proxmox": {
|
|
||||||
"host": {
|
|
||||||
"name": "서버컴 (gw-ps-5600)",
|
|
||||||
"model": "ASRock Z790M-ITX WiFi",
|
|
||||||
"cpu": "Intel Core i7-14700K",
|
|
||||||
"ram": "96GB",
|
|
||||||
"os": "Windows 11 Pro + Hyper-V",
|
|
||||||
"role": "Hyper-V 호스트",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "tcp open"
|
|
||||||
},
|
|
||||||
"proxmox": {
|
|
||||||
"name": "Proxmox VE",
|
|
||||||
"version": "8.x",
|
|
||||||
"os": "Debian 12 + Proxmox",
|
|
||||||
"cpu": "8 vCPU",
|
|
||||||
"ram": "32GB",
|
|
||||||
"storage": "1TB SSD",
|
|
||||||
"role": "Hyper-V 가상머신 · VM 호스팅",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "tcp open"
|
|
||||||
},
|
|
||||||
"vms": [
|
|
||||||
{
|
|
||||||
"name": "정환용 업무컴",
|
|
||||||
"host": "gwenc.kr",
|
|
||||||
"port": 13389,
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "tcp open"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "주영용 업무컴",
|
|
||||||
"host": "gwenc.kr",
|
|
||||||
"port": 23389,
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"status": "online",
|
|
||||||
"detail": "tcp open"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "임시용 업무컴",
|
|
||||||
"host": "gwenc.kr",
|
|
||||||
"port": 43348,
|
|
||||||
"emoji": "🖥️",
|
|
||||||
"status": "offline",
|
|
||||||
"detail": "timeout"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"summary": {
|
|
||||||
"total": 27,
|
|
||||||
"online": 26,
|
|
||||||
"offline": 1,
|
|
||||||
"domain_total": 3,
|
|
||||||
"domain_online": 3
|
|
||||||
},
|
|
||||||
"timestamp": "2026-05-05T21:13:48.223557+09:00",
|
|
||||||
"timestamp_epoch": 1777983228
|
|
||||||
}
|
|
||||||
-1781
File diff suppressed because it is too large
Load Diff
@@ -1,338 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Agent Status Health Checker v6 — 메모리/VRAM 실시간 수집 + 사양/박스 구조"""
|
|
||||||
import json, subprocess, time, re
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timezone, timedelta
|
|
||||||
|
|
||||||
KST = timezone(timedelta(hours=9))
|
|
||||||
STATUS_FILE = Path(__file__).parent / "health.json"
|
|
||||||
|
|
||||||
# ===== 정적 사양 정보 =====
|
|
||||||
PC_SPECS = {
|
|
||||||
"macmini": "M4 Pro · 64GB · 통합 GPU",
|
|
||||||
"mainpc": "Intel Core i7-14700K · 96GB · RTX 3090 24GB",
|
|
||||||
"subpc": "AMD Ryzen 9 3900X · 32GB · RTX 3080 12GB",
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== 기본 체크 함수 =====
|
|
||||||
def http_check(url, timeout=5):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}:%{time_total}",
|
|
||||||
url, "--max-time", str(timeout)],
|
|
||||||
capture_output=True, text=True, timeout=timeout+2)
|
|
||||||
parts = r.stdout.strip().split(":")
|
|
||||||
code = parts[0] if parts else "000"
|
|
||||||
return {"status": "online" if code.startswith(("2","3")) else "error",
|
|
||||||
"detail": f"HTTP {code}"}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def ssh_check(host, timeout=8):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
host, "echo alive"],
|
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
alive = r.returncode == 0 and "alive" in r.stdout
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": "alive" if alive else r.stderr.strip()[:80]}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def tailscale_ping(host, timeout=8):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["tailscale", "ping", "--until", "3s", "--c", "1", host],
|
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
alive = "pong" in r.stdout or "is local" in r.stdout
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": "" if alive else "no pong"}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def proc_check(process, via=None):
|
|
||||||
try:
|
|
||||||
if via:
|
|
||||||
cmd = ["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
via, f"pgrep -f '{process}' || echo NOT_FOUND"]
|
|
||||||
else:
|
|
||||||
cmd = ["pgrep", "-f", process]
|
|
||||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=8)
|
|
||||||
out = r.stdout.strip()
|
|
||||||
alive = out not in ["NOT_FOUND", ""] and r.returncode == 0
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": f"PID {out[:20]}" if alive else "not running"}
|
|
||||||
except: return {"status": "offline", "detail": "error"}
|
|
||||||
|
|
||||||
# ===== 상태 수집 =====
|
|
||||||
def get_local_tailscale_ip():
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["tailscale", "ip", "-4"], capture_output=True, text=True, timeout=5)
|
|
||||||
return r.stdout.strip()
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_remote_tailscale_ip(via):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
via, "tailscale ip -4"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
return r.stdout.strip() if r.returncode == 0 else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_version(label):
|
|
||||||
try:
|
|
||||||
if label == "openclaw":
|
|
||||||
r = subprocess.run(["openclaw", "--version"], capture_output=True, text=True, timeout=5)
|
|
||||||
v = r.stdout.strip().replace("OpenClaw ", "")
|
|
||||||
v = re.sub(r'\s+\([^)]+\)', '', v).strip()
|
|
||||||
return v if v else "?"
|
|
||||||
elif label == "hermes":
|
|
||||||
r = subprocess.run([
|
|
||||||
"/Users/pinksky/.hermes/hermes-agent/venv/bin/python",
|
|
||||||
"-m", "hermes_cli.main", "--version"
|
|
||||||
], capture_output=True, text=True, timeout=5)
|
|
||||||
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
||||||
return m.group(0) if m else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_uptime(pid_str):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["ps", "-p", pid_str, "-o", "etime="], capture_output=True, text=True, timeout=5)
|
|
||||||
return r.stdout.strip()
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_local_macos_memory():
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=5)
|
|
||||||
total_gb = round(int(r.stdout.strip()) / (1024**3), 1)
|
|
||||||
r = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=5)
|
|
||||||
lines = r.stdout.strip().split("\n")
|
|
||||||
page_size = 16384
|
|
||||||
total_pages = 0
|
|
||||||
for line in lines:
|
|
||||||
m = re.search(r'Pages (\w+):\s+([0-9,]+)', line)
|
|
||||||
if m:
|
|
||||||
key, val = m.group(1).lower(), int(m.group(2).replace(",",""))
|
|
||||||
if key in ("active","inactive","wired","compressed"):
|
|
||||||
total_pages += val
|
|
||||||
used_gb = round(total_pages * page_size / (1024**3), 1)
|
|
||||||
return f"{used_gb}GB/{total_gb}GB"
|
|
||||||
except: return "?.?"
|
|
||||||
|
|
||||||
def get_remote_memory(via):
|
|
||||||
result = {"sys": "?", "gpu": None}
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
via, "free -m | awk '/Mem:/ {printf \"%.1f/%.1f\", ($2-$7)/1024, \$2/1024}'"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
out = r.stdout.strip()
|
|
||||||
if out and r.returncode == 0:
|
|
||||||
result["sys"] = f"{out}GB"
|
|
||||||
except: pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
via, "nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader,nounits"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
out = r.stdout.strip()
|
|
||||||
if out and r.returncode == 0:
|
|
||||||
parts = [x.strip() for x in out.split(",")]
|
|
||||||
if len(parts) >= 2:
|
|
||||||
u, t = float(parts[0]), float(parts[1])
|
|
||||||
result["gpu"] = f"VRAM {round(u/1024,1)}GB/{round(t/1024,1)}GB"
|
|
||||||
except: pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
def proc_details(name_pattern, version_label=None, engine_name="?", via=None):
|
|
||||||
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
||||||
"engine": engine_name, "detail": "not running", "mem": None, "gpu": None}
|
|
||||||
try:
|
|
||||||
if via:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
via, f"ps aux | grep '{name_pattern}' | grep -v grep | grep -v health_check || true"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
lines = [l for l in r.stdout.strip().split("\n") if l.strip()]
|
|
||||||
if lines:
|
|
||||||
result["status"] = "online"
|
|
||||||
result["proc_count"] = len(lines)
|
|
||||||
if version_label:
|
|
||||||
result["version"] = "?" # 원격 버전 체크는 복잡하므로 여기서는 생략
|
|
||||||
mem_data = get_remote_memory(via)
|
|
||||||
result["mem"] = mem_data["sys"]
|
|
||||||
result["gpu"] = mem_data["gpu"]
|
|
||||||
result["detail"] = f"{result['proc_count']}프로세스"
|
|
||||||
else:
|
|
||||||
r = subprocess.run(["ps", "aux"], capture_output=True, text=True, timeout=5)
|
|
||||||
lines = [l for l in r.stdout.strip().split("\n") if name_pattern in l and "grep" not in l and "health_check" not in l]
|
|
||||||
if lines:
|
|
||||||
result["status"] = "online"
|
|
||||||
result["proc_count"] = len(lines)
|
|
||||||
first = lines[0].strip().split(None, 2)
|
|
||||||
if len(first) >= 2:
|
|
||||||
result["uptime"] = get_uptime(first[1])
|
|
||||||
if version_label:
|
|
||||||
result["version"] = get_version(version_label)
|
|
||||||
result["mem"] = get_local_macos_memory()
|
|
||||||
result["detail"] = f"{result['uptime']} · {result['proc_count']}프로세스"
|
|
||||||
except: pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
# ===== 메인 =====
|
|
||||||
def run_checks():
|
|
||||||
ts = datetime.now(KST)
|
|
||||||
ts_ip = get_local_tailscale_ip()
|
|
||||||
|
|
||||||
# ===== pinksky.kr (아지트) =====
|
|
||||||
pinksky_domain = http_check("https://pinksky.kr/")
|
|
||||||
macmini_detail = f"{ts_ip} · pinksky.kr"
|
|
||||||
mainpc_ts = get_remote_tailscale_ip("mainpc-wsl")
|
|
||||||
|
|
||||||
mimi = proc_details("openclaw-gateway", "openclaw", "openclaw")
|
|
||||||
ruki = proc_details("hermes", "hermes", "hermes")
|
|
||||||
mainpc = ssh_check("mainpc-win", 6)
|
|
||||||
pink_hermes = proc_details("hermes", engine_name="hermes", via="mainpc-wsl")
|
|
||||||
|
|
||||||
pinksky = {
|
|
||||||
"domain": pinksky_domain,
|
|
||||||
"pcs": {
|
|
||||||
"macmini": {
|
|
||||||
"name": "맥미니 M4 Pro", "emoji": "🖥️",
|
|
||||||
"tailscale_ip": ts_ip, "dns": "pinksky.kr",
|
|
||||||
"status": "online", "detail": macmini_detail,
|
|
||||||
"spec": PC_SPECS["macmini"],
|
|
||||||
"agents": {
|
|
||||||
"mimi": {
|
|
||||||
"name": "미미", "emoji": "🦞",
|
|
||||||
**{k:v for k,v in mimi.items() if k not in ("status","detail")},
|
|
||||||
"status": mimi["status"], "detail": mimi["detail"]
|
|
||||||
},
|
|
||||||
"ruki": {
|
|
||||||
"name": "루키", "emoji": "🌱",
|
|
||||||
**{k:v for k,v in ruki.items() if k not in ("status","detail")},
|
|
||||||
"status": ruki["status"], "detail": ruki["detail"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mainpc": {
|
|
||||||
"name": "메인컴", "emoji": "🖥️",
|
|
||||||
"tailscale_ip": mainpc_ts, "dns": "mainpc-wsl.pinksky.kr",
|
|
||||||
**mainpc,
|
|
||||||
"spec": PC_SPECS["mainpc"],
|
|
||||||
"agents": {
|
|
||||||
"pink_hermes": {
|
|
||||||
"name": "분홍", "emoji": "🤖",
|
|
||||||
**{k:v for k,v in pink_hermes.items() if k not in ("status","detail")},
|
|
||||||
"status": pink_hermes["status"], "detail": pink_hermes["detail"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== minicity.kr (집 NAS) =====
|
|
||||||
minicity_domain = http_check("https://minicity.kr/")
|
|
||||||
nas_pc = ssh_check("mh-nas", 6)
|
|
||||||
|
|
||||||
minicity = {
|
|
||||||
"domain": minicity_domain,
|
|
||||||
"pcs": {
|
|
||||||
"nas": {
|
|
||||||
"name": "NAS (minicity)", "emoji": "🗄️",
|
|
||||||
**nas_pc, "agents": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== pinksky.iptime.org (집 DDNS / 보조컴) =====
|
|
||||||
subpc_ts = get_remote_tailscale_ip("subpc-wsl")
|
|
||||||
subpc = ssh_check("subpc-wsl", 6)
|
|
||||||
sky_hermes = proc_details("hermes", engine_name="hermes", via="subpc-wsl")
|
|
||||||
|
|
||||||
iptime = {
|
|
||||||
"domain": {"status": "online" if subpc["status"] == "online" else "error", "detail": "DDNS"},
|
|
||||||
"pcs": {
|
|
||||||
"subpc": {
|
|
||||||
"name": "보조컴", "emoji": "🖥️",
|
|
||||||
"tailscale_ip": subpc_ts, "dns": "subpc-wsl.pinksky.kr",
|
|
||||||
**subpc,
|
|
||||||
"spec": PC_SPECS["subpc"],
|
|
||||||
"agents": {
|
|
||||||
"sky_hermes": {
|
|
||||||
"name": "하늘", "emoji": "🤖",
|
|
||||||
**{k:v for k,v in sky_hermes.items() if k not in ("status","detail")},
|
|
||||||
"status": sky_hermes["status"], "detail": sky_hermes["detail"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== gwenc.kr (회사) =====
|
|
||||||
gwenc_domain = http_check("https://gwenc.kr/")
|
|
||||||
server5600 = tailscale_ping("gw-ps-5600", 6)
|
|
||||||
proxmox = tailscale_ping("proxmox-minicity", 6)
|
|
||||||
office_nas = tailscale_ping("gwenc-nas2", 6)
|
|
||||||
backup_nas = tailscale_ping("ps-bk-nas", 6)
|
|
||||||
office_pc = tailscale_ping("ps-i14700k-win", 6)
|
|
||||||
office_pc_wsl = tailscale_ping("ps-i14700k-wsl", 6)
|
|
||||||
|
|
||||||
gwenc = {
|
|
||||||
"domain": gwenc_domain,
|
|
||||||
"pcs": {
|
|
||||||
"server5600": {"name": "서버컴 5600", "emoji": "🖥️", **server5600, "agents": {}},
|
|
||||||
"proxmox": {"name": "Proxmox", "emoji": "🔶", **proxmox, "agents": {}},
|
|
||||||
"office_nas": {"name": "회사 NAS", "emoji": "🗄️", **office_nas, "agents": {}},
|
|
||||||
"backup_nas": {"name": "백업 NAS", "emoji": "🗄️", **backup_nas, "agents": {}},
|
|
||||||
"office_pc": {
|
|
||||||
"name": "회사 메인PC", "emoji": "🖥️", **office_pc,
|
|
||||||
"agents": {
|
|
||||||
"office_pc_wsl": {"name": "회사 메인PC WSL", "emoji": "🖥️", **office_pc_wsl}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== 서브도메인 =====
|
|
||||||
subdomains = [
|
|
||||||
{"name": "wiki.pinksky.kr", "url": "https://wiki.pinksky.kr/", "group": "pinksky.kr", "desc": "MiniCITY 지식정원"},
|
|
||||||
{"name": "openclaw.pinksky.kr", "url": "https://openclaw.pinksky.kr/", "group": "pinksky.kr", "desc": "OpenClaw 대시보드"},
|
|
||||||
{"name": "agent.pinksky.kr", "url": "https://agent.pinksky.kr/", "group": "pinksky.kr", "desc": "에이전트 현황판"},
|
|
||||||
{"name": "search.pinksky.kr", "url": "https://search.pinksky.kr/search?q=test", "group": "pinksky.kr", "desc": "SearXNG 메타서치"},
|
|
||||||
{"name": "erp.pinksky.kr", "url": "https://erp.pinksky.kr/", "group": "pinksky.kr", "desc": "공사관리 ERP"},
|
|
||||||
{"name": "dify.pinksky.kr", "url": "https://dify.pinksky.kr:8443/", "group": "pinksky.kr", "desc": "Dify 워크플로우"},
|
|
||||||
{"name": "gwenc.kr", "url": "https://gwenc.kr/", "group": "gwenc.kr", "desc": "회사 메인 사이트"},
|
|
||||||
{"name": "minicity.kr", "url": "https://minicity.kr/", "group": "minicity.kr", "desc": "집 NAS 웹 UI"},
|
|
||||||
]
|
|
||||||
subdomain_results = [{"name": sd["name"], **http_check(sd["url"], 4), "group": sd["group"], "desc": sd["desc"]}
|
|
||||||
for sd in subdomains]
|
|
||||||
|
|
||||||
# ===== 요약 =====
|
|
||||||
all_domains = {"pinksky.kr": pinksky, "minicity.kr": minicity,
|
|
||||||
"pinksky.iptime.org": iptime, "gwenc.kr": gwenc}
|
|
||||||
all_pcs = []
|
|
||||||
for d in all_domains.values():
|
|
||||||
for pc in d["pcs"].values():
|
|
||||||
all_pcs.append(pc)
|
|
||||||
all_pcs.extend(pc["agents"].values())
|
|
||||||
|
|
||||||
online = sum(1 for p in all_pcs if p["status"] == "online")
|
|
||||||
total = len(all_pcs)
|
|
||||||
domain_online = sum(1 for d in all_domains.values() if d["domain"]["status"] == "online")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"subdomains": subdomain_results,
|
|
||||||
"domains": all_domains,
|
|
||||||
"summary": {
|
|
||||||
"total": total, "online": online, "offline": total - online,
|
|
||||||
"domain_total": len(all_domains), "domain_online": domain_online
|
|
||||||
},
|
|
||||||
"timestamp": ts.isoformat(),
|
|
||||||
"timestamp_epoch": int(ts.timestamp())
|
|
||||||
}
|
|
||||||
|
|
||||||
STATUS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
print(f"[{ts.strftime('%H:%M:%S')}] 온라인 {online}/{total} | 도메인 {domain_online}/{len(all_domains)}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_checks()
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""작은도시 인프라 현황 — PC / NAS / 공유기 / 에이전트 / 도메인 5탭"""
|
|
||||||
import json, subprocess, re
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timezone, timedelta
|
|
||||||
|
|
||||||
KST = timezone(timedelta(hours=9))
|
|
||||||
STATUS_FILE = Path(__file__).parent / "health.json"
|
|
||||||
|
|
||||||
# ===== 기본 체크 함수 =====
|
|
||||||
def http_check(url, timeout=5):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}:%{time_total}",
|
|
||||||
url, "--max-time", str(timeout)],
|
|
||||||
capture_output=True, text=True, timeout=timeout+2)
|
|
||||||
parts = r.stdout.strip().split(":")
|
|
||||||
code = parts[0] if parts else "000"
|
|
||||||
return {"status": "online" if code.startswith(("2","3")) else "error",
|
|
||||||
"detail": f"HTTP {code}",
|
|
||||||
"latency_ms": round(float(parts[1])*1000,0) if len(parts)>1 and parts[1] else None}
|
|
||||||
except: return {"status": "offline", "detail": "timeout", "latency_ms": None}
|
|
||||||
|
|
||||||
def ssh_check(host, timeout=8):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
host, "echo alive"],
|
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
alive = r.returncode == 0 and "alive" in r.stdout
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": "alive" if alive else r.stderr.strip()[:80]}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def tailscale_ping(host, timeout=8):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["tailscale", "ping", "--c", "3", host],
|
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
out = r.stdout + r.stderr
|
|
||||||
alive = "pong" in out or "is local" in out
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": "" if alive else "no pong"}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def proc_check(name_pattern, engine_name="?"):
|
|
||||||
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
||||||
"engine": engine_name, "detail": "not running", "mem": None}
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["pgrep", "-f", name_pattern], capture_output=True, text=True, timeout=5)
|
|
||||||
pids = [p.strip() for p in r.stdout.strip().split("\n") if p.strip()]
|
|
||||||
if pids:
|
|
||||||
result["status"] = "online"
|
|
||||||
result["proc_count"] = len(pids)
|
|
||||||
try:
|
|
||||||
u = subprocess.run(["ps", "-p", pids[0], "-o", "etime="], capture_output=True, text=True, timeout=5)
|
|
||||||
result["uptime"] = u.stdout.strip()
|
|
||||||
except: pass
|
|
||||||
result["detail"] = f"{result['uptime']} · {result['proc_count']}프로세스"
|
|
||||||
except: pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
def remote_proc(via, name_pattern, engine_name="?"):
|
|
||||||
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
||||||
"engine": engine_name, "detail": "not running", "mem": None}
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no", via,
|
|
||||||
f"ps aux | grep '{name_pattern}' | grep -v grep || true"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
lines = [l for l in r.stdout.strip().split("\n") if l.strip()]
|
|
||||||
if lines:
|
|
||||||
result["status"] = "online"
|
|
||||||
result["proc_count"] = len(lines)
|
|
||||||
result["detail"] = f"{result['proc_count']}프로세스"
|
|
||||||
except: pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
# ===== 버전/TS IP =====
|
|
||||||
def get_local_ts_ip():
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["tailscale", "ip", "-4"], capture_output=True, text=True, timeout=5)
|
|
||||||
return r.stdout.strip()
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_remote_ts(via):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", via, "tailscale ip -4"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
return r.stdout.strip() if r.returncode == 0 else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_local_version(label):
|
|
||||||
try:
|
|
||||||
if label == "openclaw":
|
|
||||||
r = subprocess.run(["openclaw", "--version"], capture_output=True, text=True, timeout=5)
|
|
||||||
v = r.stdout.strip().replace("OpenClaw ", "")
|
|
||||||
v = re.sub(r'\s+\([^)]+\)', '', v).strip()
|
|
||||||
return v if v else "?"
|
|
||||||
elif label == "hermes":
|
|
||||||
r = subprocess.run(["/Users/pinksky/.hermes/hermes-agent/venv/bin/python","-m","hermes_cli.main","--version"],
|
|
||||||
capture_output=True, text=True, timeout=5)
|
|
||||||
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
||||||
return m.group(0) if m else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_remote_hermes_version(via):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", via,
|
|
||||||
"export PATH=\"$HOME/.local/bin:$PATH\"; hermes --version"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
||||||
return m.group(0) if m else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
# ===== 메인 =====
|
|
||||||
def run_checks():
|
|
||||||
ts = datetime.now(KST)
|
|
||||||
ts_ip = get_local_ts_ip()
|
|
||||||
|
|
||||||
# --- PC ---
|
|
||||||
mainpc_ssh = ssh_check("mainpc-win", 6)
|
|
||||||
mainpc_ts = get_remote_ts("mainpc-wsl")
|
|
||||||
subpc_ssh = ssh_check("subpc-wsl", 6)
|
|
||||||
subpc_ts = get_remote_ts("subpc-wsl")
|
|
||||||
office_pc = tailscale_ping("office-pc", 6)
|
|
||||||
|
|
||||||
pcs = [
|
|
||||||
{"name": "맥미니 M4 Pro", "emoji": "🖥️", "spec": "Apple M4 Pro · 64GB · 통합 GPU",
|
|
||||||
"net": f"{ts_ip} · pinksky.kr", "status": "online", "detail": ""},
|
|
||||||
{"name": "메인컴 (PS-i14700K)", "emoji": "🖥️", "spec": "Intel Core i7-14700K · 96GB · RTX 3090 24GB",
|
|
||||||
"net": f"{mainpc_ts} · mainpc-wsl.pinksky.kr", **mainpc_ssh},
|
|
||||||
{"name": "보조컴 (MH-3900x)", "emoji": "🖥️", "spec": "AMD Ryzen 9 3900X · 32GB · RTX 3080 12GB",
|
|
||||||
"net": f"{subpc_ts} · subpc-wsl.pinksky.kr", **subpc_ssh},
|
|
||||||
{"name": "회사 메인PC (PS-i14700K)", "emoji": "🖥️", "spec": "Intel Core i7-14700K · 96GB · RTX 3090",
|
|
||||||
"net": "", **office_pc},
|
|
||||||
]
|
|
||||||
|
|
||||||
# --- NAS ---
|
|
||||||
nas_mh = ssh_check("mh-nas", 6)
|
|
||||||
nas_gwenc2 = tailscale_ping("gwenc-nas2", 6)
|
|
||||||
nas_bk = tailscale_ping("ps-bk-nas", 6)
|
|
||||||
|
|
||||||
nas = [
|
|
||||||
{"name": "집 NAS (mh-nas)", "emoji": "🗄️", "spec": "Synology DS418+",
|
|
||||||
"net": "100.69.107.65 · minicity.kr", **nas_mh},
|
|
||||||
{"name": "회사 NAS (gwenc-nas2)", "emoji": "🗄️", "spec": "Synology",
|
|
||||||
"net": "100.105.95.19", **nas_gwenc2},
|
|
||||||
{"name": "백업 NAS (ps-bk-nas)", "emoji": "🗄️", "spec": "14TB HDD",
|
|
||||||
"net": "100.83.176.55", **nas_bk},
|
|
||||||
]
|
|
||||||
|
|
||||||
# --- 공유기 ---
|
|
||||||
routers = [
|
|
||||||
{"name": "집 공유기", "emoji": "📡", "spec": "아이피타임 AX8004BCM",
|
|
||||||
"net": "pinksky.iptime.org", "status": "online", "detail": "DDNS · 원격관리 9090"},
|
|
||||||
{"name": "아지트 공유기", "emoji": "📡", "spec": "TP-LINK Archer BE550",
|
|
||||||
"net": "pinksky.kr", "status": "online", "detail": "헬로비전 · TP-Link 앱"},
|
|
||||||
{"name": "회사 공유기", "emoji": "📡", "spec": "TP-LINK AX18000",
|
|
||||||
"net": "gwenc.kr", "status": "online", "detail": "회사 자산 · 원격관리 9000/9443"},
|
|
||||||
]
|
|
||||||
|
|
||||||
# --- 에이전트 ---
|
|
||||||
mimi = proc_check("openclaw-gateway", "openclaw")
|
|
||||||
mimi["name"] = "미미"
|
|
||||||
mimi["emoji"] = "🦞"
|
|
||||||
mimi["host"] = "맥미니 (openclaw)"
|
|
||||||
mimi["version"] = get_local_version("openclaw")
|
|
||||||
|
|
||||||
ruki = proc_check("hermes", "hermes")
|
|
||||||
ruki["name"] = "루키"
|
|
||||||
ruki["emoji"] = "🌱"
|
|
||||||
ruki["host"] = "맥미니 (hermes)"
|
|
||||||
ruki["version"] = get_local_version("hermes")
|
|
||||||
|
|
||||||
pink = remote_proc("mainpc-wsl", "hermes", "hermes")
|
|
||||||
pink["name"] = "분홍"
|
|
||||||
pink["emoji"] = "🤖"
|
|
||||||
pink["host"] = "메인컴 WSL"
|
|
||||||
pink["version"] = get_remote_hermes_version("mainpc-wsl")
|
|
||||||
|
|
||||||
sky = remote_proc("subpc-wsl", "hermes", "hermes")
|
|
||||||
sky["name"] = "하늘"
|
|
||||||
sky["emoji"] = "🤖"
|
|
||||||
sky["host"] = "보조컴 WSL"
|
|
||||||
sky["version"] = get_remote_hermes_version("subpc-wsl")
|
|
||||||
|
|
||||||
agents = [a for a in [mimi, ruki, pink, sky]]
|
|
||||||
|
|
||||||
# --- 도메인 ---
|
|
||||||
domains = [
|
|
||||||
{"name": "wiki.pinksky.kr", "url": "https://wiki.pinksky.kr/", "group": "pinksky.kr", "desc": "MiniCITY 지식정원"},
|
|
||||||
{"name": "openclaw.pinksky.kr", "url": "https://openclaw.pinksky.kr/", "group": "pinksky.kr", "desc": "OpenClaw 대시보드"},
|
|
||||||
{"name": "agent.pinksky.kr", "url": "https://agent.pinksky.kr/", "group": "pinksky.kr", "desc": "인프라 현황판"},
|
|
||||||
{"name": "search.pinksky.kr", "url": "https://search.pinksky.kr/search?q=test", "group": "pinksky.kr", "desc": "SearXNG 메타서치"},
|
|
||||||
{"name": "erp.pinksky.kr", "url": "https://erp.pinksky.kr/", "group": "pinksky.kr", "desc": "공사관리 ERP"},
|
|
||||||
{"name": "dify.pinksky.kr:8443", "url": "https://dify.pinksky.kr:8443/", "group": "pinksky.kr", "desc": "Dify 워크플로우"},
|
|
||||||
{"name": "gwenc.kr", "url": "https://gwenc.kr/", "group": "gwenc.kr", "desc": "회사 메인 사이트"},
|
|
||||||
{"name": "minicity.kr", "url": "https://minicity.kr/", "group": "minicity.kr", "desc": "집 NAS 웹 UI"},
|
|
||||||
]
|
|
||||||
domain_results = []
|
|
||||||
for d in domains:
|
|
||||||
chk = http_check(d["url"], 4)
|
|
||||||
domain_results.append({
|
|
||||||
"name": d["name"], "group": d["group"], "desc": d["desc"],
|
|
||||||
"status": chk["status"], "detail": chk["detail"],
|
|
||||||
"latency_ms": chk.get("latency_ms")
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- 요약 ---
|
|
||||||
all_items = pcs + nas + routers + agents + domain_results
|
|
||||||
online = sum(1 for x in all_items if x["status"] == "online")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"pcs": pcs,
|
|
||||||
"nas": nas,
|
|
||||||
"routers": routers,
|
|
||||||
"agents": agents,
|
|
||||||
"domains": domain_results,
|
|
||||||
"timestamp": ts.isoformat(),
|
|
||||||
"timestamp_epoch": int(ts.timestamp()),
|
|
||||||
"summary": {"online": online, "total": len(all_items)}
|
|
||||||
}
|
|
||||||
|
|
||||||
STATUS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
print(f"[{ts.strftime('%H:%M:%S')}] 온라인 {online}/{len(all_items)}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_checks()
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""작은도시 인프라 현황 v5 — 도메인 계층 + 에이전트/서브도메인 2탭"""
|
|
||||||
import json, subprocess, re
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timezone, timedelta
|
|
||||||
|
|
||||||
KST = timezone(timedelta(hours=9))
|
|
||||||
STATUS_FILE = Path(__file__).parent / "health.json"
|
|
||||||
|
|
||||||
PC_SPECS = {
|
|
||||||
"macmini": {"name": "맥미니 M4 Pro", "emoji": "🖥️", "spec": "Apple M4 Pro · 64GB · 통합 GPU", "dns": "pinksky.kr"},
|
|
||||||
"mainpc": {"name": "메인컴 (PS-i14700K)", "emoji": "🖥️", "spec": "Intel Core i7-14700K · 96GB · RTX 3090 24GB", "dns": "mainpc-wsl.pinksky.kr"},
|
|
||||||
"subpc": {"name": "보조컴 (MH-3900x)", "emoji": "🖥️", "spec": "AMD Ryzen 9 3900X · 32GB · RTX 3080 12GB", "dns": "subpc-wsl.pinksky.kr"},
|
|
||||||
"officepc": {"name": "회사 메인PC (PS-i14700K)", "emoji": "🖥️", "spec": "Intel Core i7-14700K · 96GB · RTX 3090", "dns": ""},
|
|
||||||
"nas_home": {"name": "집 NAS (mh-nas)", "emoji": "🗄️", "spec": "Synology DS418+", "dns": "minicity.kr"},
|
|
||||||
"nas_office": {"name": "회사 NAS (gwenc-nas2)", "emoji": "🗄️", "spec": "Synology", "dns": ""},
|
|
||||||
"nas_backup": {"name": "백업 NAS (ps-bk-nas)", "emoji": "🗄️", "spec": "14TB HDD", "dns": ""},
|
|
||||||
"router_home": {"name": "집 공유기", "emoji": "📡", "spec": "아이피타임 AX8004BCM", "dns": "pinksky.iptime.org"},
|
|
||||||
"router_apt": {"name": "아지트 공유기", "emoji": "📡", "spec": "TP-LINK Archer BE550", "dns": "pinksky.kr"},
|
|
||||||
"router_office": {"name": "회사 공유기", "emoji": "📡", "spec": "TP-LINK AX18000", "dns": "gwenc.kr"},
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== 기본 체크 함수 =====
|
|
||||||
def http_check(url, timeout=5):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}:%{time_total}",
|
|
||||||
url, "--max-time", str(timeout)],
|
|
||||||
capture_output=True, text=True, timeout=timeout+2)
|
|
||||||
parts = r.stdout.strip().split(":")
|
|
||||||
code = parts[0] if parts else "000"
|
|
||||||
latency = round(float(parts[1])*1000, 0) if len(parts) > 1 and parts[1] else None
|
|
||||||
return {"status": "online" if code.startswith(("2","3")) else "error",
|
|
||||||
"detail": f"HTTP {code}", "latency_ms": latency}
|
|
||||||
except: return {"status": "offline", "detail": "timeout", "latency_ms": None}
|
|
||||||
|
|
||||||
def ssh_check(host, timeout=8):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
host, "echo alive"],
|
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
alive = r.returncode == 0 and "alive" in r.stdout
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": "alive" if alive else r.stderr.strip()[:80]}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def tailscale_ping(host, timeout=8):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["tailscale", "ping", "--c", "3", host],
|
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
out = r.stdout + r.stderr
|
|
||||||
alive = "pong" in out or "is local" in out
|
|
||||||
return {"status": "online" if alive else "offline",
|
|
||||||
"detail": "" if alive else "no pong"}
|
|
||||||
except: return {"status": "offline", "detail": "timeout"}
|
|
||||||
|
|
||||||
def get_local_ts_ip():
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["tailscale", "ip", "-4"], capture_output=True, text=True, timeout=5)
|
|
||||||
return r.stdout.strip()
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_remote_ts(via):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", via, "tailscale ip -4"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
return r.stdout.strip() if r.returncode == 0 else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_version(label):
|
|
||||||
try:
|
|
||||||
if label == "openclaw":
|
|
||||||
r = subprocess.run(["openclaw", "--version"], capture_output=True, text=True, timeout=5)
|
|
||||||
v = r.stdout.strip().replace("OpenClaw ", "")
|
|
||||||
v = re.sub(r'\s+\([^)]+\)', '', v).strip()
|
|
||||||
return v if v else "?"
|
|
||||||
elif label == "hermes":
|
|
||||||
r = subprocess.run([
|
|
||||||
"/Users/pinksky/.hermes/hermes-agent/venv/bin/python",
|
|
||||||
"-m", "hermes_cli.main", "--version"],
|
|
||||||
capture_output=True, text=True, timeout=5)
|
|
||||||
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
||||||
return m.group(0) if m else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def get_remote_hermes_version(via):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", via,
|
|
||||||
"export PATH=\"$HOME/.local/bin:$PATH\"; hermes --version"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
||||||
return m.group(0) if m else "?"
|
|
||||||
except: return "?"
|
|
||||||
|
|
||||||
def proc_detail(pattern, engine_name="?"):
|
|
||||||
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
||||||
"engine": engine_name, "detail": "not running"}
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["pgrep", "-f", pattern], capture_output=True, text=True, timeout=5)
|
|
||||||
pids = [p.strip() for p in r.stdout.strip().split("\n") if p.strip()]
|
|
||||||
if pids:
|
|
||||||
result["status"] = "online"
|
|
||||||
result["proc_count"] = len(pids)
|
|
||||||
try:
|
|
||||||
u = subprocess.run(["ps", "-p", pids[0], "-o", "etime="],
|
|
||||||
capture_output=True, text=True, timeout=5)
|
|
||||||
result["uptime"] = u.stdout.strip()
|
|
||||||
except: pass
|
|
||||||
result["detail"] = f"{result['uptime']} · {result['proc_count']}프로세스"
|
|
||||||
except: pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
def remote_proc(via, pattern, engine_name="?"):
|
|
||||||
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
||||||
"engine": engine_name, "detail": "not running"}
|
|
||||||
try:
|
|
||||||
r = subprocess.run(
|
|
||||||
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
||||||
via, f"ps aux | grep '{pattern}' | grep -v grep || true"],
|
|
||||||
capture_output=True, text=True, timeout=8)
|
|
||||||
lines = [l for l in r.stdout.strip().split("\n") if l.strip()]
|
|
||||||
if lines:
|
|
||||||
result["status"] = "online"
|
|
||||||
result["proc_count"] = len(lines)
|
|
||||||
result["detail"] = f"{result['proc_count']}프로세스"
|
|
||||||
except: pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
# ===== 메인 =====
|
|
||||||
def run_checks():
|
|
||||||
ts = datetime.now(KST)
|
|
||||||
ts_ip = get_local_ts_ip()
|
|
||||||
|
|
||||||
# --- PC checks ---
|
|
||||||
mainpc_ssh = ssh_check("mainpc-win", 6)
|
|
||||||
mainpc_ts = get_remote_ts("mainpc-wsl")
|
|
||||||
subpc_ssh = ssh_check("subpc-wsl", 6)
|
|
||||||
subpc_ts = get_remote_ts("subpc-wsl")
|
|
||||||
office_pc = tailscale_ping("office-pc", 6)
|
|
||||||
|
|
||||||
# --- NAS checks ---
|
|
||||||
nas_mh = ssh_check("mh-nas", 6)
|
|
||||||
nas_gwenc2 = tailscale_ping("gwenc-nas2", 6)
|
|
||||||
nas_bk = tailscale_ping("ps-bk-nas", 6)
|
|
||||||
|
|
||||||
# --- Agent checks ---
|
|
||||||
mimi = proc_detail("openclaw", "openclaw")
|
|
||||||
mimi["name"] = "미미"
|
|
||||||
mimi["emoji"] = "🦞"
|
|
||||||
mimi["version"] = get_version("openclaw")
|
|
||||||
|
|
||||||
ruki = proc_detail("hermes", "hermes")
|
|
||||||
ruki["name"] = "루키"
|
|
||||||
ruki["emoji"] = "🌱"
|
|
||||||
ruki["version"] = get_version("hermes")
|
|
||||||
|
|
||||||
pink = remote_proc("mainpc-wsl", "hermes", "hermes")
|
|
||||||
pink["name"] = "분홍"
|
|
||||||
pink["emoji"] = "🤖"
|
|
||||||
pink["version"] = get_remote_hermes_version("mainpc-wsl")
|
|
||||||
|
|
||||||
sky = remote_proc("subpc-wsl", "hermes", "hermes")
|
|
||||||
sky["name"] = "하늘"
|
|
||||||
sky["emoji"] = "🤖"
|
|
||||||
sky["version"] = get_remote_hermes_version("subpc-wsl")
|
|
||||||
|
|
||||||
# --- Subdomains ---
|
|
||||||
subdomains = [
|
|
||||||
{"name": "wiki.pinksky.kr", "url": "https://wiki.pinksky.kr/", "group": "pinksky.kr", "desc": "MiniCITY 지식정원"},
|
|
||||||
{"name": "openclaw.pinksky.kr", "url": "https://openclaw.pinksky.kr/", "group": "pinksky.kr", "desc": "OpenClaw 대시보드"},
|
|
||||||
{"name": "agent.pinksky.kr", "url": "https://agent.pinksky.kr/", "group": "pinksky.kr", "desc": "인프라 현황판"},
|
|
||||||
{"name": "search.pinksky.kr", "url": "https://search.pinksky.kr/search?q=test", "group": "pinksky.kr", "desc": "SearXNG 메타서치"},
|
|
||||||
{"name": "erp.pinksky.kr", "url": "https://erp.pinksky.kr/", "group": "pinksky.kr", "desc": "공사관리 ERP"},
|
|
||||||
{"name": "dify.pinksky.kr", "url": "https://dify.pinksky.kr/", "group": "pinksky.kr", "desc": "Dify 워크플로우"},
|
|
||||||
{"name": "photo.pinksky.kr", "url": "https://photo.pinksky.kr/", "group": "pinksky.kr", "desc": "PhotoVault 자료정리"},
|
|
||||||
{"name": "gwenc.kr", "url": "https://gwenc.kr/", "group": "gwenc.kr", "desc": "회사 메인 사이트"},
|
|
||||||
{"name": "minicity.kr", "url": "https://minicity.kr/", "group": "minicity.kr", "desc": "집 NAS 웹 UI"},
|
|
||||||
]
|
|
||||||
sub_results = []
|
|
||||||
for s in subdomains:
|
|
||||||
chk = http_check(s["url"], 4)
|
|
||||||
sub_results.append({
|
|
||||||
"name": s["name"], "group": s["group"], "desc": s["desc"],
|
|
||||||
"status": chk["status"], "detail": chk["detail"], "latency_ms": chk.get("latency_ms")
|
|
||||||
})
|
|
||||||
|
|
||||||
# --- Domain hierarchy ---
|
|
||||||
domains = {
|
|
||||||
"pinksky.kr": {
|
|
||||||
"domain": {"status": "online", "detail": f"{ts_ip}"},
|
|
||||||
"pcs": {
|
|
||||||
"macmini": {
|
|
||||||
**PC_SPECS["macmini"],
|
|
||||||
"tailscale_ip": ts_ip,
|
|
||||||
"status": "online", "detail": "",
|
|
||||||
"agents": {
|
|
||||||
"mimi": {**mimi},
|
|
||||||
"ruki": {**ruki},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mainpc": {
|
|
||||||
**PC_SPECS["mainpc"],
|
|
||||||
"tailscale_ip": mainpc_ts,
|
|
||||||
"status": mainpc_ssh["status"], "detail": mainpc_ssh["detail"],
|
|
||||||
"agents": {
|
|
||||||
"pink": {**pink},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"subpc": {
|
|
||||||
**PC_SPECS["subpc"],
|
|
||||||
"tailscale_ip": subpc_ts,
|
|
||||||
"status": subpc_ssh["status"], "detail": subpc_ssh["detail"],
|
|
||||||
"agents": {
|
|
||||||
"sky": {**sky},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minicity.kr": {
|
|
||||||
"domain": {"status": "online", "detail": "집 NAS/공유기"},
|
|
||||||
"pcs": {
|
|
||||||
"nas_home": {
|
|
||||||
**PC_SPECS["nas_home"],
|
|
||||||
"tailscale_ip": "100.69.107.65",
|
|
||||||
"status": nas_mh["status"], "detail": nas_mh["detail"],
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"router_home": {
|
|
||||||
**PC_SPECS["router_home"],
|
|
||||||
"tailscale_ip": "",
|
|
||||||
"status": "online", "detail": "DDNS · 원격관리 9090",
|
|
||||||
"agents": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gwenc.kr": {
|
|
||||||
"domain": {"status": office_pc["status"], "detail": office_pc["detail"]},
|
|
||||||
"pcs": {
|
|
||||||
"officepc": {
|
|
||||||
**PC_SPECS["officepc"],
|
|
||||||
"tailscale_ip": "",
|
|
||||||
"status": office_pc["status"], "detail": office_pc["detail"],
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"nas_office": {
|
|
||||||
**PC_SPECS["nas_office"],
|
|
||||||
"tailscale_ip": "100.105.95.19",
|
|
||||||
"status": nas_gwenc2["status"], "detail": nas_gwenc2["detail"],
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"nas_backup": {
|
|
||||||
**PC_SPECS["nas_backup"],
|
|
||||||
"tailscale_ip": "100.83.176.55",
|
|
||||||
"status": nas_bk["status"], "detail": nas_bk["detail"],
|
|
||||||
"agents": {}
|
|
||||||
},
|
|
||||||
"router_office": {
|
|
||||||
**PC_SPECS["router_office"],
|
|
||||||
"tailscale_ip": "",
|
|
||||||
"status": "online", "detail": "원격관리 9000/9443",
|
|
||||||
"agents": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
all_items = []
|
|
||||||
for d in domains.values():
|
|
||||||
for pc in d["pcs"].values():
|
|
||||||
all_items.append(pc)
|
|
||||||
for a in pc.get("agents", {}).values():
|
|
||||||
all_items.append(a)
|
|
||||||
for s in sub_results:
|
|
||||||
all_items.append(s)
|
|
||||||
|
|
||||||
online = sum(1 for x in all_items if x["status"] == "online")
|
|
||||||
offline = sum(1 for x in all_items if x["status"] == "offline")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"subdomains": sub_results,
|
|
||||||
"domains": domains,
|
|
||||||
"summary": {
|
|
||||||
"total": len(all_items),
|
|
||||||
"online": online,
|
|
||||||
"offline": offline,
|
|
||||||
"domain_total": 3,
|
|
||||||
"domain_online": sum(1 for d in domains.values() if d["domain"]["status"] == "online")
|
|
||||||
},
|
|
||||||
"timestamp": ts.isoformat(),
|
|
||||||
"timestamp_epoch": int(ts.timestamp())
|
|
||||||
}
|
|
||||||
|
|
||||||
STATUS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
print(f"[{ts.strftime('%H:%M:%S')}] Online {online}/{len(all_items)} · Domains {data['summary']['domain_online']}/3")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_checks()
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>작은도시 에이전트 현황</title>
|
|
||||||
<style>
|
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
||||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Pretendard',sans-serif;background:#0d1117;color:#e6edf3;min-height:100vh}
|
|
||||||
.container{max-width:1100px;margin:0 auto;padding:0 20px}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header{padding:28px 0 16px;border-bottom:1px solid #21262d;margin-bottom:24px}
|
|
||||||
.header h1{font-size:26px;font-weight:700;display:flex;align-items:center;gap:8px}
|
|
||||||
.header h1 small{font-size:13px;font-weight:400;color:#8b949e}
|
|
||||||
.summary-bar{display:flex;gap:20px;margin-top:10px;flex-wrap:wrap}
|
|
||||||
.summary-bar .stat{display:flex;align-items:center;gap:5px;font-size:13px}
|
|
||||||
.dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex-shrink:0}
|
|
||||||
.dot.on{background:#3fb950;box-shadow:0 0 6px rgba(63,185,80,.4)}
|
|
||||||
.dot.off{background:#f85149;box-shadow:0 0 6px rgba(248,81,73,.4)}
|
|
||||||
.dot.warn{background:#d29922}
|
|
||||||
.last-updated{font-size:12px;color:#484f58;margin-top:6px}
|
|
||||||
|
|
||||||
/* Tabs */
|
|
||||||
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid #21262d}
|
|
||||||
.tab-btn{
|
|
||||||
padding:10px 20px;font-size:14px;font-weight:500;cursor:pointer;
|
|
||||||
border:none;background:transparent;color:#8b949e;
|
|
||||||
border-bottom:2px solid transparent;transition:all .15s
|
|
||||||
}
|
|
||||||
.tab-btn:hover{color:#e6edf3}
|
|
||||||
.tab-btn.active{color:#58a6ff;border-bottom-color:#58a6ff}
|
|
||||||
.tab-content{display:none}
|
|
||||||
.tab-content.active{display:block}
|
|
||||||
|
|
||||||
/* Domain Cards */
|
|
||||||
.card{
|
|
||||||
background:#161b22;border:1px solid #21262d;border-radius:12px;
|
|
||||||
margin-bottom:20px;overflow:hidden
|
|
||||||
}
|
|
||||||
.card-header{
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
padding:14px 18px;background:#1c2333;border-bottom:1px solid #21262d
|
|
||||||
}
|
|
||||||
.card-header .ch-left{display:flex;align-items:center;gap:10px}
|
|
||||||
.card-header .ch-name{font-size:16px;font-weight:700;font-family:'SF Mono',monospace}
|
|
||||||
.card-header .ch-name .proto{color:#484f58;font-weight:400}
|
|
||||||
.tag{
|
|
||||||
font-size:11px;padding:2px 10px;border-radius:12px;font-weight:600
|
|
||||||
}
|
|
||||||
.tag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.tag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
.tag.er{background:rgba(210,153,34,.15);color:#d29922}
|
|
||||||
|
|
||||||
/* PC Sub-box */
|
|
||||||
.pc-box{
|
|
||||||
margin:12px 18px;border:1px solid #21262d;border-radius:10px;
|
|
||||||
background:#0d1117;overflow:hidden
|
|
||||||
}
|
|
||||||
.pc-box:first-child{margin-top:14px}
|
|
||||||
|
|
||||||
/* PC Row */
|
|
||||||
.pc-row{
|
|
||||||
display:flex;align-items:flex-start;justify-content:space-between;
|
|
||||||
padding:10px 14px 4px;gap:12px
|
|
||||||
}
|
|
||||||
.pc-left{display:flex;align-items:flex-start;gap:10px;min-width:0;flex:1}
|
|
||||||
.pc-left .emoji{font-size:20px;flex-shrink:0;line-height:1.3}
|
|
||||||
.pc-left .pcinfo{min-width:0}
|
|
||||||
.pc-left .pname{font-size:14px;font-weight:500;line-height:1.3}
|
|
||||||
.pc-spec{font-size:11px;color:#7d8590;margin-top:1px}
|
|
||||||
.pc-net{font-size:11px;color:#484f58;margin-top:1px;font-family:'SF Mono',monospace}
|
|
||||||
.pc-right{display:flex;align-items:flex-start;gap:8px;flex-shrink:0;padding-top:2px}
|
|
||||||
.ptag{font-size:11px;padding:1px 8px;border-radius:10px;font-weight:500}
|
|
||||||
.ptag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.ptag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
.ptag.er{background:rgba(210,153,34,.15);color:#d29922}
|
|
||||||
|
|
||||||
/* Agents */
|
|
||||||
.agents-wrap{
|
|
||||||
padding:0 14px 10px;display:flex;flex-direction:column;gap:6px
|
|
||||||
}
|
|
||||||
.agents-wrap .agent-block:first-child{margin-top:4px}
|
|
||||||
.agent-block{
|
|
||||||
display:flex;flex-direction:column;
|
|
||||||
font-size:12px;padding:5px 10px;border-radius:8px;
|
|
||||||
border:1px solid #21262d;background:#161b22
|
|
||||||
}
|
|
||||||
.agent-top{display:flex;align-items:center;gap:6px}
|
|
||||||
.agent-top .emoji{font-size:15px}
|
|
||||||
.agent-top .aname{color:#e6edf3;font-weight:500}
|
|
||||||
.agent-top .aengine{font-size:10px;color:#8b949e;font-family:'SF Mono',monospace}
|
|
||||||
.agent-top .atag{font-size:10px;padding:1px 6px;border-radius:8px;font-weight:500}
|
|
||||||
.agent-top .atag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.agent-top .atag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
.agent-info{font-size:11px;color:#8b949e;margin-top:2px;margin-left:21px;font-family:'SF Mono',monospace}
|
|
||||||
.agent-mem{font-size:10px;color:#7d8590;margin-top:1px;margin-left:21px;font-family:'SF Mono',monospace}
|
|
||||||
|
|
||||||
/* Subdomain rows */
|
|
||||||
.sub-row{
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
padding:12px 18px;border-bottom:1px solid #1c2333;gap:12px
|
|
||||||
}
|
|
||||||
.sub-row:last-child{border-bottom:none}
|
|
||||||
.sub-left{display:flex;align-items:center;gap:10px;min-width:0;flex:1}
|
|
||||||
.sub-left .semoji{font-size:16px;flex-shrink:0}
|
|
||||||
.sub-left .sname{font-size:14px;font-weight:500;font-family:'SF Mono',monospace}
|
|
||||||
.sub-left .sname .sproto{color:#484f58;font-weight:400}
|
|
||||||
.sub-left .sgroup{font-size:11px;color:#484f58;margin-left:6px}
|
|
||||||
.sub-mid{flex:0 0 auto;padding:0 10px}
|
|
||||||
.sub-mid .sdesc{font-size:12px;color:#8b949e;white-space:nowrap}
|
|
||||||
.sub-right{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
||||||
.sub-right .slatency{font-size:11px;color:#8b949e}
|
|
||||||
.stag{font-size:11px;padding:1px 8px;border-radius:10px;font-weight:500}
|
|
||||||
.stag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.stag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
.stag.er{background:rgba(210,153,34,.15);color:#d29922}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer{text-align:center;padding:30px 0;border-top:1px solid #21262d;font-size:12px;color:#484f58}
|
|
||||||
|
|
||||||
@media(max-width:640px){
|
|
||||||
.agents-wrap{padding-left:10px}
|
|
||||||
.pc-row,.sub-row{flex-wrap:wrap}
|
|
||||||
.header h1{font-size:20px}
|
|
||||||
.tabs{overflow-x:auto}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1>🏙️ 작은도시 에이전트 현황</h1>
|
|
||||||
<div class="summary-bar" id="summary-bar">
|
|
||||||
<span class="stat"><span class="dot on"></span> <span id="online-count">0</span> Online</span>
|
|
||||||
<span class="stat"><span class="dot off"></span> <span id="offline-count">0</span> Offline</span>
|
|
||||||
<span class="stat"><span class="dot domain"></span> 도메인 <span id="domain-online">0</span>/<span id="domain-total">4</span></span>
|
|
||||||
</div>
|
|
||||||
<div class="last-updated" id="last-updated">갱신 중...</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="tabs" id="tabs">
|
|
||||||
<button class="tab-btn active" data-tab="agents">🤖 에이전트</button>
|
|
||||||
<button class="tab-btn" data-tab="subdomains">🌐 서브도메인</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tab-agents" class="tab-content active"></div>
|
|
||||||
<div id="tab-subdomains" class="tab-content"></div>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
작은도시 에이전트 현황 · 1분마다 자동 갱신
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function statusClass(st) {
|
|
||||||
if (st === "online") return "on";
|
|
||||||
if (st === "error") return "er";
|
|
||||||
return "off";
|
|
||||||
}
|
|
||||||
function statusLabel(st) {
|
|
||||||
if (st === "online") return "● 온라인";
|
|
||||||
if (st === "error") return "● 오류";
|
|
||||||
return "● 오프라인";
|
|
||||||
}
|
|
||||||
|
|
||||||
const DOMAIN_ICONS = {"pinksky.kr":"🏠","minicity.kr":"🏡","pinksky.iptime.org":"🏡","gwenc.kr":"🏢"};
|
|
||||||
const GROUP_ICONS = {"pinksky.kr":"🏠","minicity.kr":"🏡","pinksky.iptime.org":"🏡","gwenc.kr":"🏢"};
|
|
||||||
|
|
||||||
function buildAgents(data) {
|
|
||||||
let html = "";
|
|
||||||
for (const dname of ["pinksky.kr","minicity.kr","pinksky.iptime.org","gwenc.kr"]) {
|
|
||||||
const dom = data.domains[dname];
|
|
||||||
if (!dom) continue;
|
|
||||||
const icon = DOMAIN_ICONS[dname] || "🌐";
|
|
||||||
const tcls = statusClass(dom.domain.status);
|
|
||||||
html += `<div class="card"><div class="card-header">`;
|
|
||||||
html += `<div class="ch-left"><span>${icon}</span><span class="ch-name"><span class="proto">https://</span>${dname}</span></div>`;
|
|
||||||
html += `<span class="tag ${tcls}">${statusLabel(dom.domain.status)}</span></div>`;
|
|
||||||
for (const pc of Object.values(dom.pcs)) {
|
|
||||||
const pcls = statusClass(pc.status);
|
|
||||||
html += `<div class="pc-box">`;
|
|
||||||
html += `<div class="pc-row"><div class="pc-left"><span class="emoji">${pc.emoji}</span>`;
|
|
||||||
html += `<div class="pcinfo"><div class="pname">${pc.name}</div>`;
|
|
||||||
if (pc.spec) {
|
|
||||||
html += `<div class="pc-spec">${pc.spec}</div>`;
|
|
||||||
}
|
|
||||||
if (pc.tailscale_ip && pc.dns) {
|
|
||||||
html += `<div class="pc-net">${pc.tailscale_ip} · ${pc.dns}</div>`;
|
|
||||||
}
|
|
||||||
html += `</div></div>`;
|
|
||||||
html += `<div class="pc-right"><span class="ptag ${pcls}">${statusLabel(pc.status)}</span></div></div>`;
|
|
||||||
const agents = Object.values(pc.agents || {});
|
|
||||||
if (agents.length) {
|
|
||||||
html += `<div class="agents-wrap">`;
|
|
||||||
for (const a of agents) {
|
|
||||||
const acls = statusClass(a.status);
|
|
||||||
const eng = a.engine && a.version ? `${a.engine}_v${a.version}` : '';
|
|
||||||
html += `<div class="agent-block"><div class="agent-top"><span class="emoji">${a.emoji}</span><span class="aname">${a.name}</span>`;
|
|
||||||
if (eng) html += `<span class="aengine">(${eng})</span>`;
|
|
||||||
html += `<span class="atag ${acls}">${statusLabel(a.status)}</span></div>`;
|
|
||||||
// 가동시간 · 프로세스
|
|
||||||
if (a.uptime && a.uptime !== "?") {
|
|
||||||
html += `<div class="agent-info">${a.uptime} · ${a.proc_count}프로세스</div>`;
|
|
||||||
}
|
|
||||||
// 메모리 · VRAM
|
|
||||||
let memLine = "";
|
|
||||||
if (a.mem && a.mem !== "?") memLine += a.mem;
|
|
||||||
if (a.gpu && a.gpu !== "?") memLine += (memLine ? " · " : "") + a.gpu;
|
|
||||||
if (memLine) {
|
|
||||||
html += `<div class="agent-mem">${memLine}</div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSubdomains(data) {
|
|
||||||
if (!data.subdomains || !data.subdomains.length) {
|
|
||||||
return `<div style="text-align:center;padding:40px;color:#8b949e">서브도메인 데이터 없음</div>`;
|
|
||||||
}
|
|
||||||
let html = "";
|
|
||||||
for (const sd of data.subdomains) {
|
|
||||||
const cls = statusClass(sd.status);
|
|
||||||
const gicon = GROUP_ICONS[sd.group] || "🌐";
|
|
||||||
html += `<div class="sub-row">`;
|
|
||||||
html += `<div class="sub-left"><span class="semoji">${gicon}</span><a href="https://${sd.name}" target="_blank" rel="noopener" style="color:inherit;text-decoration:none"><span class="sname"><span class="sproto">https://</span>${sd.name}</span></a><span class="sgroup">${sd.group}</span></div>`;
|
|
||||||
html += `<div class="sub-mid"><span class="sdesc">${sd.desc || ''}</span></div>`;
|
|
||||||
html += `<div class="sub-right">`;
|
|
||||||
if (sd.latency_ms != null) html += `<span class="slatency">${sd.latency_ms}ms</span>`;
|
|
||||||
html += `<span class="stag ${cls}">${statusLabel(sd.status)}</span></div></div>`;
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSummary(data) {
|
|
||||||
document.getElementById("online-count").textContent = data.summary.online;
|
|
||||||
document.getElementById("offline-count").textContent = data.summary.offline;
|
|
||||||
document.getElementById("domain-online").textContent = data.summary.domain_online;
|
|
||||||
document.getElementById("domain-total").textContent = data.summary.domain_total;
|
|
||||||
const ts = new Date(data.timestamp_epoch * 1000);
|
|
||||||
document.getElementById("last-updated").textContent =
|
|
||||||
`마지막 갱신: ${ts.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" })}`;
|
|
||||||
document.title = `작은도시 에이전트 현황 (${data.summary.online}/${data.summary.total})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("click", function(e) {
|
|
||||||
const btn = e.target.closest(".tab-btn");
|
|
||||||
if (!btn) return;
|
|
||||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
|
||||||
document.querySelectorAll(".tab-content").forEach(t => t.classList.remove("active"));
|
|
||||||
btn.classList.add("active");
|
|
||||||
document.getElementById("tab-" + btn.dataset.tab).classList.add("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchStatus() {
|
|
||||||
try {
|
|
||||||
const res = await fetch("health.json?" + Date.now());
|
|
||||||
const data = await res.json();
|
|
||||||
document.getElementById("tab-agents").innerHTML = buildAgents(data);
|
|
||||||
document.getElementById("tab-subdomains").innerHTML = buildSubdomains(data);
|
|
||||||
updateSummary(data);
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById("tab-agents").innerHTML =
|
|
||||||
`<div style="text-align:center;padding:40px;color:#f85149">❌ 데이터를 불러오지 못했습니다</div>`;
|
|
||||||
document.getElementById("tab-subdomains").innerHTML = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchStatus();
|
|
||||||
setInterval(fetchStatus, 60000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>작은도시 인프라 현황</title>
|
|
||||||
<style>
|
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
||||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Pretendard',sans-serif;background:#0d1117;color:#e6edf3;min-height:100vh}
|
|
||||||
.container{max-width:1100px;margin:0 auto;padding:0 20px}
|
|
||||||
|
|
||||||
.header{padding:28px 0 16px;border-bottom:1px solid #21262d;margin-bottom:24px}
|
|
||||||
.header h1{font-size:26px;font-weight:700;display:flex;align-items:center;gap:8px}
|
|
||||||
.header h1 small{font-size:13px;font-weight:400;color:#8b949e}
|
|
||||||
.summary-bar{display:flex;gap:20px;margin-top:10px;flex-wrap:wrap}
|
|
||||||
.summary-bar .stat{display:flex;align-items:center;gap:5px;font-size:13px}
|
|
||||||
.dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex-shrink:0}
|
|
||||||
.dot.on{background:#3fb950;box-shadow:0 0 6px rgba(63,185,80,.4)}
|
|
||||||
.dot.off{background:#f85149;box-shadow:0 0 6px rgba(248,81,73,.4)}
|
|
||||||
.dot.warn{background:#d29922}
|
|
||||||
.last-updated{font-size:12px;color:#484f58;margin-top:6px}
|
|
||||||
|
|
||||||
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid #21262d;flex-wrap:wrap}
|
|
||||||
.tab-btn{
|
|
||||||
padding:10px 18px;font-size:14px;font-weight:500;cursor:pointer;
|
|
||||||
border:none;background:transparent;color:#8b949e;
|
|
||||||
border-bottom:2px solid transparent;transition:all .15s
|
|
||||||
}
|
|
||||||
.tab-btn:hover{color:#e6edf3}
|
|
||||||
.tab-btn.active{color:#58a6ff;border-bottom-color:#58a6ff}
|
|
||||||
.tab-content{display:none}
|
|
||||||
.tab-content.active{display:block}
|
|
||||||
|
|
||||||
.card{
|
|
||||||
background:#161b22;border:1px solid #21262d;border-radius:12px;
|
|
||||||
margin-bottom:16px;overflow:hidden
|
|
||||||
}
|
|
||||||
.card-header{
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
padding:14px 18px;background:#1c2333;border-bottom:1px solid #21262d
|
|
||||||
}
|
|
||||||
.card-header .ch-left{display:flex;align-items:center;gap:10px}
|
|
||||||
.card-header .ch-name{font-size:16px;font-weight:700}
|
|
||||||
.tag{
|
|
||||||
font-size:11px;padding:2px 10px;border-radius:12px;font-weight:600
|
|
||||||
}
|
|
||||||
.tag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.tag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
.tag.er{background:rgba(210,153,34,.15);color:#d29922}
|
|
||||||
|
|
||||||
.item-row{
|
|
||||||
display:flex;align-items:flex-start;justify-content:space-between;
|
|
||||||
padding:12px 18px;border-bottom:1px solid #1c2333;gap:12px
|
|
||||||
}
|
|
||||||
.item-row:last-child{border-bottom:none}
|
|
||||||
.item-left{display:flex;align-items:flex-start;gap:10px;min-width:0;flex:1}
|
|
||||||
.item-left .emoji{font-size:20px;flex-shrink:0;line-height:1.3}
|
|
||||||
.item-info{min-width:0}
|
|
||||||
.item-name{font-size:14px;font-weight:500;line-height:1.3}
|
|
||||||
.item-spec{font-size:11px;color:#7d8590;margin-top:1px}
|
|
||||||
.item-net{font-size:11px;color:#484f58;margin-top:1px;font-family:'SF Mono',monospace}
|
|
||||||
.item-right{display:flex;align-items:flex-start;gap:8px;flex-shrink:0;padding-top:2px}
|
|
||||||
.ptag{font-size:11px;padding:1px 8px;border-radius:10px;font-weight:500}
|
|
||||||
.ptag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.ptag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
.ptag.er{background:rgba(210,153,34,.15);color:#d29922}
|
|
||||||
|
|
||||||
.detail{font-size:11px;color:#8b949e;margin-top:2px;font-family:'SF Mono',monospace}
|
|
||||||
|
|
||||||
.sub-row{
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
padding:12px 18px;border-bottom:1px solid #1c2333;gap:12px
|
|
||||||
}
|
|
||||||
.sub-row:last-child{border-bottom:none}
|
|
||||||
.sub-left{display:flex;align-items:center;gap:10px;min-width:0;flex:1}
|
|
||||||
.sub-left .semoji{font-size:16px;flex-shrink:0}
|
|
||||||
.sname{font-size:14px;font-weight:500;font-family:'SF Mono',monospace}
|
|
||||||
.sname .sproto{color:#484f58;font-weight:400}
|
|
||||||
.sgroup{font-size:11px;color:#484f58;margin-left:6px}
|
|
||||||
.sub-right{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
||||||
.stag{font-size:11px;padding:1px 8px;border-radius:10px;font-weight:500}
|
|
||||||
.stag.on{background:rgba(63,185,80,.15);color:#3fb950}
|
|
||||||
.stag.off{background:rgba(248,81,73,.15);color:#f85149}
|
|
||||||
|
|
||||||
.footer{text-align:center;padding:30px 0;border-top:1px solid #21262d;font-size:12px;color:#484f58}
|
|
||||||
|
|
||||||
@media(max-width:640px){
|
|
||||||
.item-row,.sub-row{flex-wrap:wrap}
|
|
||||||
.header h1{font-size:20px}
|
|
||||||
.tabs{overflow-x:auto}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1>🏙️ 작은도시 인프라 현황</h1>
|
|
||||||
<div class="summary-bar" id="summary-bar">
|
|
||||||
<span class="stat"><span class="dot on"></span> <span id="online-count">0</span> Online</span>
|
|
||||||
<span class="stat"><span class="dot off"></span> <span id="offline-count">0</span> Offline</span>
|
|
||||||
</div>
|
|
||||||
<div class="last-updated" id="last-updated">갱신 중...</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="tabs" id="tabs">
|
|
||||||
<button class="tab-btn active" data-tab="pc">🖥️ PC</button>
|
|
||||||
<button class="tab-btn" data-tab="nas">🗄️ NAS</button>
|
|
||||||
<button class="tab-btn" data-tab="router">📡 공유기</button>
|
|
||||||
<button class="tab-btn" data-tab="agent">🤖 에이전트</button>
|
|
||||||
<button class="tab-btn" data-tab="domain">🌐 도메인</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tab-pc" class="tab-content active"></div>
|
|
||||||
<div id="tab-nas" class="tab-content"></div>
|
|
||||||
<div id="tab-router" class="tab-content"></div>
|
|
||||||
<div id="tab-agent" class="tab-content"></div>
|
|
||||||
<div id="tab-domain" class="tab-content"></div>
|
|
||||||
|
|
||||||
<footer class="footer">작은도시 인프라 현황 · 1분마다 자동 갱신</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function statusClass(st) {
|
|
||||||
if (st === "online") return "on";
|
|
||||||
if (st === "error") return "er";
|
|
||||||
return "off";
|
|
||||||
}
|
|
||||||
function statusLabel(st) {
|
|
||||||
if (st === "online") return "● 온라인";
|
|
||||||
if (st === "error") return "● 오류";
|
|
||||||
return "● 오프라인";
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCard(title, items) {
|
|
||||||
if (!items || !items.length) return `<div style="text-align:center;padding:40px;color:#8b949e">데이터 없음</div>`;
|
|
||||||
let html = `<div class="card"><div class="card-header"><div class="ch-left"><span class="ch-name">${title}</span></div></div>`;
|
|
||||||
for (const item of items) {
|
|
||||||
const cls = statusClass(item.status);
|
|
||||||
html += `<div class="item-row">`;
|
|
||||||
html += `<div class="item-left"><span class="emoji">${item.emoji || '📦'}</span>`;
|
|
||||||
html += `<div class="item-info"><div class="item-name">${item.name}</div>`;
|
|
||||||
if (item.spec) html += `<div class="item-spec">${item.spec}</div>`;
|
|
||||||
if (item.net) html += `<div class="item-net">${item.net}</div>`;
|
|
||||||
if (item.detail) html += `<div class="detail">${item.detail}</div>`;
|
|
||||||
html += `</div></div>`;
|
|
||||||
html += `<div class="item-right"><span class="ptag ${cls}">${statusLabel(item.status)}</span></div></div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAgentsTab(items) {
|
|
||||||
if (!items || !items.length) return `<div style="text-align:center;padding:40px;color:#8b949e">에이전트 데이터 없음</div>`;
|
|
||||||
let html = `<div class="card"><div class="card-header"><div class="ch-left"><span class="ch-name">🤖 에이전트</span></div></div>`;
|
|
||||||
for (const a of items) {
|
|
||||||
const cls = statusClass(a.status);
|
|
||||||
const eng = a.engine && a.version ? `${a.engine}_v${a.version}` : '';
|
|
||||||
html += `<div class="item-row">`;
|
|
||||||
html += `<div class="item-left"><span class="emoji">${a.emoji || '🤖'}</span>`;
|
|
||||||
html += `<div class="item-info">`;
|
|
||||||
html += `<div class="item-name">${a.name}${eng ? ' <span style="font-size:11px;color:#8b949e;font-family:monospace">(' + eng + ')</span>' : ''}</div>`;
|
|
||||||
if (a.host) html += `<div class="item-net">${a.host}</div>`;
|
|
||||||
let infoLine = '';
|
|
||||||
if (a.uptime && a.uptime !== '?') infoLine += a.uptime;
|
|
||||||
if (a.proc_count != null) infoLine += (infoLine ? ' · ' : '') + a.proc_count + '프로세스';
|
|
||||||
if (a.mem) infoLine += (infoLine ? ' · ' : '') + a.mem;
|
|
||||||
if (infoLine) html += `<div class="detail">${infoLine}</div>`;
|
|
||||||
html += `</div></div>`;
|
|
||||||
html += `<div class="item-right"><span class="ptag ${cls}">${statusLabel(a.status)}</span></div></div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDomainsTab(items) {
|
|
||||||
if (!items || !items.length) return `<div style="text-align:center;padding:40px;color:#8b949e">도메인 데이터 없음</div>`;
|
|
||||||
let html = `<div class="card"><div class="card-header"><div class="ch-left"><span class="ch-name">🌐 도메인 및 서브도메인</span></div></div>`;
|
|
||||||
for (const d of items) {
|
|
||||||
const cls = statusClass(d.status);
|
|
||||||
const gicon = {"pinksky.kr":"🏠","minicity.kr":"🏡","gwenc.kr":"🏢"}[d.group] || "🌐";
|
|
||||||
html += `<div class="sub-row">`;
|
|
||||||
html += `<div class="sub-left"><span class="semoji">${gicon}</span>`;
|
|
||||||
html += `<a href="https://${d.name}" target="_blank" rel="noopener" style="color:inherit;text-decoration:none"><span class="sname"><span class="sproto">https://</span>${d.name}</span></a>`;
|
|
||||||
html += `<span class="sgroup">${d.group}</span>`;
|
|
||||||
html += `</div>`;
|
|
||||||
html += `<div class="sub-right">`;
|
|
||||||
if (d.latency_ms != null) html += `<span style="font-size:11px;color:#8b949e;margin-right:6px">${d.latency_ms}ms</span>`;
|
|
||||||
html += `<span class="stag ${cls}">${statusLabel(d.status)}</span></div></div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSummary(data) {
|
|
||||||
const all = [
|
|
||||||
...(data.pcs || []),
|
|
||||||
...(data.nas || []),
|
|
||||||
...(data.routers || []),
|
|
||||||
...(data.agents || []),
|
|
||||||
...(data.domains || [])
|
|
||||||
];
|
|
||||||
const online = all.filter(x => x.status === "online").length;
|
|
||||||
document.getElementById("online-count").textContent = online;
|
|
||||||
document.getElementById("offline-count").textContent = all.length - online;
|
|
||||||
const ts = data.timestamp ? new Date(data.timestamp_epoch * 1000) : new Date();
|
|
||||||
document.getElementById("last-updated").textContent =
|
|
||||||
`마지막 갱신: ${ts.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" })}`;
|
|
||||||
document.title = `작은도시 인프라 현황 (${online}/${all.length})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStatus() {
|
|
||||||
try {
|
|
||||||
const res = await fetch("health.json?" + Date.now());
|
|
||||||
const data = await res.json();
|
|
||||||
document.getElementById("tab-pc").innerHTML = buildCard("🖥️ PC", data.pcs || []);
|
|
||||||
document.getElementById("tab-nas").innerHTML = buildCard("🗄️ NAS", data.nas || []);
|
|
||||||
document.getElementById("tab-router").innerHTML = buildCard("📡 공유기", data.routers || []);
|
|
||||||
document.getElementById("tab-agent").innerHTML = buildAgentsTab(data.agents || []);
|
|
||||||
document.getElementById("tab-domain").innerHTML = buildDomainsTab(data.domains || []);
|
|
||||||
updateSummary(data);
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById("tab-pc").innerHTML =
|
|
||||||
`<div style="text-align:center;padding:40px;color:#f85149">❌ 데이터를 불러오지 못했습니다</div>`;
|
|
||||||
["tab-nas","tab-router","tab-agent","tab-domain"].forEach(id => document.getElementById(id).innerHTML = "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("click", function(e) {
|
|
||||||
const btn = e.target.closest(".tab-btn");
|
|
||||||
if (!btn) return;
|
|
||||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
|
||||||
document.querySelectorAll(".tab-content").forEach(t => t.classList.remove("active"));
|
|
||||||
btn.classList.add("active");
|
|
||||||
document.getElementById("tab-" + btn.dataset.tab).classList.add("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchStatus();
|
|
||||||
setInterval(fetchStatus, 60000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 🏙️ MiniCITY v0.5 — 의견 요청 (분홍)
|
|
||||||
|
|
||||||
안녕하세요 분홍! 저는 루키입니다.
|
|
||||||
|
|
||||||
아빠(미미아빠)의 요청으로 MiniCITY 옵시디언 볼트를 v0.5로 재구조화하려고 합니다.
|
|
||||||
현재 문제점: 17,681개 파일 중 11,620개(66%)가 ERP 소스코드 중복입니다. 실제 문서는 5~6천 개 수준.
|
|
||||||
|
|
||||||
## 제안 구조
|
|
||||||
```
|
|
||||||
MiniCITY(v0.5_PARA_0502-)/
|
|
||||||
├── 00_INBOX/
|
|
||||||
├── 01_PROJECTS/
|
|
||||||
│ ├── A_mini-happy/
|
|
||||||
│ ├── B_선번조사툴/ ← 분홍 담당
|
|
||||||
│ ├── C_공사관리ERP/ ← 분홍 담당
|
|
||||||
│ ├── D_자료관리/
|
|
||||||
│ ├── E_공가신청해지/
|
|
||||||
│ ├── H_음성알림시스템/
|
|
||||||
│ └── AgentStatusDash/
|
|
||||||
├── 02_AREAS/
|
|
||||||
│ ├── 인프라/
|
|
||||||
│ └── 에이전트/
|
|
||||||
│ ├── 미미/
|
|
||||||
│ ├── 루키/
|
|
||||||
│ ├── 분홍/ ← 분홍 개인 작업공간
|
|
||||||
│ └── 하늘/
|
|
||||||
├── 03_RESOURCES/
|
|
||||||
├── 04_ARCHIVE/
|
|
||||||
└── 98_HISTORY/
|
|
||||||
└── 99_TEMPLATES/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 분홍에게 묻고 싶은 점
|
|
||||||
1. B_선번조사툴 / C_공사관리ERP 문서를 v0.x_구상/초안/진행중 3단계로 정리하는 것 괜찮나요?
|
|
||||||
2. 작업하던 ERP 소스코드가 옵시디언에 11,620개나 중복되어 있는데, 전부 제거해도 되나요? (git 저장소로 이동)
|
|
||||||
3. 분홍 개인 작업공간(02_AREAS/에이전트/분홍/)이 필요하다면 어떤 내용을 담고 싶나요?
|
|
||||||
4. 기타 제안이나 우려사항이 있으면 알려주세요.
|
|
||||||
|
|
||||||
회신은 이 파일 아래에 작성해주세요. (파일을 수정해서 답변을 추가해주세요)
|
|
||||||
---
|
|
||||||
의견:
|
|
||||||
Reference in New Issue
Block a user