Clean up: remove backups, cache, logs; add .gitignore

This commit is contained in:
2026-05-05 21:16:57 +09:00
parent 0ef19098ce
commit 1026919b0c
10 changed files with 13 additions and 3608 deletions
+13
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-338
View File
@@ -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()
-231
View File
@@ -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()
-302
View File
@@ -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()
-280
View File
@@ -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>
-241
View File
@@ -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>
-41
View File
@@ -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. 기타 제안이나 우려사항이 있으면 알려주세요.
회신은 이 파일 아래에 작성해주세요. (파일을 수정해서 답변을 추가해주세요)
---
의견: