提交 f3893286 authored 作者: 陈泽健's avatar 陈泽健

feat(service-monitor): 左侧菜单布局改造与报告列表独立页

- 新增 base.html 共享基础模板:左侧固定侧边栏 + 顶部条 + 内容区
- 侧边栏 4 个菜单项:监测目标/巡检报告/定时任务/目标管理
- 移动端 768px 以下汉堡菜单滑出
- index.html 继承 base.html,只保留目标卡片
- 新增 reports.html 巡检报告独立页,支持按目标筛选
- targets.html / run.html / report.html 继承 base.html
- routes.py 新增 page_reports 路由,所有路由增加 active_menu 参数
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 8a63c076
...@@ -22,7 +22,7 @@ from flask import ( ...@@ -22,7 +22,7 @@ from flask import (
Response, redirect, url_for, stream_with_context, Response, redirect, url_for, stream_with_context,
) )
from .services import target_service, report_service, runner_service from .services import target_service, report_service, runner_service, schedule_service
logger = logging.getLogger("service_monitor.routes") logger = logging.getLogger("service_monitor.routes")
...@@ -61,16 +61,16 @@ def _require_admin_json(): ...@@ -61,16 +61,16 @@ def _require_admin_json():
@bp.route('/service-monitor') @bp.route('/service-monitor')
def page_index(): def page_index():
"""监测主页:目标概览 + 最近报告。""" """监测主页:目标概览。"""
if 'user' not in session: if 'user' not in session:
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
user = _current_user() user = _current_user()
targets = target_service.list_targets(role=user.get('role', '')) targets = target_service.list_targets(role=user.get('role', ''))
reports = report_service.list_reports(limit=20)
return render_template( return render_template(
'service_monitor/index.html', 'service_monitor/index.html',
user=user, targets=targets, reports=reports, user=user, targets=targets,
is_admin=(user.get('role') == 'admin'), is_admin=(user.get('role') == 'admin'),
active_menu='targets',
) )
...@@ -83,7 +83,43 @@ def page_targets(): ...@@ -83,7 +83,43 @@ def page_targets():
if user.get('role') != 'admin': if user.get('role') != 'admin':
return redirect(url_for('service-monitor.page_index')) return redirect(url_for('service-monitor.page_index'))
targets = target_service.list_targets(role='admin') targets = target_service.list_targets(role='admin')
return render_template('service_monitor/targets.html', user=user, targets=targets) return render_template(
'service_monitor/targets.html', user=user, targets=targets,
is_admin=True, active_menu='manage',
)
@bp.route('/service-monitor/reports')
def page_reports():
"""巡检报告列表页(所有登录用户可看)。"""
if 'user' not in session:
return redirect(url_for('auth.login'))
user = _current_user()
target_id = request.args.get('target_id')
reports = report_service.list_reports(target_id=target_id, limit=50)
targets = target_service.list_targets(role=user.get('role', ''))
return render_template(
'service_monitor/reports.html',
user=user, reports=reports, targets=targets,
is_admin=(user.get('role') == 'admin'),
active_menu='reports',
)
@bp.route('/service-monitor/schedule')
def page_schedule():
"""定时任务管理页(所有登录用户可看,操作仅管理员)。"""
if 'user' not in session:
return redirect(url_for('auth.login'))
user = _current_user()
schedules = schedule_service.list_schedules()
targets = target_service.list_targets(role=user.get('role', ''))
return render_template(
'service_monitor/schedule.html',
user=user, schedules=schedules, targets=targets,
is_admin=(user.get('role') == 'admin'),
active_menu='schedule',
)
@bp.route('/service-monitor/run/<target_id>') @bp.route('/service-monitor/run/<target_id>')
...@@ -99,7 +135,8 @@ def page_run(target_id): ...@@ -99,7 +135,8 @@ def page_run(target_id):
return redirect(url_for('service-monitor.page_index')) return redirect(url_for('service-monitor.page_index'))
suite = request.args.get('suite', 'quick') suite = request.args.get('suite', 'quick')
return render_template('service_monitor/run.html', return render_template('service_monitor/run.html',
user=user, target=target, suite=suite) user=user, target=target, suite=suite,
is_admin=True, active_menu='targets')
@bp.route('/service-monitor/report/<report_id>') @bp.route('/service-monitor/report/<report_id>')
...@@ -111,7 +148,11 @@ def page_report(report_id): ...@@ -111,7 +148,11 @@ def page_report(report_id):
report = report_service.get_report(report_id) report = report_service.get_report(report_id)
if not report: if not report:
return redirect(url_for('service-monitor.page_index')) return redirect(url_for('service-monitor.page_index'))
return render_template('service_monitor/report.html', user=user, report=report) return render_template(
'service_monitor/report.html', user=user, report=report,
is_admin=(user.get('role') == 'admin'),
active_menu='reports',
)
# ============================================================ # ============================================================
...@@ -290,3 +331,72 @@ def api_fix(): ...@@ -290,3 +331,72 @@ def api_fix():
if guard: if guard:
return guard return guard
return jsonify({"success": False, "message": "修复能力开发中,敬请期待"}) return jsonify({"success": False, "message": "修复能力开发中,敬请期待"})
# ============================================================
# API:定时任务(管理员操作,登录可查看)
# ============================================================
@bp.route('/api/service-monitor/schedules', methods=['GET'])
def api_list_schedules():
guard = _require_login_json()
if guard:
return guard
schedules = schedule_service.list_schedules()
return jsonify({"success": True, "schedules": schedules})
@bp.route('/api/service-monitor/schedules', methods=['POST'])
def api_create_schedule():
guard = _require_admin_json()
if guard:
return guard
try:
sched = schedule_service.create_schedule(
request.get_json(force=True) or {},
created_by=_current_user().get('username', 'admin'),
)
# 通知调度器重新加载
schedule_service.reload_job(sched['id'])
return jsonify({"success": True, "schedule": sched})
except ValueError as e:
return jsonify({"success": False, "error": {"code": 400, "message": str(e)}}), 400
@bp.route('/api/service-monitor/schedules/<schedule_id>', methods=['PUT'])
def api_update_schedule(schedule_id):
guard = _require_admin_json()
if guard:
return guard
try:
sched = schedule_service.update_schedule(
schedule_id, request.get_json(force=True) or {}
)
schedule_service.reload_job(schedule_id)
return jsonify({"success": True, "schedule": sched})
except ValueError as e:
return jsonify({"success": False, "error": {"code": 400, "message": str(e)}}), 400
@bp.route('/api/service-monitor/schedules/<schedule_id>', methods=['DELETE'])
def api_delete_schedule(schedule_id):
guard = _require_admin_json()
if guard:
return guard
ok = schedule_service.delete_schedule(schedule_id)
if ok:
schedule_service.reload_job(schedule_id) # 会移除 job
return jsonify({"success": ok})
@bp.route('/api/service-monitor/schedules/<schedule_id>/toggle', methods=['POST'])
def api_toggle_schedule(schedule_id):
guard = _require_admin_json()
if guard:
return guard
try:
sched = schedule_service.toggle_enabled(schedule_id)
schedule_service.reload_job(schedule_id)
return jsonify({"success": True, "schedule": sched})
except ValueError as e:
return jsonify({"success": False, "error": {"code": 400, "message": str(e)}}), 400
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5">
<title>{% block title %}服务监测{% endblock %} - 运行维护平台</title>
<style>
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--green: #16a34a;
--yellow: #d97706;
--red: #dc2626;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-700: #374151;
--gray-900: #111827;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Microsoft YaHei", sans-serif;
background: #f3f4f6;
min-height: 100vh;
}
/* 侧边栏 */
.sidebar {
position: fixed;
left: 0;
top: 0;
width: 200px;
height: 100vh;
background: linear-gradient(180deg, #1e293b 0%, #334155 100%);
z-index: 100;
transition: transform 0.3s ease;
}
.sidebar-brand {
color: #fff;
font-size: 18px;
font-weight: 700;
padding: 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-nav {
padding: 16px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
color: #94a3b8;
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
}
.nav-item:hover {
background: rgba(255,255,255,0.05);
color: #fff;
}
.nav-item.active {
background: var(--primary);
color: #fff;
}
.nav-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
/* 顶部条 */
.topbar {
position: fixed;
left: 200px;
top: 0;
right: 0;
height: 56px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 99;
}
.topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.hamburger {
display: none;
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.35);
color: #fff;
font-size: 18px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
}
.topbar-home {
background: rgba(255,255,255,0.2);
color: #fff;
border: 1px solid rgba(255,255,255,0.35);
padding: 6px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
}
.topbar-home:hover {
background: rgba(255,255,255,0.3);
}
.topbar-user {
color: #fff;
display: flex;
align-items: center;
gap: 14px;
font-size: 14px;
}
.btn-logout {
background: rgba(255,255,255,0.15);
color: #fff;
border: 1px solid rgba(255,255,255,0.3);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.btn-logout:hover {
background: rgba(255,255,255,0.25);
}
/* 主内容区 */
.main-content {
margin-left: 200px;
padding: 84px 24px 28px;
min-height: 100vh;
}
/* 通用组件样式 */
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: var(--gray-900);
display: flex;
align-items: center;
gap: 8px;
}
.btn-add {
background: var(--primary);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
}
.btn-add:hover {
background: var(--primary-hover);
}
.empty {
text-align: center;
padding: 40px;
color: var(--gray-500);
}
.footer {
text-align: center;
padding: 20px;
color: var(--gray-500);
font-size: 12px;
}
/* 移动端响应式 */
@media screen and (max-width: 768px) {
.sidebar {
transform: translateX(-200px);
}
.sidebar.open {
transform: translateX(0);
}
.topbar {
left: 0;
}
.main-content {
margin-left: 0;
padding: 84px 14px 28px;
}
.hamburger {
display: block;
}
.topbar-home {
display: none;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 侧边栏 -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">🛠️ 运维</div>
<nav class="sidebar-nav">
<a href="/service-monitor" class="nav-item {{ 'active' if active_menu == 'targets' }}">
<span class="nav-icon">📊</span>
<span>监测目标</span>
</a>
<a href="/service-monitor/reports" class="nav-item {{ 'active' if active_menu == 'reports' }}">
<span class="nav-icon">📋</span>
<span>巡检报告</span>
</a>
<a href="/service-monitor/schedule" class="nav-item {{ 'active' if active_menu == 'schedule' }}">
<span class="nav-icon"></span>
<span>定时任务</span>
</a>
{% if is_admin %}
<a href="/service-monitor/targets" class="nav-item {{ 'active' if active_menu == 'manage' }}">
<span class="nav-icon">⚙️</span>
<span>目标管理</span>
</a>
{% endif %}
</nav>
</aside>
<!-- 顶部条 -->
<header class="topbar">
<div class="topbar-left">
<button class="hamburger" id="hamburger" onclick="toggleSidebar()"></button>
<a href="/" class="topbar-home">🏠 返回首页</a>
</div>
<div class="topbar-user">
<span>{{ user.username }}({{ '管理员' if is_admin else '用户' }})</span>
<button class="btn-logout" onclick="handleLogout()">🚪 退出</button>
</div>
</header>
<!-- 主内容区 -->
<main class="main-content">
{% block content %}{% endblock %}
</main>
{% if not no_footer %}
<div class="footer">运行维护平台 · 服务监测模块</div>
{% endif %}
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
}
// 点击遮罩关闭侧边栏
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('sidebar');
const hamburger = document.getElementById('hamburger');
if (sidebar.classList.contains('open') &&
!sidebar.contains(e.target) &&
!hamburger.contains(e.target)) {
sidebar.classList.remove('open');
}
});
async function handleLogout() {
try {
await fetch('/logout', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include'
});
} catch (e) {}
window.location.href = '/login';
}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
\ No newline at end of file
{% extends "service_monitor/base.html" %}
{% block title %}巡检报告{% endblock %}
{% block extra_css %}
<style>
/* 筛选栏 */
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.filter-bar select {
padding: 8px 12px;
border: 1px solid var(--gray-200);
border-radius: 8px;
font-size: 14px;
color: var(--gray-700);
background: #fff;
}
.filter-bar select:focus {
outline: none;
border-color: var(--primary);
}
.report-count {
font-size: 13px;
color: var(--gray-500);
margin-left: auto;
}
/* 报告列表 */
.report-list {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
border: 1px solid var(--gray-200);
}
.report-row {
display: grid;
grid-template-columns: 2fr 1fr 2fr 2fr 1fr;
gap: 12px;
align-items: center;
padding: 14px 18px;
border-bottom: 1px solid var(--gray-100);
text-decoration: none;
}
.report-row:last-child { border-bottom: none; }
.report-row:hover { background: var(--gray-50); }
.report-target { font-weight: 600; color: var(--gray-900); }
.report-suite { font-size: 13px; color: var(--gray-500); }
.report-summary { display: flex; gap: 10px; font-size: 13px; }
.sm-ok { color: var(--green); } .sm-warn { color: var(--yellow); } .sm-crit { color: var(--red); }
.report-time { font-size: 13px; color: var(--gray-500); }
.btn-view {
background: var(--gray-100);
color: var(--gray-700);
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
text-align: center;
}
.btn-view:hover { background: var(--gray-200); }
@media screen and (max-width: 768px) {
.report-row { grid-template-columns: 1fr 1fr; gap: 6px; }
.report-summary { font-size: 12px; }
.filter-bar { flex-wrap: wrap; }
}
</style>
{% endblock %}
{% block content %}
<div class="section-head">
<div class="section-title">📋 巡检报告</div>
</div>
<div class="filter-bar">
<select id="filter-target" onchange="filterByTarget()">
<option value="">全部目标</option>
{% for t in targets %}
<option value="{{ t.id }}" {{ 'selected' if request.args.get('target_id') == t.id }}>{{ t.name }}</option>
{% endfor %}
</select>
<span class="report-count">共 {{ reports | length }} 条报告</span>
</div>
{% if reports %}
<div class="report-list">
{% for r in reports %}
<a href="/service-monitor/report/{{ r.id }}" class="report-row">
<span class="report-target">{{ r.target_name }}</span>
<span class="report-suite">{{ '快速' if r.suite == 'quick' else '全量' }}</span>
<span class="report-summary">
<span class="sm-ok">✓{{ r.summary.正常 if r.summary else 0 }}</span>
<span class="sm-warn">⚠{{ r.summary.警告 if r.summary else 0 }}</span>
<span class="sm-crit">✗{{ r.summary.严重 if r.summary else 0 }}</span>
</span>
<span class="report-time">{{ (r.started_at or '')[5:16].replace('T', ' ') }}</span>
<span class="btn-view">查看</span>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty">📭 暂无巡检报告,点击目标卡片开始巡检</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
function filterByTarget() {
const targetId = document.getElementById('filter-target').value;
const url = targetId
? '/service-monitor/reports?target_id=' + encodeURIComponent(targetId)
: '/service-monitor/reports';
window.location.href = url;
}
</script>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html> {% extends "service_monitor/base.html" %}
<html lang="zh-CN"> {% block title %}巡检执行{% endblock %}
<head> {% set no_footer = true %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5"> {% block extra_css %}
<title>巡检执行 - 服务监测</title> <style>
<style> .run-card { background: #fff; border-radius: 14px; box-shadow: 0 2px 8px rgba(0,0,0,.06); border: 1px solid var(--gray-200); padding: 26px; max-width: 720px; }
:root { .run-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
--primary:#2563eb; --primary-hover:#1d4ed8; .run-title { font-size: 18px; font-weight: 700; color: var(--gray-900); }
--green:#16a34a; --yellow:#d97706; --red:#dc2626; .run-title .suite-tag { font-size: 13px; font-weight: 600; padding: 3px 10px; border-radius: 6px; margin-left: 8px; background: #eef2ff; color: var(--primary); }
--gray-50:#f9fafb; --gray-100:#f3f4f6; --gray-200:#e5e7eb; .btn-cancel { background: #fee2e2; color: var(--red); border: none; padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
--gray-400:#9ca3af; --gray-500:#6b7280; --gray-700:#374151; --gray-900:#111827; .btn-cancel:disabled { opacity: .4; cursor: not-allowed; }
}
* { box-sizing:border-box; margin:0; padding:0; }
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Microsoft YaHei",sans-serif; background:#f3f4f6; min-height:100vh; }
.navbar { background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); padding:14px 24px; display:flex; align-items:center; justify-content:space-between; }
.navbar-left { display:flex; align-items:center; gap:14px; }
.navbar-brand { color:#fff; font-size:18px; font-weight:700; text-decoration:none; }
.navbar-module { color:rgba(255,255,255,.7); font-size:14px; }
.navbar-home { background:rgba(255,255,255,.2); color:#fff; border:1px solid rgba(255,255,255,.35); padding:6px 14px; border-radius:8px; font-size:13px; font-weight:600; text-decoration:none; }
.navbar-user { color:#fff; font-size:14px; }
.container { max-width:720px; margin:0 auto; padding:28px 24px; }
.run-card { background:#fff; border-radius:14px; box-shadow:0 2px 8px rgba(0,0,0,.06); border:1px solid var(--gray-200); padding:26px; }
.run-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; }
.run-title { font-size:18px; font-weight:700; color:var(--gray-900); }
.run-title .suite-tag { font-size:13px; font-weight:600; padding:3px 10px; border-radius:6px; margin-left:8px; background:#eef2ff; color:var(--primary); }
.btn-cancel { background:#fee2e2; color:var(--red); border:none; padding:8px 16px; border-radius:8px; font-size:14px; font-weight:600; cursor:pointer; }
.btn-cancel:disabled { opacity:.4; cursor:not-allowed; }
/* 进度条 */ /* 进度条 */
.progress-wrap { margin-bottom:6px; } .progress-wrap { margin-bottom: 6px; }
.progress-bar { height:12px; background:var(--gray-100); border-radius:6px; overflow:hidden; } .progress-bar { height: 12px; background: var(--gray-100); border-radius: 6px; overflow: hidden; }
.progress-fill { height:100%; background:linear-gradient(90deg,var(--primary),#60a5fa); width:0%; transition:width .3s; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--primary), #60a5fa); width: 0%; transition: width .3s; }
.progress-text { display:flex; justify-content:space-between; font-size:13px; color:var(--gray-500); margin-top:8px; } .progress-text { display: flex; justify-content: space-between; font-size: 13px; color: var(--gray-500); margin-top: 8px; }
/* 模块清单 */ /* 模块清单 */
.module-list { margin-top:20px; } .module-list { margin-top: 20px; }
.module-item { display:flex; align-items:center; gap:12px; padding:12px 0; border-bottom:1px solid var(--gray-100); } .module-item { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--gray-100); }
.module-item:last-child { border-bottom:none; } .module-item:last-child { border-bottom: none; }
.module-status { font-size:18px; width:24px; text-align:center; } .module-status { font-size: 18px; width: 24px; text-align: center; }
.module-name { flex:1; font-size:15px; color:var(--gray-700); } .module-name { flex: 1; font-size: 15px; color: var(--gray-700); }
.module-info { font-size:13px; color:var(--gray-500); } .module-info { font-size: 13px; color: var(--gray-500); }
.st-pending .module-name { color:var(--gray-400); } .st-pending .module-name { color: var(--gray-400); }
.st-running .module-name { color:var(--primary); font-weight:600; } .st-running .module-name { color: var(--primary); font-weight: 600; }
.st-done .module-name { color:var(--gray-900); } .st-done .module-name { color: var(--gray-900); }
.sm-ok { color:var(--green); } .sm-warn { color:var(--yellow); } .sm-crit { color:var(--red); } .sm-ok { color: var(--green); } .sm-warn { color: var(--yellow); } .sm-crit { color: var(--red); }
.spinner { display:inline-block; width:16px; height:16px; border:2px solid var(--gray-200); border-top-color:var(--primary); border-radius:50%; animation:spin .8s linear infinite; } .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--gray-200); border-top-color: var(--primary); border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
.done-banner { display:none; text-align:center; padding:20px; margin-top:10px; border-radius:10px; } .done-banner { display: none; text-align: center; padding: 20px; margin-top: 10px; border-radius: 10px; }
.done-banner.show { display:block; } .done-banner.show { display: block; }
.done-ok { background:#dcfce7; color:#15803d; } .done-ok { background: #dcfce7; color: #15803d; }
.done-err { background:#fee2e2; color:var(--red); } .done-err { background: #fee2e2; color: var(--red); }
.btn-report { display:inline-block; margin-top:12px; background:var(--primary); color:#fff; padding:9px 22px; border-radius:8px; font-size:14px; font-weight:600; text-decoration:none; } .btn-report { display: inline-block; margin-top: 12px; background: var(--primary); color: #fff; padding: 9px 22px; border-radius: 8px; font-size: 14px; font-weight: 600; text-decoration: none; }
</style>
@media screen and (max-width:768px) { {% endblock %}
.container { padding:20px 14px; }
.navbar-module { display:none; } {% block content %}
} <div class="run-card">
</style>
</head>
<body>
<div class="navbar">
<div class="navbar-left">
<a href="/" class="navbar-brand">🛠️ 运行维护平台</a>
<span class="navbar-module">/ 服务监测 / 巡检执行</span>
<a href="/service-monitor" class="navbar-home">← 返回</a>
</div>
<div class="navbar-user">{{ user.username }}(管理员)</div>
</div>
<div class="container">
<div class="run-card">
<div class="run-head"> <div class="run-head">
<div class="run-title"> <div class="run-title">
{{ '🖥️' if target.type == 'local' else '🌐' }} {{ target.name }} {{ '🖥️' if target.type == 'local' else '🌐' }} {{ target.name }}
...@@ -96,10 +64,11 @@ ...@@ -96,10 +64,11 @@
<div id="done-text"></div> <div id="done-text"></div>
<a href="#" class="btn-report" id="btn-report" style="display:none;">查看报告 →</a> <a href="#" class="btn-report" id="btn-report" style="display:none;">查看报告 →</a>
</div> </div>
</div> </div>
</div> {% endblock %}
<script> {% block extra_js %}
<script>
const TARGET_ID = "{{ target.id }}"; const TARGET_ID = "{{ target.id }}";
const SUITE = "{{ suite }}"; const SUITE = "{{ suite }}";
let currentRunId = null; let currentRunId = null;
...@@ -204,6 +173,5 @@ ...@@ -204,6 +173,5 @@
} }
window.addEventListener('DOMContentLoaded', startInspection); window.addEventListener('DOMContentLoaded', startInspection);
</script> </script>
</body> {% endblock %}
</html> \ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论