|
|
@@ -0,0 +1,286 @@
|
|
|
+{% extends "base.html" %}
|
|
|
+
|
|
|
+{% block title %}系统设备自检{% endblock %}
|
|
|
+
|
|
|
+{% block content %}
|
|
|
+ <div class="module-header">
|
|
|
+ <h2>🛡️ 系统设备自检</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="sub-section">
|
|
|
+ <div style="background: white; padding: 25px; border-radius: 10px; border: 1px solid #eee; text-align: center;">
|
|
|
+ <p style="color: #666; margin-bottom: 20px; font-size: 1.1em;">
|
|
|
+ 点击下方按钮开始对所有连接的设备进行状态检查。<br>
|
|
|
+ 检查项目包括:电视(Kodi)连通性、门禁控制器、展品LED控制器、以及Home Assistant设备状态。
|
|
|
+ </p>
|
|
|
+ <button id="btnStartCheck" class="btn btn-primary" style="font-size: 1.2em; padding: 15px 40px; border-radius: 50px; box-shadow: 0 4px 15px rgba(78, 205, 196, 0.4);" onclick="startFullSelfCheck()">
|
|
|
+ 🚀 开始全面自检
|
|
|
+ </button>
|
|
|
+ <div id="progressArea" style="display: none; margin-top: 20px;">
|
|
|
+ <div style="color: #2c3e50; font-weight: bold; margin-bottom: 10px;">正在检测中...</div>
|
|
|
+ <div style="width: 100%; height: 10px; background: #eee; border-radius: 5px; overflow: hidden; max-width: 500px; margin: 0 auto;">
|
|
|
+ <div id="progressBar" style="width: 0%; height: 100%; background: var(--primary-color); transition: width 0.3s;"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="reportArea" style="display: none;">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
+ <h3 style="color: #2c3e50; margin: 0;">📋 自检报告</h3>
|
|
|
+ <span id="checkTime" style="color: #999; font-size: 0.9em;"></span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 概览卡片 -->
|
|
|
+ <div style="display: flex; gap: 20px; margin-bottom: 30px; flex-wrap: wrap;">
|
|
|
+ <div class="summary-card" id="summaryTotal">
|
|
|
+ <div class="label">总设备数</div>
|
|
|
+ <div class="value">0</div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-card" id="summaryOnline">
|
|
|
+ <div class="label">在线</div>
|
|
|
+ <div class="value" style="color: #2ecc71;">0</div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-card" id="summaryOffline">
|
|
|
+ <div class="label">离线/异常</div>
|
|
|
+ <div class="value" style="color: #e74c3c;">0</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 邮件发送区域 -->
|
|
|
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 25px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
|
|
|
+ <div style="flex: 1; min-width: 250px;">
|
|
|
+ <label for="reportEmail" style="font-weight: bold; color: #555; margin-right: 10px;">📧 发送报告到邮箱:</label>
|
|
|
+ <input type="email" id="reportEmail" placeholder="留空则发送给默认接收者" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; width: 250px;">
|
|
|
+ </div>
|
|
|
+ <button class="btn btn-info" onclick="sendReport()">📤 发送报告</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 详细结果 -->
|
|
|
+ <div id="detailResults">
|
|
|
+ <!-- 动态生成 -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+{% endblock %}
|
|
|
+
|
|
|
+{% block extra_styles %}
|
|
|
+ .summary-card {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 150px;
|
|
|
+ background: white;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
|
|
+ text-align: center;
|
|
|
+ border: 1px solid #eee;
|
|
|
+ }
|
|
|
+ .summary-card .label { color: #888; margin-bottom: 5px; font-size: 0.9em; }
|
|
|
+ .summary-card .value { font-size: 2em; font-weight: bold; color: #333; }
|
|
|
+
|
|
|
+ .module-report {
|
|
|
+ background: white;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
|
|
+ margin-bottom: 25px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .module-report-header {
|
|
|
+ padding: 15px 20px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ .module-title { font-weight: bold; font-size: 1.1em; color: #2c3e50; }
|
|
|
+ .module-stats { font-size: 0.9em; color: #666; }
|
|
|
+
|
|
|
+ .device-list { padding: 0; }
|
|
|
+ .device-item {
|
|
|
+ padding: 12px 20px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ .device-item:last-child { border-bottom: none; }
|
|
|
+ .device-info { flex: 1; }
|
|
|
+ .device-name { font-weight: 600; color: #444; }
|
|
|
+ .device-meta { font-size: 0.85em; color: #999; margin-top: 2px; }
|
|
|
+ .device-status { font-weight: bold; display: flex; align-items: center; gap: 6px; font-size: 0.9em; }
|
|
|
+
|
|
|
+ .status-online { color: #2ecc71; }
|
|
|
+ .status-offline { color: #e74c3c; }
|
|
|
+{% endblock %}
|
|
|
+
|
|
|
+{% block scripts %}
|
|
|
+<script>
|
|
|
+ async function startFullSelfCheck() {
|
|
|
+ const btn = document.getElementById('btnStartCheck');
|
|
|
+ const progressArea = document.getElementById('progressArea');
|
|
|
+ const progressBar = document.getElementById('progressBar');
|
|
|
+ const reportArea = document.getElementById('reportArea');
|
|
|
+
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.style.opacity = '0.7';
|
|
|
+ btn.innerHTML = '⏳ 检测进行中...';
|
|
|
+ progressArea.style.display = 'block';
|
|
|
+ reportArea.style.display = 'none';
|
|
|
+ progressBar.style.width = '10%';
|
|
|
+
|
|
|
+ // 定义检测任务
|
|
|
+ const tasks = [
|
|
|
+ { id: 'kodi', name: '电视系统 (Kodi)', url: '/api/mitv/self_check' },
|
|
|
+ { id: 'door', name: '门禁系统', url: '/api/door/self_check' },
|
|
|
+ { id: 'led', name: '展品灯座', url: '/api/led/self_check' },
|
|
|
+ { id: 'ha', name: '展厅灯光 (HA)', url: '/api/ha/self_check' },
|
|
|
+ { id: 'pc', name: '展厅PC', url: '/api/pc/self_check' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ let results = {};
|
|
|
+ let completed = 0;
|
|
|
+
|
|
|
+ for (let i = 0; i < tasks.length; i++) {
|
|
|
+ const task = tasks[i];
|
|
|
+ try {
|
|
|
+ const response = await fetch(task.url, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' }
|
|
|
+ });
|
|
|
+ const json = await response.json();
|
|
|
+ results[task.id] = {
|
|
|
+ success: json.success,
|
|
|
+ data: json.data || [],
|
|
|
+ error: json.success ? null : json.message
|
|
|
+ };
|
|
|
+ } catch (e) {
|
|
|
+ results[task.id] = {
|
|
|
+ success: false,
|
|
|
+ data: [],
|
|
|
+ error: e.message
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ completed++;
|
|
|
+ progressBar.style.width = `${10 + (completed / tasks.length) * 90}%`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染报告
|
|
|
+ renderReport(results, tasks);
|
|
|
+
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.style.opacity = '1';
|
|
|
+ btn.innerHTML = '🔄 重新开始自检';
|
|
|
+ progressArea.style.display = 'none';
|
|
|
+ reportArea.style.display = 'block';
|
|
|
+
|
|
|
+ // 滚动到报告区域
|
|
|
+ reportArea.scrollIntoView({ behavior: 'smooth' });
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderReport(results, tasks) {
|
|
|
+ const detailContainer = document.getElementById('detailResults');
|
|
|
+ detailContainer.innerHTML = '';
|
|
|
+
|
|
|
+ let totalDevices = 0;
|
|
|
+ let totalOnline = 0;
|
|
|
+ let totalOffline = 0;
|
|
|
+
|
|
|
+ document.getElementById('checkTime').textContent = '检测时间: ' + new Date().toLocaleString();
|
|
|
+
|
|
|
+ tasks.forEach(task => {
|
|
|
+ const result = results[task.id];
|
|
|
+ const deviceList = result.data;
|
|
|
+
|
|
|
+ let moduleOnline = 0;
|
|
|
+ let moduleTotal = deviceList.length;
|
|
|
+ let contentHtml = '';
|
|
|
+
|
|
|
+ if (!result.success) {
|
|
|
+ contentHtml = `<div style="padding: 20px; color: #e74c3c; text-align: center;">检测失败: ${result.error || '未知错误'}</div>`;
|
|
|
+ totalOffline++; // 整个模块失败算一个异常
|
|
|
+ } else if (moduleTotal === 0) {
|
|
|
+ contentHtml = `<div style="padding: 20px; color: #999; text-align: center;">未配置设备</div>`;
|
|
|
+ } else {
|
|
|
+ contentHtml = '<div class="device-list">';
|
|
|
+ deviceList.forEach(device => {
|
|
|
+ const isOnline = device.is_online;
|
|
|
+ if (isOnline) {
|
|
|
+ moduleOnline++;
|
|
|
+ totalOnline++;
|
|
|
+ } else {
|
|
|
+ totalOffline++;
|
|
|
+ }
|
|
|
+ totalDevices++;
|
|
|
+
|
|
|
+ let extraInfo = [];
|
|
|
+ if (device.ip) extraInfo.push(`IP: ${device.ip}`);
|
|
|
+ if (device.entity_id) extraInfo.push(`Entity: ${device.entity_id}`);
|
|
|
+ if (device.state && device.state !== 'unknown') extraInfo.push(`State: ${device.state}`);
|
|
|
+ if (device.error) extraInfo.push(`<span style="color: #e74c3c;">Err: ${device.error}</span>`);
|
|
|
+
|
|
|
+ contentHtml += `
|
|
|
+ <div class="device-item">
|
|
|
+ <div class="device-info">
|
|
|
+ <div class="device-name">${device.name || 'Unknown Device'}</div>
|
|
|
+ <div class="device-meta">ID: ${device.id} ${extraInfo.length ? '| ' + extraInfo.join(' | ') : ''}</div>
|
|
|
+ </div>
|
|
|
+ <div class="device-status ${isOnline ? 'status-online' : 'status-offline'}">
|
|
|
+ ${isOnline ? '✅ 在线' : '❌ 离线'}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+ contentHtml += '</div>';
|
|
|
+ }
|
|
|
+
|
|
|
+ const moduleHtml = `
|
|
|
+ <div class="module-report">
|
|
|
+ <div class="module-report-header">
|
|
|
+ <div class="module-title">${task.name}</div>
|
|
|
+ <div class="module-stats">
|
|
|
+ ${result.success ? `在线: <span style="color: ${moduleOnline === moduleTotal && moduleTotal > 0 ? '#2ecc71' : '#666'}">${moduleOnline}</span> / ${moduleTotal}` : '<span style="color: #e74c3c;">检测失败</span>'}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ${contentHtml}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ detailContainer.innerHTML += moduleHtml;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新概览
|
|
|
+ document.getElementById('summaryTotal').querySelector('.value').textContent = totalDevices;
|
|
|
+ document.getElementById('summaryOnline').querySelector('.value').textContent = totalOnline;
|
|
|
+ document.getElementById('summaryOffline').querySelector('.value').textContent = totalOffline;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function sendReport() {
|
|
|
+ const email = document.getElementById('reportEmail').value.trim();
|
|
|
+ const btn = document.querySelector('button[onclick="sendReport()"]');
|
|
|
+
|
|
|
+ try {
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.textContent = '发送中...';
|
|
|
+
|
|
|
+ const response = await fetch('/api/send_report', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ email: email })
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.success) {
|
|
|
+ showMessage(result.message);
|
|
|
+ } else {
|
|
|
+ showMessage(result.message, 'error');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showMessage('网络错误: ' + error.message, 'error');
|
|
|
+ } finally {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = '📤 发送报告';
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+{% endblock %}
|