diff --git a/palladium-monitor/README.md b/palladium-monitor/README.md new file mode 100644 index 0000000..489e7ac --- /dev/null +++ b/palladium-monitor/README.md @@ -0,0 +1,64 @@ +# Palladium Z1 Monitor + +LD 逻辑板使用监控系统 — 解析 `test_server -short` 输出,可视化展示 Palladium Z1 仿真器的逻辑板状态、逻辑板利用率、作业信息和 T-Pod 可用性。 + +## 功能 + +- **逻辑板状态可视化** — Rack / Cluster / Board 三级结构,每块 LD 板的 8 个逻辑板 (D0-D7) 以色块展示 + - 🟢 可用 (`-`) + - 🔵 已加载/预留 (``) + - 🔴 已禁用 (`D`) + - ⬜ 不存在 (`X`) +- **实时指标** — 逻辑板总数、在线/离线、逻辑板利用率、活跃作业数 +- **作业信息** — 用户、PID、T-Pod、设计名、已运行时间 +- **T-Pod 可用性** — 按 Rack 展示 HDSB / T-Pod 的可用/锁定/预留/不可用状态 +- **历史趋势** — 利用率折线图 (Chart.js),支持历史快照查看和清理 +- **自动刷新** — 15s/30s/1m/5m 可选间隔 +- **多数据来源** — 手动粘贴、API 推送、定时采集脚本 + +## 部署 + +### 监控服务器 + +```bash +pip install -r requirements.txt --break-system-packages +python3 app.py +``` + +访问 `http://<服务器IP>:5100` + +### 数据采集 (Palladium 主机) + +在 Palladium 主机部署 `collect.sh`: + +```bash +chmod +x collect.sh + +# 测试 +./collect.sh --dry-run + +# crontab 每30分钟采集 +*/30 * * * * /path/to/collect.sh >> /var/log/palladium-collect.log 2>&1 +``` + +## 配置 + +collect.sh 可通过环境变量覆盖默认配置: + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `TEST_SERVER_CMD` | `/edatools/vxe/VXE23.03.001/bin/test_server -short` | 采集命令 | +| `MONITOR_URL` | `http://10.168.1.209:5100/api/collect` | 监控服务器 API | +| `TIMEOUT_SEC` | `300` | 命令超时秒数 | + +## API 端点 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/` | 仪表板页面 | +| GET | `/history` | 历史快照列表 | +| GET | `/snapshot/` | 快照详情 | +| POST | `/api/collect` | 接收原始输出 (JSON) | +| GET | `/api/latest` | 最新快照 JSON | +| GET | `/api/history` | 历史趋势 JSON | +| POST | `/api/cleanup?days=N` | 清理 N 天前数据 | diff --git a/palladium-monitor/app.py b/palladium-monitor/app.py new file mode 100644 index 0000000..56e76a5 --- /dev/null +++ b/palladium-monitor/app.py @@ -0,0 +1,633 @@ +#!/usr/bin/env python3 +""" +Palladium Z1 Monitor — LD逻辑板使用监控系统 + +解析 `test_server -short` 输出,展示 Palladium Z1 仿真器的 +逻辑板 (LD) 状态、逻辑板利用率、作业信息和 T-Pod 可用性。 +""" + +import os +import re +import json +import subprocess +from datetime import datetime, timezone, timedelta +from flask import Flask, render_template, request, jsonify, redirect, url_for +from flask_sqlalchemy import SQLAlchemy + +# ═══ Config ═══ +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INSTANCE_DIR = os.path.join(BASE_DIR, "instance") +os.makedirs(INSTANCE_DIR, exist_ok=True) + +app = Flask(__name__) +app.config["SECRET_KEY"] = os.environ.get("PALLADIUM_SECRET", "palladium-z1-monitor-2026") +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join(INSTANCE_DIR, "palladium.db") +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +db = SQLAlchemy(app) + +# ═══ Domain State Constants ═══ +DOMAIN_AVAILABLE = "available" +DOMAIN_DOWNLOADED = "downloaded" +DOMAIN_DISABLED = "disabled" +DOMAIN_NOT_EXIST = "not_exist" + +DOMAIN_COLORS = { + DOMAIN_AVAILABLE: "#28a745", + DOMAIN_DOWNLOADED: "#007bff", + DOMAIN_DISABLED: "#dc3545", + DOMAIN_NOT_EXIST: "#4a4a5a", +} + +DOMAIN_LABELS = { + DOMAIN_AVAILABLE: "可用", + DOMAIN_DOWNLOADED: "已加载", + DOMAIN_DISABLED: "已禁用", + DOMAIN_NOT_EXIST: "不存在", +} + +# ═══ Models ═══ +class ScanSnapshot(db.Model): + __tablename__ = "snapshots" + id = db.Column(db.Integer, primary_key=True) + timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), index=True) + emulator = db.Column(db.String(100), default="") + hardware = db.Column(db.String(100), default="") + configmgr = db.Column(db.String(200), default="") + system_status = db.Column(db.String(20), default="") + source = db.Column(db.String(50), default="manual") + raw_output = db.Column(db.Text) + total_boards = db.Column(db.Integer, default=0) + online_boards = db.Column(db.Integer, default=0) + offline_boards = db.Column(db.Integer, default=0) + used_boards = db.Column(db.Integer, default=0) + total_domains = db.Column(db.Integer, default=0) + available_domains = db.Column(db.Integer, default=0) + downloaded_domains = db.Column(db.Integer, default=0) + disabled_domains = db.Column(db.Integer, default=0) + not_exist_domains = db.Column(db.Integer, default=0) + active_jobs = db.Column(db.Integer, default=0) + utilization = db.Column(db.Float, default=0.0) + + boards = db.relationship("BoardStatus", backref="snapshot", cascade="all, delete-orphan", lazy=True) + jobs = db.relationship("JobInfo", backref="snapshot", cascade="all, delete-orphan", lazy=True) + pods = db.relationship("PodInfo", backref="snapshot", cascade="all, delete-orphan", lazy=True) + + +class BoardStatus(db.Model): + __tablename__ = "board_statuses" + id = db.Column(db.Integer, primary_key=True) + snapshot_id = db.Column(db.Integer, db.ForeignKey("snapshots.id"), nullable=False, index=True) + rack = db.Column(db.Integer) + cluster = db.Column(db.Integer) + ld_index = db.Column(db.Integer) + status = db.Column(db.String(20)) + ccd = db.Column(db.String(20)) + d0 = db.Column(db.String(20), default="-") + d1 = db.Column(db.String(20), default="-") + d2 = db.Column(db.String(20), default="-") + d3 = db.Column(db.String(20), default="-") + d4 = db.Column(db.String(20), default="-") + d5 = db.Column(db.String(20), default="-") + d6 = db.Column(db.String(20), default="-") + d7 = db.Column(db.String(20), default="-") + + +class JobInfo(db.Model): + __tablename__ = "job_infos" + id = db.Column(db.Integer, primary_key=True) + snapshot_id = db.Column(db.Integer, db.ForeignKey("snapshots.id"), nullable=False, index=True) + job_index = db.Column(db.Integer) + owner = db.Column(db.String(100), default="") + pid = db.Column(db.String(200), default="") + t_pod = db.Column(db.String(200), default="") + design = db.Column(db.String(200), default="") + elap_time = db.Column(db.String(20), default="") + reserved_key = db.Column(db.String(100), default="") + + +class PodInfo(db.Model): + __tablename__ = "pod_infos" + id = db.Column(db.Integer, primary_key=True) + snapshot_id = db.Column(db.Integer, db.ForeignKey("snapshots.id"), nullable=False, index=True) + rack = db.Column(db.Integer) + all_hdsb = db.Column(db.Text, default="") + all_t_pods = db.Column(db.Text, default="") + available_hdsb = db.Column(db.Text, default="") + available_t_pods = db.Column(db.Text, default="") + locked_hdsb = db.Column(db.Text, default="") + locked_t_pods = db.Column(db.Text, default="") + reserved_hdsb = db.Column(db.Text, default="") + reserved_t_pods = db.Column(db.Text, default="") + unavailable_hdsb = db.Column(db.Text, default="") + unavailable_t_pods = db.Column(db.Text, default="") + + +# ═══ Parser ═══ +def parse_test_server_output(raw_text): + result = {"system": {}, "racks": [], "jobs": [], "pods": []} + + # 系统信息 (Configmgr 值可能与 System Status 拼接) + emu_match = re.search(r"Emulator:\s*(\S+)", raw_text) + hw_match = re.search(r"Hardware:\s*(.+?)\s+Configmgr:", raw_text) + cfg_match = re.search(r"Configmgr:\s*(.+?)(?:System\s*Status|$)", raw_text) + status_match = re.search(r"System\s*Status:\s*(\w+)", raw_text) + result["system"] = { + "emulator": emu_match.group(1).strip() if emu_match else "", + "hardware": hw_match.group(1).strip() if hw_match else "", + "configmgr": cfg_match.group(1).strip() if cfg_match else "", + "system_status": status_match.group(1).strip() if status_match else "", + } + + lines = raw_text.split("\n") + current_rack = None + board_re = re.compile( + r"^\s*(\d+)\s+(ONLINE|OFFLINE)\s+" + r"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+" + r"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$" + ) + + for line in lines: + rack_match = re.match(r"Rack\s+(\d+)\s+has\s+(\d+)\s+clusters", line) + if rack_match: + current_rack = int(rack_match.group(1)) + result["racks"].append({"rack": current_rack, "clusters": []}) + continue + + cluster_match = re.match(r"Cluster\s+(\d+)\s+has\s+(\d+)\s+boards\s+CCD:\s*(\w+)", line) + if cluster_match and result["racks"]: + result["racks"][-1]["clusters"].append({ + "cluster": int(cluster_match.group(1)), + "ccd": cluster_match.group(3), + "boards": [], + }) + continue + + board_match = board_re.match(line) + if board_match and result["racks"] and result["racks"][-1]["clusters"]: + result["racks"][-1]["clusters"][-1]["boards"].append({ + "ld": int(board_match.group(1)), + "status": board_match.group(2), + "domains": [board_match.group(i) for i in range(3, 11)], + }) + continue + + # 作业信息 + job_section = re.search(r"Job Information:.*?(?=Target Pod Information:|Exit code:|$)", raw_text, re.DOTALL) + if job_section: + for jline in job_section.group(0).split("\n"): + jline = jline.strip() + if not jline or jline.startswith("Job Information") or jline.startswith("Index"): + continue + parts = jline.split() + if len(parts) >= 7: + result["jobs"].append({ + "index": int(parts[0]), + "owner": parts[1], + "pid": parts[2], + "t_pod": " ".join(parts[3:-3]), + "design": parts[-3], + "elap_time": parts[-2], + "reserved_key": parts[-1], + }) + + # T-Pod 信息 + pod_section = re.search(r"Target Pod Information:.*?(?=Exit code:|$)", raw_text, re.DOTALL) + if pod_section: + for m in re.finditer(r"Rack\s+(\d+):(.*?)(?=Rack\s+\d+:|Exit code:|$)", pod_section.group(0), re.DOTALL): + rack_num = int(m.group(1)) + rack_text = m.group(2) + all_hdsb = {} + all_t_pods = {} + fields = { + "available_hdsb": "", "available_t_pods": "", + "locked_hdsb": "", "locked_t_pods": "", + "reserved_hdsb": "", "reserved_t_pods": "", + "unavailable_hdsb": "", "unavailable_t_pods": "", + } + for rline in rack_text.split("\n"): + rline = rline.strip() + if not rline: + continue + all_h_match = re.match(r"All length (\d+) HDSB:\s*(.+)", rline) + if all_h_match: + all_hdsb[all_h_match.group(1)] = all_h_match.group(2).strip() + continue + all_t_match = re.match(r"All length (\d+) T-Pods:\s*(.+)", rline) + if all_t_match: + all_t_pods[all_t_match.group(1)] = all_t_match.group(2).strip() + continue + for field_key in fields: + label = field_key.replace("_", " ").title() + label = label.replace("Hdsb", "HDSB").replace("T Pods", "T-Pods") + if rline.startswith(label + ":"): + fields[field_key] = rline.split(":", 1)[1].strip() + break + result["pods"].append({ + "rack": rack_num, + "all_hdsb": json.dumps(all_hdsb), + "all_t_pods": json.dumps(all_t_pods), + **fields, + }) + + return result + + +def classify_domain(raw_value): + v = raw_value.strip() + if v == "-": + return DOMAIN_AVAILABLE + if v == "X": + return DOMAIN_NOT_EXIST + if v == "D": + return DOMAIN_DISABLED + if v.isdigit(): + return DOMAIN_DOWNLOADED + return DOMAIN_AVAILABLE + + +def calculate_stats(parsed): + total_boards = online = offline = used = 0 + total_dom = avail = dl = dis = ne = 0 + + for rack in parsed["racks"]: + for cluster in rack["clusters"]: + for board in cluster["boards"]: + total_boards += 1 + if board["status"] == "ONLINE": + online += 1 + else: + offline += 1 + board_used = False + for d in board["domains"]: + state = classify_domain(d) + total_dom += 1 + if state == DOMAIN_AVAILABLE: + avail += 1 + elif state == DOMAIN_DOWNLOADED: + dl += 1 + board_used = True + elif state == DOMAIN_DISABLED: + dis += 1 + elif state == DOMAIN_NOT_EXIST: + ne += 1 + if board_used: + used += 1 + + util = round(used / total_boards * 100, 1) if total_boards > 0 else 0.0 + return { + "total_boards": total_boards, + "online_boards": online, + "offline_boards": offline, + "used_boards": used, + "total_domains": total_dom, + "available_domains": avail, + "downloaded_domains": dl, + "disabled_domains": dis, + "not_exist_domains": ne, + "active_jobs": len(parsed["jobs"]), + "utilization": util, + } + + +# ═══ Template Filters ═══ +@app.template_filter("domain_state") +def domain_state_filter(raw_value): + return classify_domain(raw_value) + +@app.template_filter("domain_color") +def domain_color_filter(raw_value): + return DOMAIN_COLORS.get(classify_domain(raw_value), "#888") + +@app.template_filter("domain_label") +def domain_label_filter(raw_value): + return DOMAIN_LABELS.get(classify_domain(raw_value), "未知") + +@app.template_filter("fmt_time") +def fmt_time_filter(dt): + if not dt: + return "—" + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S") + +@app.template_filter("fmt_time_short") +def fmt_time_short_filter(dt): + if not dt: + return "—" + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone().strftime("%H:%M:%S") + +@app.template_filter("from_json") +def from_json_filter(s): + if not s: + return {} + try: + return json.loads(s) + except (ValueError, TypeError): + return {} + + +# ═══ Routes ═══ +@app.route("/") +def dashboard(): + snapshot = ScanSnapshot.query.order_by(ScanSnapshot.timestamp.desc()).first() + stats = None + racks_data = [] + jobs_data = [] + pods_data = [] + history = [] + + if snapshot: + stats = _snapshot_stats(snapshot) + racks_data = _build_racks(snapshot.id) + jobs_data = JobInfo.query.filter_by(snapshot_id=snapshot.id).all() + pods_data = PodInfo.query.filter_by(snapshot_id=snapshot.id).order_by(PodInfo.rack).all() + + history = ScanSnapshot.query.order_by(ScanSnapshot.timestamp.desc()).limit(50).all() + history = list(reversed(history)) + + return render_template("dashboard.html", snapshot=snapshot, stats=stats, + racks=racks_data, jobs=jobs_data, pods=pods_data, history=history) + + +@app.route("/history") +def history(): + page = request.args.get("page", 1, type=int) + pagination = ScanSnapshot.query.order_by(ScanSnapshot.timestamp.desc()).paginate( + page=page, per_page=30, error_out=False) + return render_template("history.html", pagination=pagination) + + +@app.route("/snapshot/") +def snapshot_detail(sid): + snapshot = db.get_or_404(ScanSnapshot, sid) + stats = _snapshot_stats(snapshot) + racks_data = _build_racks(sid) + jobs_data = JobInfo.query.filter_by(snapshot_id=sid).all() + pods_data = PodInfo.query.filter_by(snapshot_id=sid).order_by(PodInfo.rack).all() + return render_template("snapshot.html", snapshot=snapshot, stats=stats, + racks=racks_data, jobs=jobs_data, pods=pods_data) + + +@app.route("/paste", methods=["POST"]) +def paste_data(): + raw_output = request.form.get("raw_output", "").strip() + if not raw_output: + return jsonify({"error": "无输出内容"}), 400 + store_snapshot(raw_output, source="paste") + return redirect(url_for("dashboard")) + + +@app.route("/api/collect", methods=["POST"]) +def api_collect(): + data = request.get_json(silent=True) + if not data or "raw_output" not in data: + return jsonify({"error": "缺少 raw_output 字段"}), 400 + sid = store_snapshot(data["raw_output"], source=data.get("source", "api")) + snap = db.session.get(ScanSnapshot, sid) + return jsonify({"ok": True, "snapshot_id": sid, "stats": _snapshot_stats(snap)}) + + +@app.route("/api/latest") +def api_latest(): + snapshot = ScanSnapshot.query.order_by(ScanSnapshot.timestamp.desc()).first() + if not snapshot: + return jsonify({"error": "无数据"}), 404 + boards = BoardStatus.query.filter_by(snapshot_id=snapshot.id).all() + jobs = JobInfo.query.filter_by(snapshot_id=snapshot.id).all() + pods = PodInfo.query.filter_by(snapshot_id=snapshot.id).all() + return jsonify({ + "snapshot": {"id": snapshot.id, "timestamp": snapshot.timestamp.isoformat() if snapshot.timestamp else None, + "emulator": snapshot.emulator, "hardware": snapshot.hardware, + "configmgr": snapshot.configmgr, "system_status": snapshot.system_status, + "source": snapshot.source}, + "stats": _snapshot_stats(snapshot), + "boards": [{"rack": b.rack, "cluster": b.cluster, "ld": b.ld_index, + "status": b.status, "ccd": b.ccd, + "domains": [b.d0, b.d1, b.d2, b.d3, b.d4, b.d5, b.d6, b.d7]} for b in boards], + "jobs": [{"index": j.job_index, "owner": j.owner, "pid": j.pid, + "t_pod": j.t_pod, "design": j.design, + "elap_time": j.elap_time, "reserved_key": j.reserved_key} for j in jobs], + "pods": [{"rack": p.rack, + "all_hdsb": json.loads(p.all_hdsb) if p.all_hdsb else {}, + "all_t_pods": json.loads(p.all_t_pods) if p.all_t_pods else {}, + "available_hdsb": p.available_hdsb, "available_t_pods": p.available_t_pods, + "locked_hdsb": p.locked_hdsb, "locked_t_pods": p.locked_t_pods, + "reserved_hdsb": p.reserved_hdsb, "reserved_t_pods": p.reserved_t_pods, + "unavailable_hdsb": p.unavailable_hdsb, "unavailable_t_pods": p.unavailable_t_pods} for p in pods], + }) + + +@app.route("/api/history") +def api_history(): + snapshots = ScanSnapshot.query.order_by(ScanSnapshot.timestamp.desc()).limit(100).all() + snapshots = list(reversed(snapshots)) + return jsonify({"data": [{ + "timestamp": s.timestamp.isoformat() if s.timestamp else None, + "utilization": s.utilization, "online_boards": s.online_boards, + "offline_boards": s.offline_boards, "used_boards": s.used_boards, + "active_jobs": s.active_jobs, + } for s in snapshots]}) + + +@app.route("/api/snapshot//delete", methods=["POST"]) +def delete_snapshot(sid): + snapshot = db.get_or_404(ScanSnapshot, sid) + db.session.delete(snapshot) + db.session.commit() + return redirect(url_for("history")) + + +@app.route("/api/cleanup", methods=["POST"]) +def cleanup_old(): + days = request.args.get("days", 30, type=int) + cutoff = datetime.now(timezone.utc) - timedelta(days=days) + old = ScanSnapshot.query.filter(ScanSnapshot.timestamp < cutoff).all() + for s in old: + db.session.delete(s) + db.session.commit() + return jsonify({"ok": True, "deleted": len(old)}) + + +# ═══ Helpers ═══ +def _snapshot_stats(snapshot): + return { + "total_boards": snapshot.total_boards, + "online_boards": snapshot.online_boards, + "offline_boards": snapshot.offline_boards, + "used_boards": snapshot.used_boards, + "total_domains": snapshot.total_domains, + "available_domains": snapshot.available_domains, + "downloaded_domains": snapshot.downloaded_domains, + "disabled_domains": snapshot.disabled_domains, + "not_exist_domains": snapshot.not_exist_domains, + "active_jobs": snapshot.active_jobs, + "utilization": snapshot.utilization, + } + + +def _build_racks(snapshot_id): + boards = BoardStatus.query.filter_by(snapshot_id=snapshot_id).order_by( + BoardStatus.rack, BoardStatus.cluster, BoardStatus.ld_index).all() + rack_map = {} + for b in boards: + if b.rack not in rack_map: + rack_map[b.rack] = {} + if b.cluster not in rack_map[b.rack]: + rack_map[b.rack][b.cluster] = {"cluster": b.cluster, "ccd": b.ccd or "UNKNOWN", "boards": []} + rack_map[b.rack][b.cluster]["boards"].append({ + "ld": b.ld_index, "status": b.status, + "domains": [b.d0, b.d1, b.d2, b.d3, b.d4, b.d5, b.d6, b.d7], + }) + return [{"rack": rn, "clusters": [rack_map[rn][c] for c in sorted(rack_map[rn])]} + for rn in sorted(rack_map)] + + +def store_snapshot(raw_output, source="manual"): + parsed = parse_test_server_output(raw_output) + stats = calculate_stats(parsed) + sysinfo = parsed["system"] + + snap = ScanSnapshot( + emulator=sysinfo.get("emulator", ""), hardware=sysinfo.get("hardware", ""), + configmgr=sysinfo.get("configmgr", ""), system_status=sysinfo.get("system_status", ""), + source=source, raw_output=raw_output, + total_boards=stats["total_boards"], online_boards=stats["online_boards"], + offline_boards=stats["offline_boards"], used_boards=stats["used_boards"], + total_domains=stats["total_domains"], available_domains=stats["available_domains"], + downloaded_domains=stats["downloaded_domains"], disabled_domains=stats["disabled_domains"], + not_exist_domains=stats["not_exist_domains"], active_jobs=stats["active_jobs"], + utilization=stats["utilization"], + ) + db.session.add(snap) + db.session.flush() + + for rack in parsed["racks"]: + for cluster in rack["clusters"]: + for board in cluster["boards"]: + d = board["domains"] + db.session.add(BoardStatus( + snapshot_id=snap.id, rack=rack["rack"], cluster=cluster["cluster"], + ld_index=board["ld"], status=board["status"], ccd=cluster["ccd"], + d0=d[0], d1=d[1], d2=d[2], d3=d[3], d4=d[4], d5=d[5], d6=d[6], d7=d[7], + )) + + for job in parsed["jobs"]: + db.session.add(JobInfo( + snapshot_id=snap.id, job_index=job["index"], owner=job["owner"], + pid=job["pid"], t_pod=job["t_pod"], design=job["design"], + elap_time=job["elap_time"], reserved_key=job["reserved_key"], + )) + + for pod in parsed["pods"]: + db.session.add(PodInfo( + snapshot_id=snap.id, rack=pod["rack"], + all_hdsb=pod.get("all_hdsb", ""), all_t_pods=pod.get("all_t_pods", ""), + available_hdsb=pod.get("available_hdsb", ""), available_t_pods=pod.get("available_t_pods", ""), + locked_hdsb=pod.get("locked_hdsb", ""), locked_t_pods=pod.get("locked_t_pods", ""), + reserved_hdsb=pod.get("reserved_hdsb", ""), reserved_t_pods=pod.get("reserved_t_pods", ""), + unavailable_hdsb=pod.get("unavailable_hdsb", ""), unavailable_t_pods=pod.get("unavailable_t_pods", ""), + )) + + db.session.commit() + return snap.id + + +# ═══ Seed ═══ +SAMPLE_OUTPUT = r"""Emulator: sc01_emu Hardware: Palladium Z1 Configmgr: V21.02.102.s002System Status: PARTIAL +Rack 0 has 2 clusters +Cluster 0 has 6 boards CCD: ONLINE + LD Status D0 D1 D2 D3 D4 D5 D6 D7 + 0 ONLINE - - - - - - - - + 1 ONLINE - - - - - - - - + 2 ONLINE - - - - - - - - + 3 ONLINE - - - - - - - - + 4 ONLINE - - - - - - - - + 5 ONLINE - - - - - - - - + +Cluster 1 has 6 boards CCD: ONLINE + LD Status D0 D1 D2 D3 D4 D5 D6 D7 + 6 ONLINE 1 1 1 1 1 1 1 1 + 7 ONLINE 1 1 1 1 1 1 1 1 + 8 ONLINE - - - - - - - - + 9 OFFLINE X X X X X X X X + 10 ONLINE - - - - - - - - + 11 ONLINE - - - - - - - - + +Rack 1 has 2 clusters +Cluster 3 has 6 boards CCD: ONLINE + LD Status D0 D1 D2 D3 D4 D5 D6 D7 + 18 ONLINE - - - - - - - - + 19 ONLINE - - - - - - - - + 20 ONLINE - - - - - - - - + 21 ONLINE - - - - - - - - + 22 ONLINE - - - - - - - - + 23 ONLINE - - - - - - - - + +Cluster 4 has 6 boards CCD: ONLINE + LD Status D0 D1 D2 D3 D4 D5 D6 D7 + 24 ONLINE - - - - - - - - + 25 ONLINE - - - - - - - - + 26 ONLINE - - - - - - - - + 27 ONLINE - - - - - - - - + 28 ONLINE - - - - - - - - + 29 ONLINE - - - - - - - - + + +X: Domain does not exist +D: Domain is disabled +-: Domain is available +: Domain is downloaded or reserved + +Job Information: +Index Owner PID T-Pod Design ElapTime ReservedKey + 1 pzuser101 scmp03:106144 -- -- top 00:35:39 -- + +Target Pod Information: +Rack 0: +All length 5 HDSB: T4 +All length 5 T-Pods: T0, T1, T2, T3, T5, T6, T7, T8, T9 +Available HDSB: T4 +Available T-Pods: T0, T1, T2, T3, T5 +Locked HDSB: NONE +Locked T-Pods: NONE +Reserved HDSB: NONE +Reserved T-Pods: NONE +Unavailable HDSB: NONE +Unavailable T-Pods: T6, T7, T8, T9 + +Rack 1: +All length 5 HDSB: T4 +All length 30 HDSB: T5 +Available HDSB: T4, T5 +Available T-Pods: NONE +Locked HDSB: NONE +Locked T-Pods: NONE +Reserved HDSB: NONE +Reserved T-Pods: NONE +Unavailable HDSB: NONE +Unavailable T-Pods: NONE + +Exit code: 0 +Deinitializing IB - 0 - 0 +""" + + +def seed_sample_data(): + if ScanSnapshot.query.first(): + return + store_snapshot(SAMPLE_OUTPUT, source="sample") + print("[seed] 示例数据已写入") + + +if __name__ == "__main__": + with app.app_context(): + db.create_all() + seed_sample_data() + print("════════════════════════════════════════════════════════") + print(" Palladium Z1 Monitor") + print(" http://0.0.0.0:5100") + print("════════════════════════════════════════════════════════") + app.run(host="0.0.0.0", port=5100, debug=True) diff --git a/palladium-monitor/collect.sh b/palladium-monitor/collect.sh new file mode 100755 index 0000000..4d1a4ae --- /dev/null +++ b/palladium-monitor/collect.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════════ +# collect.sh — Palladium Z1 数据采集脚本 (Shell 版, 无需 Python) +# +# 在 Palladium 主机上运行, 执行 test_server -short 并推送到监控服务器 +# +# 用法: +# ./collect.sh # 使用默认配置 +# MONITOR_URL=http://10.0.0.1:5100/api/collect ./collect.sh +# TEST_SERVER_CMD="/edatools/vxe/VXE23.03.001/bin/test_server -short" ./collect.sh +# ./collect.sh --dry-run # 只采集不推送 +# +# Crontab (每30分钟): +# */30 * * * * /path/to/collect.sh >> /var/log/palladium-collect.log 2>&1 +# ═══════════════════════════════════════════════════════════════ + +# ─── 配置 (可通过环境变量覆盖) ─────────────────────────────── +TEST_SERVER_CMD="${TEST_SERVER_CMD:-/edatools/vxe/VXE23.03.001/bin/test_server -short}" +MONITOR_URL="${MONITOR_URL:-http://10.168.1.209:5100/api/collect}" +TIMEOUT_SEC="${TIMEOUT_SEC:-300}" +LOG_PREFIX="[Palladium-Collect]" + +# ─── 参数解析 ─────────────────────────────────────────────── +DRY_RUN=false +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --help|-h) + head -18 "$0" | tail -15 + exit 0 ;; + *) shift ;; + esac +done + +# ─── 日志函数 ─────────────────────────────────────────────── +log() { echo "$LOG_PREFIX $(date '+%Y-%m-%d %H:%M:%S') $*"; } + +# ─── 执行采集 ─────────────────────────────────────────────── +log "执行: $TEST_SERVER_CMD" +OUTPUT=$(timeout "$TIMEOUT_SEC" bash -c "$TEST_SERVER_CMD" 2>&1) +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + log "[!] 命令退出码: $EXIT_CODE" + # test_server 可能输出 exit code != 0 但仍有有效输出, 继续处理 +fi + +if [ -z "$OUTPUT" ]; then + log "[!] 命令无输出, 跳过" + exit 1 +fi + +OUTPUT_LEN=${#OUTPUT} +log "采集成功, 输出 ${OUTPUT_LEN} 字节" + +if [ "$DRY_RUN" = true ]; then + echo "──────────────────────────────────────────" + echo "$OUTPUT" + echo "──────────────────────────────────────────" + log "dry-run 模式, 不推送" + exit 0 +fi + +# ─── 推送到监控服务器 ─────────────────────────────────────── +log "推送到: $MONITOR_URL" + +# 用 python3 做 JSON 转义 (如果可用), 否则用 sed +if command -v python3 &>/dev/null; then + JSON_PAYLOAD=$(python3 -c " +import sys, json +print(json.dumps({'raw_output': sys.stdin.read(), 'source': 'cron'})) +" <<< "$OUTPUT") +else + # 简单转义 (无 python3 的后备方案) + ESCAPED=$(echo "$OUTPUT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + JSON_PAYLOAD="{\"raw_output\": \"$ESCAPED\", \"source\": \"cron\"}" +fi + +HTTP_CODE=$(curl -s -o /tmp/palladium_resp.json -w "%{http_code}" \ + -X POST "$MONITOR_URL" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" \ + --connect-timeout 10 \ + --max-time 30 2>/dev/null) + +if [ "$HTTP_CODE" = "200" ]; then + RESP=$(cat /tmp/palladium_resp.json 2>/dev/null) + log "推送成功 — $(echo "$RESP" | grep -o '"snapshot_id":[0-9]*' | head -1)" + # 打印简要统计 + if command -v python3 &>/dev/null; then + echo "$RESP" | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + s = d.get('stats', {}) + if s: + print(f' 板: {s[\"total_boards\"]} (在线 {s[\"online_boards\"]}, 离线 {s[\"offline_boards\"]})') + print(f' 逻辑板: 利用率 {s[\"utilization\"]}% (使用 {s[\"used_boards\"]}/{s[\"total_boards\"]})') + print(f' 作业: {s[\"active_jobs\"]}') +except: pass +" 2>/dev/null + fi +else + log "[!] 推送失败: HTTP $HTTP_CODE" + cat /tmp/palladium_resp.json 2>/dev/null + exit 1 +fi + +rm -f /tmp/palladium_resp.json 2>/dev/null diff --git a/palladium-monitor/requirements.txt b/palladium-monitor/requirements.txt new file mode 100644 index 0000000..c34608a --- /dev/null +++ b/palladium-monitor/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.3 +Flask-SQLAlchemy==3.1.1 diff --git a/palladium-monitor/static/style.css b/palladium-monitor/static/style.css new file mode 100644 index 0000000..fe6b499 --- /dev/null +++ b/palladium-monitor/static/style.css @@ -0,0 +1,79 @@ +:root{--bg-primary:#0d1117;--bg-secondary:#161b22;--bg-card:#1c2330;--bg-card-hover:#232b3a;--border:#30363d;--border-light:#3a4250;--text-primary:#e6edf3;--text-secondary:#8b949e;--text-muted:#6e7681;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--orange:#d29922;--blue:#58a6ff;--gray:#6e7681;--radius:8px;--shadow:0 2px 8px rgba(0,0,0,.3)} +*{margin:0;padding:0;box-sizing:border-box} +body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans SC","Microsoft YaHei",sans-serif;background:var(--bg-primary);color:var(--text-primary);font-size:14px;line-height:1.5} +.topnav{display:flex;align-items:center;justify-content:space-between;padding:0 24px;height:56px;background:var(--bg-secondary);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100} +.nav-brand{display:flex;align-items:center;gap:8px}.nav-logo{font-size:22px}.nav-title{font-size:17px;font-weight:700} +.nav-links{display:flex;align-items:center;gap:4px} +.nav-links a{padding:8px 16px;color:var(--text-secondary);text-decoration:none;border-radius:var(--radius);transition:all .15s;font-weight:500} +.nav-links a:hover{color:var(--text-primary);background:var(--bg-card)}.nav-links a.active{color:var(--accent);background:rgba(88,166,255,.1)} +.nav-btn{padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:var(--radius);cursor:pointer;font-weight:600;font-size:13px;transition:opacity .15s}.nav-btn:hover{opacity:.85} +.container{max-width:1600px;margin:0 auto;padding:20px 24px 60px} +.system-bar{display:flex;align-items:center;flex-wrap:wrap;gap:24px;padding:12px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:16px} +.sys-item{display:flex;flex-direction:column;gap:2px}.sys-label{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px} +.sys-value{font-size:14px;font-weight:600;font-family:"SF Mono","Fira Code",Consolas,monospace} +.refresh-controls{margin-left:auto;display:flex;align-items:center;gap:8px} +.refresh-controls label{display:flex;align-items:center;gap:4px;font-size:13px;color:var(--text-secondary);cursor:pointer} +.refresh-controls select{background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;padding:3px 8px;font-size:12px} +.btn-sm{padding:4px 8px;font-size:12px;background:var(--bg-card-hover);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;cursor:pointer;text-decoration:none;transition:all .15s}.btn-sm:hover{background:var(--border-light)}.btn-sm.btn-danger{color:var(--red);border-color:var(--red)}.btn-sm.btn-danger:hover{background:rgba(248,81,73,.15)} +.status-badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:700} +.status-ok{background:rgba(63,185,80,.15);color:var(--green)}.status-warning{background:rgba(210,153,34,.15);color:var(--orange)}.status-error{background:rgba(248,81,73,.15);color:var(--red)}.status-unknown{background:rgba(110,118,129,.15);color:var(--gray)} +.status-online{background:rgba(63,185,80,.15);color:var(--green)}.status-offline{background:rgba(248,81,73,.15);color:var(--red)} +.metrics-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-bottom:20px} +.metric-card{display:flex;align-items:center;gap:14px;padding:16px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);transition:border-color .15s}.metric-card:hover{border-color:var(--border-light)} +.metric-icon{font-size:28px}.metric-label{font-size:12px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px} +.metric-value{font-size:28px;font-weight:700;line-height:1.2}.metric-value.metric-warn{color:var(--orange)}.metric-sub{font-size:12px;color:var(--text-secondary);margin-top:2px} +.section-header{display:flex;align-items:center;justify-content:space-between;margin:24px 0 12px}.section-header h2{font-size:18px;font-weight:700} +.legend{display:flex;gap:16px;font-size:12px;color:var(--text-secondary)}.legend-item{display:flex;align-items:center;gap:4px} +.rack-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden} +.rack-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--bg-secondary);border-bottom:1px solid var(--border)} +.rack-title{font-size:15px;font-weight:700}.rack-count{font-size:12px;color:var(--text-muted)} +.cluster-section{padding:12px 16px;border-bottom:1px solid var(--border)}.cluster-section:last-child{border-bottom:none} +.cluster-header{display:flex;align-items:center;gap:12px;margin-bottom:10px;font-size:14px;font-weight:600;color:var(--text-secondary)} +.ccd-badge{padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600}.ccd-online{background:rgba(63,185,80,.15);color:var(--green)}.ccd-offline{background:rgba(248,81,73,.15);color:var(--red)} +.boards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:8px} +.board-card{background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;padding:8px 10px;transition:all .15s}.board-card:hover{border-color:var(--border-light);transform:translateY(-1px)} +.board-card.board-offline{border-color:rgba(248,81,73,.4);background:rgba(248,81,73,.05)} +.board-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px} +.board-ld{font-size:13px;font-weight:700}.board-status-badge{padding:1px 6px;border-radius:8px;font-size:10px;font-weight:600} +.domains-row{display:grid;grid-template-columns:repeat(8,1fr);gap:2px} +.domain-cell{text-align:center;font-size:10px;font-weight:700;padding:4px 0;border-radius:3px;min-width:18px;transition:transform .1s;cursor:default;font-family:"SF Mono","Fira Code",Consolas,monospace} +.domain-cell:hover{transform:scale(1.15);z-index:5;position:relative} +.domain-available{background:rgba(63,185,80,.15);color:var(--green);border:1px solid rgba(63,185,80,.3)} +.domain-downloaded{background:rgba(88,166,255,.2);color:var(--blue);border:1px solid rgba(88,166,255,.4)} +.domain-disabled{background:rgba(248,81,73,.15);color:var(--red);border:1px solid rgba(248,81,73,.3)} +.domain-not_exist{background:rgba(110,118,129,.1);color:var(--gray);border:1px solid rgba(110,118,129,.2)} +.card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:12px}.card.empty-state{padding:24px;text-align:center;color:var(--text-muted)} +.data-table{width:100%;border-collapse:collapse;font-size:13px}.data-table thead{background:var(--bg-secondary)} +.data-table th{padding:10px 14px;text-align:left;font-weight:600;color:var(--text-muted);text-transform:uppercase;font-size:11px;letter-spacing:.5px;border-bottom:1px solid var(--border);white-space:nowrap} +.data-table td{padding:8px 14px;border-bottom:1px solid rgba(48,54,61,.5);color:var(--text-primary)}.data-table tbody tr:hover{background:var(--bg-card-hover)}.data-table tbody tr:last-child td{border-bottom:none} +.text-mono{font-family:"SF Mono","Fira Code",Consolas,monospace;font-size:12px} +.text-green{color:var(--green)}.text-red{color:var(--red)}.text-blue{color:var(--blue)}.text-gray{color:var(--gray)} +.pods-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px;margin-bottom:12px} +.pod-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} +.pod-header{padding:8px 14px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-weight:700;font-size:14px} +.pod-table{width:100%;border-collapse:collapse;font-size:12px}.pod-table td{padding:5px 14px;border-bottom:1px solid rgba(48,54,61,.4)}.pod-table tr:last-child td{border-bottom:none} +.pod-label{color:var(--text-secondary);white-space:nowrap;width:40%}.pod-sublabel{color:var(--text-muted);font-size:11px;padding-left:24px!important}.pod-value{font-family:"SF Mono","Fira Code",Consolas,monospace} +.pod-row-available td{color:var(--green)}.pod-row-locked td{color:var(--orange)}.pod-row-reserved td{color:var(--blue)}.pod-row-unavailable td{color:var(--red)} +.util-bar{position:relative;width:120px;height:20px;background:var(--bg-primary);border-radius:10px;overflow:hidden;border:1px solid var(--border)} +.util-fill{height:100%;background:linear-gradient(90deg,var(--green),var(--blue));border-radius:10px;transition:width .3s} +.util-text{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;text-shadow:0 1px 2px rgba(0,0,0,.5)} +.pagination{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px} +.page-btn{padding:6px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-primary);text-decoration:none;font-size:13px;transition:all .15s}.page-btn:hover{border-color:var(--accent);color:var(--accent)} +.page-info{font-size:13px;color:var(--text-secondary)} +.tag{display:inline-block;padding:1px 8px;background:rgba(88,166,255,.1);color:var(--accent);border-radius:4px;font-size:11px;font-weight:600} +.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:200} +.modal{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;width:90%;max-width:800px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.5)} +.modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border)}.modal-header h2{font-size:16px;font-weight:700} +.modal-close{background:none;border:none;color:var(--text-secondary);font-size:18px;cursor:pointer;padding:4px 8px;border-radius:4px}.modal-close:hover{background:var(--bg-card-hover);color:var(--text-primary)} +.modal-body{padding:16px 20px;overflow-y:auto;flex:1}.modal-hint{margin-bottom:10px;color:var(--text-secondary);font-size:13px}.modal-hint code{background:var(--bg-primary);padding:1px 6px;border-radius:3px;font-size:12px;color:var(--accent)} +.paste-textarea{width:100%;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border);border-radius:var(--radius);padding:12px;font-family:"SF Mono","Fira Code",Consolas,monospace;font-size:12px;resize:vertical;line-height:1.6}.paste-textarea:focus{outline:none;border-color:var(--accent)} +.modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--border)} +.btn{padding:8px 16px;border:none;border-radius:var(--radius);cursor:pointer;font-weight:600;font-size:13px;text-decoration:none;transition:all .15s} +.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:#4493f8}.btn-secondary{background:var(--bg-card-hover);color:var(--text-primary);border:1px solid var(--border)}.btn-secondary:hover{border-color:var(--border-light)}.btn-lg{padding:12px 24px;font-size:15px} +.empty-state-full{text-align:center;padding:80px 20px}.empty-icon{font-size:64px;margin-bottom:16px}.empty-state-full h2{font-size:22px;margin-bottom:8px}.empty-state-full p{color:var(--text-secondary);margin-bottom:20px}.empty-actions{margin-bottom:16px}.empty-hint{color:var(--text-muted)!important;font-size:13px}.empty-hint code{background:var(--bg-card);padding:2px 8px;border-radius:4px;color:var(--accent)} +.snapshot-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.snapshot-meta{font-size:13px;color:var(--text-secondary);margin-top:4px} +.raw-output-section{margin-top:24px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px} +.raw-output-section summary{cursor:pointer;font-weight:600;color:var(--text-secondary);padding:4px 0} +.raw-output{margin-top:12px;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);padding:12px;font-family:"SF Mono","Fira Code",Consolas,monospace;font-size:11px;color:var(--text-secondary);overflow-x:auto;white-space:pre-wrap;max-height:500px;overflow-y:auto} +.card canvas{padding:16px} +@media(max-width:768px){.topnav{padding:0 12px}.container{padding:12px}.system-bar{gap:12px}.refresh-controls{margin-left:0}.boards-grid{grid-template-columns:1fr}.metrics-grid{grid-template-columns:1fr}} diff --git a/palladium-monitor/templates/base.html b/palladium-monitor/templates/base.html new file mode 100644 index 0000000..39ea9c5 --- /dev/null +++ b/palladium-monitor/templates/base.html @@ -0,0 +1,53 @@ + + + + + + {% block title %}Palladium Z1 Monitor{% endblock %} + + {% block head %}{% endblock %} + + + +
+ {% block content %}{% endblock %} +
+ + + {% block scripts %}{% endblock %} + + diff --git a/palladium-monitor/templates/dashboard.html b/palladium-monitor/templates/dashboard.html new file mode 100644 index 0000000..38ef8ef --- /dev/null +++ b/palladium-monitor/templates/dashboard.html @@ -0,0 +1,227 @@ +{% extends "base.html" %} +{% block title %}仪表板 — Palladium Z1 Monitor{% endblock %} +{% block head %} + +{% endblock %} + +{% block content %} +{% if snapshot %} + +
+
仿真器{{ snapshot.emulator }}
+
硬件{{ snapshot.hardware }}
+
ConfigMgr{{ snapshot.configmgr }}
+
+ 系统状态 + {% set ss = snapshot.system_status.upper() %} + {% if ss == 'OK' %}● {{ snapshot.system_status }} + {% elif 'PARTIAL' in ss %}● {{ snapshot.system_status }} + {% elif 'DOWN' in ss or 'ERROR' in ss %}● {{ snapshot.system_status }} + {% else %}● {{ snapshot.system_status }}{% endif %} +
+
最后更新{{ snapshot.timestamp | fmt_time }}
+
数据来源{{ snapshot.source }}
+
+ + + +
+
+ +
+
+
🖥️
+
+
逻辑板总数
+
{{ stats.total_boards }}
+
在线 {{ stats.online_boards }} · 离线 {{ stats.offline_boards }}
+
+
+
+
📊
+
+
逻辑板利用率
+
{{ stats.utilization }}%
+
使用 {{ stats.used_boards }} / 空闲 {{ stats.online_boards - stats.used_boards }}
+
+
+
+
🔲
+
+
逻辑板状态
+
{{ stats.total_boards }}
+
+ ● {{ stats.online_boards - stats.used_boards }} 空闲 + ● {{ stats.used_boards }} 使用 + ● {{ stats.offline_boards }} 离线 +
+
+
+
+
⚙️
+
+
活跃作业
+
{{ stats.active_jobs }}
+
{% if jobs %}{{ jobs[0].owner }} · {{ jobs[0].elap_time }}{% else %}无活跃作业{% endif %}
+
+
+
+ +
+

🏗️ 逻辑板状态

+
+ - 可用 + 1 已加载 + D 已禁用 + X 不存在 +
+
+ +{% for rack in racks %} +
+
Rack {{ rack.rack }}{{ rack.clusters | length }} clusters
+ {% for cluster in rack.clusters %} +
+
+ Cluster {{ cluster.cluster }} + {% set ccd = cluster.ccd.upper() %} + {% if ccd == 'ONLINE' %}CCD: {{ cluster.ccd }} + {% else %}CCD: {{ cluster.ccd }}{% endif %} +
+
+ {% for board in cluster.boards %} +
+
+ LD {{ board.ld }} + {{ board.status }} +
+
+ {% for d in board.domains %} +
{{ d }}
+ {% endfor %} +
+
+ {% endfor %} +
+
+ {% endfor %} +
+{% endfor %} + +

📋 作业信息

+{% if jobs %} +
+ + + + {% for job in jobs %} + + + + + + + + + + {% endfor %} + +
#用户PIDT-Pod设计名已运行预留Key
{{ job.job_index }}{{ job.owner }}{{ job.pid }}{{ job.t_pod }}{{ job.design }}{{ job.elap_time }}{{ job.reserved_key }}
+
+{% else %} +
当前无活跃作业
+{% endif %} + +

🎯 T-Pod 可用性

+
+ {% for pod in pods %} +
+
Rack {{ pod.rack }}
+ + {% set all_h = pod.all_hdsb | from_json %} + {% set all_t = pod.all_t_pods | from_json %} + {% for length, val in all_h.items() %} + + {% endfor %} + {% for length, val in all_t.items() %} + + {% endfor %} + + + + + + + + +
HDSB L{{ length }}{{ val }}
T-Pods L{{ length }}{{ val }}
可用 HDSB{{ pod.available_hdsb }}
可用 T-Pods{{ pod.available_t_pods }}
锁定 HDSB{{ pod.locked_hdsb }}
锁定 T-Pods{{ pod.locked_t_pods }}
预留 HDSB{{ pod.reserved_hdsb }}
预留 T-Pods{{ pod.reserved_t_pods }}
不可用 HDSB{{ pod.unavailable_hdsb }}
不可用 T-Pods{{ pod.unavailable_t_pods }}
+
+ {% endfor %} +
+ +{% if history | length > 1 %} +

📈 利用率趋势

+
+{% endif %} + +{% else %} +
+
📊
+

暂无监控数据

+

请通过以下方式导入数据:

+
+ +
+

或在 Palladium 主机配置定时采集脚本 collect.sh

+
+{% endif %} +{% endblock %} + +{% block scripts %} +{% if snapshot and history | length > 1 %} + +{% endif %} + +{% endblock %} diff --git a/palladium-monitor/templates/history.html b/palladium-monitor/templates/history.html new file mode 100644 index 0000000..3c2bba4 --- /dev/null +++ b/palladium-monitor/templates/history.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} +{% block title %}历史记录 — Palladium Z1 Monitor{% endblock %} +{% block content %} +
+

📅 历史快照

+ +
+{% if pagination.items %} +
+ + + + + + {% for snap in pagination.items %} + + + + + + + + + + + + + {% endfor %} + +
ID时间仿真器系统状态逻辑板逻辑板利用率使用/总数作业数来源操作
{{ snap.id }}{{ snap.timestamp | fmt_time }}{{ snap.emulator }} + {% set ss = snap.system_status.upper() %} + {% if ss == 'OK' %}{{ snap.system_status }} + {% elif 'PARTIAL' in ss %}{{ snap.system_status }} + {% elif 'DOWN' in ss or 'ERROR' in ss %}{{ snap.system_status }} + {% else %}{{ snap.system_status }}{% endif %} + {{ snap.total_boards }} ({{ snap.online_boards }}/{{ snap.offline_boards }}) +
+
+ {{ snap.utilization }}% +
+
{{ snap.used_boards }} / {{ snap.total_boards }}{{ snap.active_jobs }}{{ snap.source }} + 查看 +
+ +
+
+
+ +{% else %} +
暂无历史数据
+{% endif %} + +{% endblock %} diff --git a/palladium-monitor/templates/snapshot.html b/palladium-monitor/templates/snapshot.html new file mode 100644 index 0000000..779aacf --- /dev/null +++ b/palladium-monitor/templates/snapshot.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% block title %}快照 #{{ snapshot.id }} — Palladium Z1 Monitor{% endblock %} +{% block content %} +
+
+

📸 快照 #{{ snapshot.id }}

+
{{ snapshot.timestamp | fmt_time }} · 来源: {{ snapshot.source }} · {{ snapshot.emulator }} / {{ snapshot.hardware }}
+
+ ← 返回仪表板 +
+
+
仿真器{{ snapshot.emulator }}
+
硬件{{ snapshot.hardware }}
+
ConfigMgr{{ snapshot.configmgr }}
+
+ 系统状态 + {% set ss = snapshot.system_status.upper() %} + {% if ss == 'OK' %}{{ snapshot.system_status }} + {% elif 'PARTIAL' in ss %}{{ snapshot.system_status }} + {% elif 'DOWN' in ss or 'ERROR' in ss %}{{ snapshot.system_status }} + {% else %}{{ snapshot.system_status }}{% endif %} +
+
+
+
🖥️
逻辑板
{{ stats.total_boards }}
在线 {{ stats.online_boards }} · 离线 {{ stats.offline_boards }}
+
📊
逻辑板利用率
{{ stats.utilization }}%
使用 {{ stats.used_boards }} / 空闲 {{ stats.online_boards - stats.used_boards }}
+
⚙️
活跃作业
{{ stats.active_jobs }}
+
+
+

🏗️ 逻辑板状态

+
+ - 可用 + 1 已加载 + D 已禁用 + X 不存在 +
+
+{% for rack in racks %} +
+
Rack {{ rack.rack }}
+ {% for cluster in rack.clusters %} +
+
+ Cluster {{ cluster.cluster }} + {% set ccd = cluster.ccd.upper() %} + {% if ccd == 'ONLINE' %}CCD: {{ cluster.ccd }} + {% else %}CCD: {{ cluster.ccd }}{% endif %} +
+
+ {% for board in cluster.boards %} +
+
LD {{ board.ld }}{{ board.status }}
+
+ {% for d in board.domains %} +
{{ d }}
+ {% endfor %} +
+
+ {% endfor %} +
+
+ {% endfor %} +
+{% endfor %} +

📋 作业信息

+{% if jobs %} +
+ + + + {% for job in jobs %} + + {% endfor %} + +
#用户PIDT-Pod设计名已运行预留Key
{{ job.job_index }}{{ job.owner }}{{ job.pid }}{{ job.t_pod }}{{ job.design }}{{ job.elap_time }}{{ job.reserved_key }}
+
+{% else %} +
该快照无活跃作业
+{% endif %} +

🎯 T-Pod 可用性

+
+ {% for pod in pods %} +
+
Rack {{ pod.rack }}
+ + {% set all_h = pod.all_hdsb | from_json %} + {% set all_t = pod.all_t_pods | from_json %} + {% for length, val in all_h.items() %}{% endfor %} + {% for length, val in all_t.items() %}{% endfor %} + + + + + + + + +
HDSB L{{ length }}{{ val }}
T-Pods L{{ length }}{{ val }}
可用 HDSB{{ pod.available_hdsb }}
可用 T-Pods{{ pod.available_t_pods }}
锁定 HDSB{{ pod.locked_hdsb }}
锁定 T-Pods{{ pod.locked_t_pods }}
预留 HDSB{{ pod.reserved_hdsb }}
预留 T-Pods{{ pod.reserved_t_pods }}
不可用 HDSB{{ pod.unavailable_hdsb }}
不可用 T-Pods{{ pod.unavailable_t_pods }}
+
+ {% endfor %} +
+
+ 📄 原始 test_server 输出 +
{{ snapshot.raw_output }}
+
+{% endblock %}