sync: import complete app.py/mailer.py/collect.py + dev customizations
完整功能 (相对于 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/' 子目录 (远端残留, 已废弃)
This commit is contained in:
@@ -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/
|
||||
@@ -1,64 +0,0 @@
|
||||
# Palladium Monitor
|
||||
|
||||
LD 逻辑板使用监控系统 — 解析 `test_server -short` 输出,可视化展示 Palladium 仿真器的逻辑板状态、逻辑板利用率、作业信息和 T-Pod 可用性。
|
||||
|
||||
## 功能
|
||||
|
||||
- **逻辑板状态可视化** — Rack / Cluster / Board 三级结构,每块 LD 板的 8 个逻辑板 (D0-D7) 以色块展示
|
||||
- 🟢 可用 (`-`)
|
||||
- 🔵 已加载/预留 (`<num>`)
|
||||
- 🔴 已禁用 (`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/<id>` | 快照详情 |
|
||||
| POST | `/api/collect` | 接收原始输出 (JSON) |
|
||||
| GET | `/api/latest` | 最新快照 JSON |
|
||||
| GET | `/api/history` | 历史趋势 JSON |
|
||||
| POST | `/api/cleanup?days=N` | 清理 N 天前数据 |
|
||||
@@ -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/<int:sid>")
|
||||
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/<int:sid>/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
|
||||
<num>: 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)
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
Flask==3.0.3
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
@@ -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}}
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Palladium Monitor{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="topnav">
|
||||
<div class="nav-brand">
|
||||
<span class="nav-logo">⚡</span>
|
||||
<span class="nav-title">Palladium Monitor</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="{{ url_for('dashboard') }}" class="{% if request.endpoint == 'dashboard' %}active{% endif %}">仪表板</a>
|
||||
<a href="{{ url_for('history') }}" class="{% if request.endpoint == 'history' %}active{% endif %}">历史记录</a>
|
||||
<button class="nav-btn" onclick="openPasteModal()">📥 粘贴数据</button>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<div id="pasteModal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2>📥 粘贴 test_server -short 输出</h2>
|
||||
<button class="modal-close" onclick="closePasteModal()">✕</button>
|
||||
</div>
|
||||
<form action="{{ url_for('paste_data') }}" method="POST">
|
||||
<div class="modal-body">
|
||||
<p class="modal-hint">在 Palladium 主机执行 <code>test_server -short</code>,将输出粘贴到下方:</p>
|
||||
<textarea name="raw_output" rows="20" class="paste-textarea"
|
||||
placeholder="Emulator: sc01_emu Hardware: Palladium ..."></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closePasteModal()">取消</button>
|
||||
<button type="submit" class="btn btn-primary">解析并存储</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function openPasteModal() { document.getElementById('pasteModal').style.display = 'flex'; }
|
||||
function closePasteModal() { document.getElementById('pasteModal').style.display = 'none'; }
|
||||
document.getElementById('pasteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closePasteModal();
|
||||
});
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,227 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}仪表板 — Palladium Monitor{% endblock %}
|
||||
{% block head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if snapshot %}
|
||||
|
||||
<div class="system-bar">
|
||||
<div class="sys-item"><span class="sys-label">仿真器</span><span class="sys-value">{{ snapshot.emulator }}</span></div>
|
||||
<div class="sys-item"><span class="sys-label">硬件</span><span class="sys-value">{{ snapshot.hardware }}</span></div>
|
||||
<div class="sys-item"><span class="sys-label">ConfigMgr</span><span class="sys-value">{{ snapshot.configmgr }}</span></div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">系统状态</span>
|
||||
{% set ss = snapshot.system_status.upper() %}
|
||||
{% if ss == 'OK' %}<span class="status-badge status-ok">● {{ snapshot.system_status }}</span>
|
||||
{% elif 'PARTIAL' in ss %}<span class="status-badge status-warning">● {{ snapshot.system_status }}</span>
|
||||
{% elif 'DOWN' in ss or 'ERROR' in ss %}<span class="status-badge status-error">● {{ snapshot.system_status }}</span>
|
||||
{% else %}<span class="status-badge status-unknown">● {{ snapshot.system_status }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="sys-item"><span class="sys-label">最后更新</span><span class="sys-value">{{ snapshot.timestamp | fmt_time }}</span></div>
|
||||
<div class="sys-item"><span class="sys-label">数据来源</span><span class="sys-value">{{ snapshot.source }}</span></div>
|
||||
<div class="sys-item refresh-controls">
|
||||
<label><input type="checkbox" id="autoRefresh" checked> 自动刷新</label>
|
||||
<select id="refreshInterval">
|
||||
<option value="15">15s</option>
|
||||
<option value="30" selected>30s</option>
|
||||
<option value="60">1m</option>
|
||||
<option value="300">5m</option>
|
||||
</select>
|
||||
<button class="btn-sm" onclick="location.reload()">🔄</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">🖥️</div>
|
||||
<div class="metric-body">
|
||||
<div class="metric-label">逻辑板总数</div>
|
||||
<div class="metric-value">{{ stats.total_boards }}</div>
|
||||
<div class="metric-sub"><span class="text-green">在线 {{ stats.online_boards }}</span> · <span class="text-red">离线 {{ stats.offline_boards }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">📊</div>
|
||||
<div class="metric-body">
|
||||
<div class="metric-label">逻辑板利用率</div>
|
||||
<div class="metric-value {{ 'metric-warn' if stats.utilization > 80 else '' }}">{{ stats.utilization }}%</div>
|
||||
<div class="metric-sub">使用 {{ stats.used_boards }} / 空闲 {{ stats.online_boards - stats.used_boards }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">🔲</div>
|
||||
<div class="metric-body">
|
||||
<div class="metric-label">逻辑板状态</div>
|
||||
<div class="metric-value">{{ stats.total_boards }}</div>
|
||||
<div class="metric-sub">
|
||||
<span class="text-green">● {{ stats.online_boards - stats.used_boards }} 空闲</span>
|
||||
<span class="text-blue">● {{ stats.used_boards }} 使用</span>
|
||||
<span class="text-red">● {{ stats.offline_boards }} 离线</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon">⚙️</div>
|
||||
<div class="metric-body">
|
||||
<div class="metric-label">活跃作业</div>
|
||||
<div class="metric-value">{{ stats.active_jobs }}</div>
|
||||
<div class="metric-sub">{% if jobs %}{{ jobs[0].owner }} · {{ jobs[0].elap_time }}{% else %}无活跃作业{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<h2>🏗️ 逻辑板状态</h2>
|
||||
<div class="legend">
|
||||
<span class="legend-item"><span class="domain-cell domain-available">-</span> 可用</span>
|
||||
<span class="legend-item"><span class="domain-cell domain-downloaded">1</span> 已加载</span>
|
||||
<span class="legend-item"><span class="domain-cell domain-disabled">D</span> 已禁用</span>
|
||||
<span class="legend-item"><span class="domain-cell domain-not_exist">X</span> 不存在</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for rack in racks %}
|
||||
<div class="rack-card">
|
||||
<div class="rack-header"><span class="rack-title">Rack {{ rack.rack }}</span><span class="rack-count">{{ rack.clusters | length }} clusters</span></div>
|
||||
{% for cluster in rack.clusters %}
|
||||
<div class="cluster-section">
|
||||
<div class="cluster-header">
|
||||
<span>Cluster {{ cluster.cluster }}</span>
|
||||
{% set ccd = cluster.ccd.upper() %}
|
||||
{% if ccd == 'ONLINE' %}<span class="ccd-badge ccd-online">CCD: {{ cluster.ccd }}</span>
|
||||
{% else %}<span class="ccd-badge ccd-offline">CCD: {{ cluster.ccd }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="boards-grid">
|
||||
{% for board in cluster.boards %}
|
||||
<div class="board-card board-{{ board.status | lower }}">
|
||||
<div class="board-top">
|
||||
<span class="board-ld">LD {{ board.ld }}</span>
|
||||
<span class="board-status-badge status-{{ board.status | lower }}">{{ board.status }}</span>
|
||||
</div>
|
||||
<div class="domains-row">
|
||||
{% for d in board.domains %}
|
||||
<div class="domain-cell domain-{{ d | domain_state }}" title="D{{ loop.index0 }}: {{ d | domain_label }}">{{ d }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="section-header"><h2>📋 作业信息</h2></div>
|
||||
{% if jobs %}
|
||||
<div class="card">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>#</th><th>用户</th><th>PID</th><th>T-Pod</th><th>设计名</th><th>已运行</th><th>预留Key</th></tr></thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
<td>{{ job.job_index }}</td>
|
||||
<td class="text-mono">{{ job.owner }}</td>
|
||||
<td class="text-mono">{{ job.pid }}</td>
|
||||
<td class="text-mono">{{ job.t_pod }}</td>
|
||||
<td>{{ job.design }}</td>
|
||||
<td class="text-mono">{{ job.elap_time }}</td>
|
||||
<td>{{ job.reserved_key }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card empty-state">当前无活跃作业</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-header"><h2>🎯 T-Pod 可用性</h2></div>
|
||||
<div class="pods-grid">
|
||||
{% for pod in pods %}
|
||||
<div class="pod-card">
|
||||
<div class="pod-header">Rack {{ pod.rack }}</div>
|
||||
<table class="pod-table">
|
||||
{% set all_h = pod.all_hdsb | from_json %}
|
||||
{% set all_t = pod.all_t_pods | from_json %}
|
||||
{% for length, val in all_h.items() %}
|
||||
<tr><td class="pod-sublabel">HDSB L{{ length }}</td><td class="pod-value">{{ val }}</td></tr>
|
||||
{% endfor %}
|
||||
{% for length, val in all_t.items() %}
|
||||
<tr><td class="pod-sublabel">T-Pods L{{ length }}</td><td class="pod-value">{{ val }}</td></tr>
|
||||
{% endfor %}
|
||||
<tr class="pod-row-available"><td class="pod-label">可用 HDSB</td><td class="pod-value">{{ pod.available_hdsb }}</td></tr>
|
||||
<tr class="pod-row-available"><td class="pod-label">可用 T-Pods</td><td class="pod-value">{{ pod.available_t_pods }}</td></tr>
|
||||
<tr class="pod-row-locked"><td class="pod-label">锁定 HDSB</td><td class="pod-value">{{ pod.locked_hdsb }}</td></tr>
|
||||
<tr class="pod-row-locked"><td class="pod-label">锁定 T-Pods</td><td class="pod-value">{{ pod.locked_t_pods }}</td></tr>
|
||||
<tr class="pod-row-reserved"><td class="pod-label">预留 HDSB</td><td class="pod-value">{{ pod.reserved_hdsb }}</td></tr>
|
||||
<tr class="pod-row-reserved"><td class="pod-label">预留 T-Pods</td><td class="pod-value">{{ pod.reserved_t_pods }}</td></tr>
|
||||
<tr class="pod-row-unavailable"><td class="pod-label">不可用 HDSB</td><td class="pod-value">{{ pod.unavailable_hdsb }}</td></tr>
|
||||
<tr class="pod-row-unavailable"><td class="pod-label">不可用 T-Pods</td><td class="pod-value">{{ pod.unavailable_t_pods }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if history | length > 1 %}
|
||||
<div class="section-header"><h2>📈 利用率趋势</h2></div>
|
||||
<div class="card"><canvas id="utilChart" height="80"></canvas></div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state-full">
|
||||
<div class="empty-icon">📊</div>
|
||||
<h2>暂无监控数据</h2>
|
||||
<p>请通过以下方式导入数据:</p>
|
||||
<div class="empty-actions">
|
||||
<button class="btn btn-primary btn-lg" onclick="openPasteModal()">📥 粘贴 test_server -short 输出</button>
|
||||
</div>
|
||||
<p class="empty-hint">或在 Palladium 主机配置定时采集脚本 <code>collect.sh</code></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if snapshot and history | length > 1 %}
|
||||
<script>
|
||||
const ctx = document.getElementById('utilChart').getContext('2d');
|
||||
const labels = [{% for h in history %}'{{ h.timestamp | fmt_time_short }}'{% if not loop.last %}, {% endif %}{% endfor %}];
|
||||
const utilData = [{% for h in history %}{{ h.utilization }}{% if not loop.last %}, {% endif %}{% endfor %}];
|
||||
const usedData = [{% for h in history %}{{ h.used_boards }}{% if not loop.last %}, {% endif %}{% endfor %}];
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{ label: '利用率 (%)', data: utilData, borderColor: '#007bff', backgroundColor: 'rgba(0,123,255,0.1)', yAxisID: 'y', tension: 0.3, fill: true },
|
||||
{ label: '使用逻辑板', data: usedData, borderColor: '#28a745', backgroundColor: 'rgba(40,167,69,0.1)', yAxisID: 'y1', tension: 0.3, fill: false }
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
scales: {
|
||||
x: { ticks: { color: '#9ba1b3', maxRotation: 45 } },
|
||||
y: { type: 'linear', position: 'left', title: { display: true, text: '利用率 %', color: '#007bff' }, min: 0, max: 100, ticks: { color: '#007bff' } },
|
||||
y1: { type: 'linear', position: 'right', title: { display: true, text: '使用逻辑板', color: '#28a745' }, ticks: { color: '#28a745' }, grid: { drawOnChartArea: false } }
|
||||
},
|
||||
plugins: { legend: { labels: { color: '#e4e6eb' } } }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
let refreshTimer = null;
|
||||
function setupRefresh() {
|
||||
if (refreshTimer) clearTimeout(refreshTimer);
|
||||
const cb = document.getElementById('autoRefresh');
|
||||
const sel = document.getElementById('refreshInterval');
|
||||
if (!cb || !sel) return;
|
||||
if (cb.checked) { refreshTimer = setTimeout(function(){ location.reload(); }, parseInt(sel.value) * 1000); }
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', setupRefresh);
|
||||
document.getElementById('autoRefresh')?.addEventListener('change', setupRefresh);
|
||||
document.getElementById('refreshInterval')?.addEventListener('change', setupRefresh);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,62 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}历史记录 — Palladium Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section-header">
|
||||
<h2>📅 历史快照</h2>
|
||||
<button class="btn btn-sm btn-secondary" onclick="cleanupOld()">🧹 清理30天前数据</button>
|
||||
</div>
|
||||
{% if pagination.items %}
|
||||
<div class="card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>时间</th><th>仿真器</th><th>系统状态</th><th>逻辑板</th><th>逻辑板利用率</th><th>使用/总数</th><th>作业数</th><th>来源</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snap in pagination.items %}
|
||||
<tr>
|
||||
<td>{{ snap.id }}</td>
|
||||
<td class="text-mono">{{ snap.timestamp | fmt_time }}</td>
|
||||
<td>{{ snap.emulator }}</td>
|
||||
<td>
|
||||
{% set ss = snap.system_status.upper() %}
|
||||
{% if ss == 'OK' %}<span class="status-badge status-ok">{{ snap.system_status }}</span>
|
||||
{% elif 'PARTIAL' in ss %}<span class="status-badge status-warning">{{ snap.system_status }}</span>
|
||||
{% elif 'DOWN' in ss or 'ERROR' in ss %}<span class="status-badge status-error">{{ snap.system_status }}</span>
|
||||
{% else %}<span class="status-badge status-unknown">{{ snap.system_status }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ snap.total_boards }} <span class="text-green">({{ snap.online_boards }}</span>/<span class="text-red">{{ snap.offline_boards }})</span></td>
|
||||
<td>
|
||||
<div class="util-bar">
|
||||
<div class="util-fill" style="width: {{ snap.utilization }}%"></div>
|
||||
<span class="util-text">{{ snap.utilization }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-mono">{{ snap.used_boards }} / {{ snap.total_boards }}</td>
|
||||
<td>{{ snap.active_jobs }}</td>
|
||||
<td><span class="tag">{{ snap.source }}</span></td>
|
||||
<td>
|
||||
<a href="{{ url_for('snapshot_detail', sid=snap.id) }}" class="btn-sm">查看</a>
|
||||
<form method="POST" action="{{ url_for('delete_snapshot', sid=snap.id) }}" style="display:inline;" onsubmit="return confirm('确认删除快照 #{{ snap.id }}?')">
|
||||
<button type="submit" class="btn-sm btn-danger">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
{% if pagination.has_prev %}<a href="{{ url_for('history', page=pagination.prev_num) }}" class="page-btn">← 上一页</a>{% endif %}
|
||||
<span class="page-info">第 {{ pagination.page }} / {{ pagination.pages }} 页 (共 {{ pagination.total }} 条)</span>
|
||||
{% if pagination.has_next %}<a href="{{ url_for('history', page=pagination.next_num) }}" class="page-btn">下一页 →</a>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card empty-state">暂无历史数据</div>
|
||||
{% endif %}
|
||||
<script>
|
||||
function cleanupOld() {
|
||||
if (!confirm('确认清理30天前的历史数据?')) return;
|
||||
fetch('/api/cleanup?days=30', { method: 'POST' }).then(r => r.json()).then(data => { alert('已清理 ' + data.deleted + ' 条快照'); location.reload(); }).catch(e => alert('清理失败: ' + e));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,106 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}快照 #{{ snapshot.id }} — Palladium Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="snapshot-header">
|
||||
<div>
|
||||
<h2>📸 快照 #{{ snapshot.id }}</h2>
|
||||
<div class="snapshot-meta">{{ snapshot.timestamp | fmt_time }} · 来源: {{ snapshot.source }} · {{ snapshot.emulator }} / {{ snapshot.hardware }}</div>
|
||||
</div>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">← 返回仪表板</a>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="sys-item"><span class="sys-label">仿真器</span><span class="sys-value">{{ snapshot.emulator }}</span></div>
|
||||
<div class="sys-item"><span class="sys-label">硬件</span><span class="sys-value">{{ snapshot.hardware }}</span></div>
|
||||
<div class="sys-item"><span class="sys-label">ConfigMgr</span><span class="sys-value">{{ snapshot.configmgr }}</span></div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">系统状态</span>
|
||||
{% set ss = snapshot.system_status.upper() %}
|
||||
{% if ss == 'OK' %}<span class="status-badge status-ok">{{ snapshot.system_status }}</span>
|
||||
{% elif 'PARTIAL' in ss %}<span class="status-badge status-warning">{{ snapshot.system_status }}</span>
|
||||
{% elif 'DOWN' in ss or 'ERROR' in ss %}<span class="status-badge status-error">{{ snapshot.system_status }}</span>
|
||||
{% else %}<span class="status-badge status-unknown">{{ snapshot.system_status }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card"><div class="metric-icon">🖥️</div><div class="metric-body"><div class="metric-label">逻辑板</div><div class="metric-value">{{ stats.total_boards }}</div><div class="metric-sub"><span class="text-green">在线 {{ stats.online_boards }}</span> · <span class="text-red">离线 {{ stats.offline_boards }}</span></div></div></div>
|
||||
<div class="metric-card"><div class="metric-icon">📊</div><div class="metric-body"><div class="metric-label">逻辑板利用率</div><div class="metric-value">{{ stats.utilization }}%</div><div class="metric-sub">使用 {{ stats.used_boards }} / 空闲 {{ stats.online_boards - stats.used_boards }}</div></div></div>
|
||||
<div class="metric-card"><div class="metric-icon">⚙️</div><div class="metric-body"><div class="metric-label">活跃作业</div><div class="metric-value">{{ stats.active_jobs }}</div></div></div>
|
||||
</div>
|
||||
<div class="section-header">
|
||||
<h2>🏗️ 逻辑板状态</h2>
|
||||
<div class="legend">
|
||||
<span class="legend-item"><span class="domain-cell domain-available">-</span> 可用</span>
|
||||
<span class="legend-item"><span class="domain-cell domain-downloaded">1</span> 已加载</span>
|
||||
<span class="legend-item"><span class="domain-cell domain-disabled">D</span> 已禁用</span>
|
||||
<span class="legend-item"><span class="domain-cell domain-not_exist">X</span> 不存在</span>
|
||||
</div>
|
||||
</div>
|
||||
{% for rack in racks %}
|
||||
<div class="rack-card">
|
||||
<div class="rack-header"><span class="rack-title">Rack {{ rack.rack }}</span></div>
|
||||
{% for cluster in rack.clusters %}
|
||||
<div class="cluster-section">
|
||||
<div class="cluster-header">
|
||||
<span>Cluster {{ cluster.cluster }}</span>
|
||||
{% set ccd = cluster.ccd.upper() %}
|
||||
{% if ccd == 'ONLINE' %}<span class="ccd-badge ccd-online">CCD: {{ cluster.ccd }}</span>
|
||||
{% else %}<span class="ccd-badge ccd-offline">CCD: {{ cluster.ccd }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="boards-grid">
|
||||
{% for board in cluster.boards %}
|
||||
<div class="board-card board-{{ board.status | lower }}">
|
||||
<div class="board-top"><span class="board-ld">LD {{ board.ld }}</span><span class="board-status-badge status-{{ board.status | lower }}">{{ board.status }}</span></div>
|
||||
<div class="domains-row">
|
||||
{% for d in board.domains %}
|
||||
<div class="domain-cell domain-{{ d | domain_state }}" title="D{{ loop.index0 }}: {{ d | domain_label }}">{{ d }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="section-header"><h2>📋 作业信息</h2></div>
|
||||
{% if jobs %}
|
||||
<div class="card">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>#</th><th>用户</th><th>PID</th><th>T-Pod</th><th>设计名</th><th>已运行</th><th>预留Key</th></tr></thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr><td>{{ job.job_index }}</td><td class="text-mono">{{ job.owner }}</td><td class="text-mono">{{ job.pid }}</td><td class="text-mono">{{ job.t_pod }}</td><td>{{ job.design }}</td><td class="text-mono">{{ job.elap_time }}</td><td>{{ job.reserved_key }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card empty-state">该快照无活跃作业</div>
|
||||
{% endif %}
|
||||
<div class="section-header"><h2>🎯 T-Pod 可用性</h2></div>
|
||||
<div class="pods-grid">
|
||||
{% for pod in pods %}
|
||||
<div class="pod-card">
|
||||
<div class="pod-header">Rack {{ pod.rack }}</div>
|
||||
<table class="pod-table">
|
||||
{% set all_h = pod.all_hdsb | from_json %}
|
||||
{% set all_t = pod.all_t_pods | from_json %}
|
||||
{% for length, val in all_h.items() %}<tr><td class="pod-sublabel">HDSB L{{ length }}</td><td class="pod-value">{{ val }}</td></tr>{% endfor %}
|
||||
{% for length, val in all_t.items() %}<tr><td class="pod-sublabel">T-Pods L{{ length }}</td><td class="pod-value">{{ val }}</td></tr>{% endfor %}
|
||||
<tr class="pod-row-available"><td class="pod-label">可用 HDSB</td><td class="pod-value">{{ pod.available_hdsb }}</td></tr>
|
||||
<tr class="pod-row-available"><td class="pod-label">可用 T-Pods</td><td class="pod-value">{{ pod.available_t_pods }}</td></tr>
|
||||
<tr class="pod-row-locked"><td class="pod-label">锁定 HDSB</td><td class="pod-value">{{ pod.locked_hdsb }}</td></tr>
|
||||
<tr class="pod-row-locked"><td class="pod-label">锁定 T-Pods</td><td class="pod-value">{{ pod.locked_t_pods }}</td></tr>
|
||||
<tr class="pod-row-reserved"><td class="pod-label">预留 HDSB</td><td class="pod-value">{{ pod.reserved_hdsb }}</td></tr>
|
||||
<tr class="pod-row-reserved"><td class="pod-label">预留 T-Pods</td><td class="pod-value">{{ pod.reserved_t_pods }}</td></tr>
|
||||
<tr class="pod-row-unavailable"><td class="pod-label">不可用 HDSB</td><td class="pod-value">{{ pod.unavailable_hdsb }}</td></tr>
|
||||
<tr class="pod-row-unavailable"><td class="pod-label">不可用 T-Pods</td><td class="pod-value">{{ pod.unavailable_t_pods }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<details class="raw-output-section">
|
||||
<summary>📄 原始 test_server 输出</summary>
|
||||
<pre class="raw-output">{{ snapshot.raw_output }}</pre>
|
||||
</details>
|
||||
{% endblock %}
|
||||
@@ -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; }
|
||||
|
||||
@@ -93,19 +93,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送时间 -->
|
||||
<!-- 发送邮件时间 -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">🕐 发送邮件时间</div>
|
||||
<div class="form-group form-group-small">
|
||||
<label for="cfg_send_hour">发送时间 (小时)</label>
|
||||
<input type="number" id="cfg_send_hour" placeholder="9" min="0" max="23">
|
||||
<input type="number" id="cfg_send_hour" placeholder="9" min="0" max="23" step="1">
|
||||
<span class="input-hint">0–23 (24小时制)</span>
|
||||
</div>
|
||||
<div class="form-group form-group-small">
|
||||
<label for="cfg_send_minute">发送时间 (分钟)</label>
|
||||
<input type="number" id="cfg_send_minute" placeholder="0" min="0" max="59">
|
||||
<input type="number" id="cfg_send_minute" placeholder="0" min="0" max="59" step="1">
|
||||
<span class="input-hint">0–59</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cfg_monitor_base_url">监控服务器地址</label>
|
||||
<input type="url" id="cfg_monitor_base_url" placeholder="http://127.0.0.1:5100">
|
||||
<span class="input-hint">邮件中「查看仪表板」链接的跳转地址</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user