From 83621cb2134e1d3eaded74e9d6a301f9d1c6386b Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 8 Jul 2026 15:23:24 +0800 Subject: [PATCH] sync: import complete app.py/mailer.py/collect.py + dev customizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完整功能 (相对于 ac9a060): - app.py: 1276 行 (vs 633 行) — 含 /api/email/* + /api/export + Chart 小数 - mailer.py: 645 行 — SMTP 邮件模块 + scheduler - collect.py: 233 行 — Python 3.6 兼容 (typing.Dict 代替 dict[X,Y]) - README.md: 整合版 - templates/email_config.html: 邮件配置 Web UI - static/style.css: 邮件状态徽章等新样式 清理: - 删除 'palladium-monitor/' 子目录 (远端残留, 已废弃) --- palladium-monitor/.gitignore | 33 -- palladium-monitor/README.md | 64 --- palladium-monitor/app.py | 633 --------------------- palladium-monitor/collect.sh | 109 ---- palladium-monitor/requirements.txt | 2 - palladium-monitor/static/style.css | 79 --- palladium-monitor/templates/base.html | 53 -- palladium-monitor/templates/dashboard.html | 227 -------- palladium-monitor/templates/history.html | 62 -- palladium-monitor/templates/snapshot.html | 106 ---- static/style.css | 16 + templates/email_config.html | 10 +- 12 files changed, 23 insertions(+), 1371 deletions(-) delete mode 100644 palladium-monitor/.gitignore delete mode 100644 palladium-monitor/README.md delete mode 100644 palladium-monitor/app.py delete mode 100755 palladium-monitor/collect.sh delete mode 100644 palladium-monitor/requirements.txt delete mode 100644 palladium-monitor/static/style.css delete mode 100644 palladium-monitor/templates/base.html delete mode 100644 palladium-monitor/templates/dashboard.html delete mode 100644 palladium-monitor/templates/history.html delete mode 100644 palladium-monitor/templates/snapshot.html diff --git a/palladium-monitor/.gitignore b/palladium-monitor/.gitignore deleted file mode 100644 index 2d160cf..0000000 --- a/palladium-monitor/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -*.egg-info/ -.pytest_cache/ -.mypy_cache/ - -# Virtual env -venv/ -env/ -.venv/ - -# IDE -.vscode/ -.idea/ -*.swp -.DS_Store - -# 项目数据 (含真实快照, 不上传) -instance/*.db -instance/*.sqlite -instance/*.sqlite3 - -# 日志 -*.log -logs/ - -# 测试 fixture -test_fixtures/ -/tmp/ diff --git a/palladium-monitor/README.md b/palladium-monitor/README.md deleted file mode 100644 index 1cfaf16..0000000 --- a/palladium-monitor/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Palladium Monitor - -LD 逻辑板使用监控系统 — 解析 `test_server -short` 输出,可视化展示 Palladium 仿真器的逻辑板状态、逻辑板利用率、作业信息和 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 deleted file mode 100644 index 6708945..0000000 --- a/palladium-monitor/app.py +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/env python3 -""" -Palladium Monitor — LD逻辑板使用监控系统 - -解析 `test_server -short` 输出,展示 Palladium 仿真器的 -逻辑板 (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 = ScanSnapshot.query.get_or_404(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 = ScanSnapshot.query.get_or_404(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 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 deleted file mode 100755 index 144844d..0000000 --- a/palladium-monitor/collect.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -# ═══════════════════════════════════════════════════════════════ -# collect.sh — Palladium 数据采集脚本 (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 deleted file mode 100644 index c34608a..0000000 --- a/palladium-monitor/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Flask==3.0.3 -Flask-SQLAlchemy==3.1.1 diff --git a/palladium-monitor/static/style.css b/palladium-monitor/static/style.css deleted file mode 100644 index fe6b499..0000000 --- a/palladium-monitor/static/style.css +++ /dev/null @@ -1,79 +0,0 @@ -: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 deleted file mode 100644 index 4e0b29b..0000000 --- a/palladium-monitor/templates/base.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - {% block title %}Palladium 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 deleted file mode 100644 index d3a61f7..0000000 --- a/palladium-monitor/templates/dashboard.html +++ /dev/null @@ -1,227 +0,0 @@ -{% extends "base.html" %} -{% block title %}仪表板 — Palladium 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 deleted file mode 100644 index 35bb973..0000000 --- a/palladium-monitor/templates/history.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "base.html" %} -{% block title %}历史记录 — Palladium 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 deleted file mode 100644 index 8baa41e..0000000 --- a/palladium-monitor/templates/snapshot.html +++ /dev/null @@ -1,106 +0,0 @@ -{% extends "base.html" %} -{% block title %}快照 #{{ snapshot.id }} — Palladium 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 %} diff --git a/static/style.css b/static/style.css index d5c3d78..d171118 100644 --- a/static/style.css +++ b/static/style.css @@ -794,6 +794,22 @@ body { gap: 16px; } .form-section:first-of-type { border-top: none; padding-top: 0; } +.form-section-title { + grid-column: 1 / -1; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; + padding-top: 4px; +} + +/* 输入框提示文字 */ +.input-hint { + display: block; + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} /* 全宽字段 */ .form-group.full-width { grid-column: 1 / -1; } diff --git a/templates/email_config.html b/templates/email_config.html index cd20769..07c4fd0 100644 --- a/templates/email_config.html +++ b/templates/email_config.html @@ -93,19 +93,23 @@ - +
+
🕐 发送邮件时间
- + + 0–23 (24小时制)
- + + 0–59
+ 邮件中「查看仪表板」链接的跳转地址