175 lines
5.2 KiB
Python
175 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Render a terminal-style HTML snapshot from a swarm poster summary JSON."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Render a terminal-style swarm snapshot HTML.")
|
|
parser.add_argument("summary_json", help="Path to the swarm poster summary JSON file.")
|
|
parser.add_argument(
|
|
"--output-html",
|
|
help="Optional output HTML path. Defaults next to the summary file.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def fmt_ts(ts: float | None) -> str:
|
|
if not ts:
|
|
return "-"
|
|
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
summary_path = Path(args.summary_json).resolve()
|
|
data = json.loads(summary_path.read_text(encoding="utf-8"))
|
|
|
|
swarm = data["swarm"]
|
|
task = data["task"]
|
|
metrics = data["metrics"]
|
|
|
|
out_path = (
|
|
Path(args.output_html).resolve()
|
|
if args.output_html
|
|
else summary_path.with_name(summary_path.stem + "-terminal.html")
|
|
)
|
|
|
|
budget_ratio = metrics.get("budget", {}).get("duration_ratio")
|
|
budget_display = f"{budget_ratio * 100:.1f}%" if isinstance(budget_ratio, (int, float)) else "-"
|
|
|
|
transcript = [
|
|
"$ python3 scripts/run_swarm_poster_demo.py",
|
|
f"[swarm] deployment_id={swarm['deployment_id']}",
|
|
f"[swarm] swarm_id={swarm['swarm_id']}",
|
|
f"[task] task_id={task['task_id']}",
|
|
f"[task] status={task['status']}",
|
|
f"[task] assigned_agent={task.get('assigned_agent_id') or '-'}",
|
|
"",
|
|
"$ curl -H \"Authorization: Bearer ***\" /api/swarms/{swarm_id}/tasks",
|
|
f"task.started_at = {fmt_ts(data.get('started_at'))}",
|
|
f"task.halfway_at = {fmt_ts(data.get('halfway_at'))}",
|
|
f"halfway.observed_at = {fmt_ts(data.get('halfway_observed_at'))}",
|
|
"",
|
|
"$ curl -H \"Authorization: Bearer ***\" /api/swarms/{swarm_id}/metrics",
|
|
f"runtime.status = {metrics.get('status')}",
|
|
f"runtime.tasks_total = {metrics.get('tasks_total')}",
|
|
f"runtime.tasks_by_status = {json.dumps(metrics.get('tasks_by_status', {}), ensure_ascii=False)}",
|
|
f"runtime.agents_connected = {metrics.get('agents_connected')}",
|
|
f"runtime.budget.duration_seconds = {metrics.get('budget', {}).get('duration_seconds')}",
|
|
f"runtime.budget.duration_ratio = {budget_display}",
|
|
"",
|
|
"# milestone",
|
|
f"> 任务开始: {fmt_ts(data.get('started_at'))}",
|
|
f"> 任务过半: {fmt_ts(data.get('halfway_observed_at'))}",
|
|
]
|
|
|
|
lines_html = "\n".join(f"<div class='line'>{line}</div>" for line in transcript)
|
|
|
|
html = f"""<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Swarm Terminal Snapshot</title>
|
|
<style>
|
|
body {{
|
|
margin: 0;
|
|
background:
|
|
radial-gradient(circle at top right, rgba(74, 222, 128, 0.10), transparent 20%),
|
|
radial-gradient(circle at top left, rgba(96, 165, 250, 0.10), transparent 24%),
|
|
#0a0f14;
|
|
color: #d7e3ee;
|
|
font-family: Menlo, Monaco, "SFMono-Regular", "JetBrains Mono", monospace;
|
|
}}
|
|
.frame {{
|
|
width: 1600px;
|
|
min-height: 900px;
|
|
margin: 0 auto;
|
|
padding: 48px;
|
|
box-sizing: border-box;
|
|
}}
|
|
.window {{
|
|
background: #0b1220;
|
|
border: 1px solid rgba(148, 163, 184, 0.20);
|
|
border-radius: 18px;
|
|
overflow: hidden;
|
|
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
|
|
}}
|
|
.topbar {{
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 18px;
|
|
background: #121a2a;
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
|
|
}}
|
|
.dots {{
|
|
display: flex;
|
|
gap: 8px;
|
|
}}
|
|
.dot {{
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
}}
|
|
.dot.red {{ background: #fb7185; }}
|
|
.dot.yellow {{ background: #fbbf24; }}
|
|
.dot.green {{ background: #4ade80; }}
|
|
.title {{
|
|
color: #93a4b8;
|
|
font-size: 15px;
|
|
letter-spacing: 0.02em;
|
|
}}
|
|
.terminal {{
|
|
padding: 28px 32px 36px;
|
|
font-size: 24px;
|
|
line-height: 1.7;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}}
|
|
.line:nth-child(1),
|
|
.line:nth-child(8),
|
|
.line:nth-child(13) {{
|
|
color: #7dd3fc;
|
|
}}
|
|
.line:nth-last-child(2),
|
|
.line:nth-last-child(1) {{
|
|
color: #bef264;
|
|
font-weight: 600;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="frame">
|
|
<div class="window">
|
|
<div class="topbar">
|
|
<div class="dots">
|
|
<span class="dot red"></span>
|
|
<span class="dot yellow"></span>
|
|
<span class="dot green"></span>
|
|
</div>
|
|
<div class="title">swarm-terminal-snapshot</div>
|
|
<div class="title">{swarm['swarm_id']}</div>
|
|
</div>
|
|
<div class="terminal">
|
|
{lines_html}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
out_path.write_text(html, encoding="utf-8")
|
|
print(out_path)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|