self_check.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. {% extends "base.html" %}
  2. {% block title %}系统设备自检{% endblock %}
  3. {% block content %}
  4. <div class="module-header">
  5. <h2>🛡️ 系统设备自检</h2>
  6. </div>
  7. <div class="sub-section">
  8. <div style="background: white; padding: 25px; border-radius: 10px; border: 1px solid #eee; text-align: center;">
  9. <p style="color: #666; margin-bottom: 20px; font-size: 1.1em;">
  10. 点击下方按钮开始对所有连接的设备进行状态检查。<br>
  11. 检查项目包括:电视(Kodi)连通性、门禁控制器、展品LED控制器、以及Home Assistant设备状态。
  12. </p>
  13. <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()">
  14. 🚀 开始全面自检
  15. </button>
  16. <div id="progressArea" style="display: none; margin-top: 20px;">
  17. <div style="color: #2c3e50; font-weight: bold; margin-bottom: 10px;">正在检测中...</div>
  18. <div style="width: 100%; height: 10px; background: #eee; border-radius: 5px; overflow: hidden; max-width: 500px; margin: 0 auto;">
  19. <div id="progressBar" style="width: 0%; height: 100%; background: var(--primary-color); transition: width 0.3s;"></div>
  20. </div>
  21. </div>
  22. </div>
  23. </div>
  24. <div id="reportArea" style="display: none;">
  25. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
  26. <h3 style="color: #2c3e50; margin: 0;">📋 自检报告</h3>
  27. <span id="checkTime" style="color: #999; font-size: 0.9em;"></span>
  28. </div>
  29. <!-- 概览卡片 -->
  30. <div style="display: flex; gap: 20px; margin-bottom: 30px; flex-wrap: wrap;">
  31. <div class="summary-card" id="summaryTotal">
  32. <div class="label">总设备数</div>
  33. <div class="value">0</div>
  34. </div>
  35. <div class="summary-card" id="summaryOnline">
  36. <div class="label">在线</div>
  37. <div class="value" style="color: #2ecc71;">0</div>
  38. </div>
  39. <div class="summary-card" id="summaryOffline">
  40. <div class="label">离线/异常</div>
  41. <div class="value" style="color: #e74c3c;">0</div>
  42. </div>
  43. </div>
  44. <!-- 邮件发送区域 -->
  45. <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;">
  46. <div style="flex: 1; min-width: 250px;">
  47. <label for="reportEmail" style="font-weight: bold; color: #555; margin-right: 10px;">📧 发送报告到邮箱:</label>
  48. <input type="email" id="reportEmail" placeholder="留空则发送给默认接收者" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; width: 250px;">
  49. </div>
  50. <button class="btn btn-info" onclick="sendReport()">📤 发送报告</button>
  51. </div>
  52. <!-- 详细结果 -->
  53. <div id="detailResults">
  54. <!-- 动态生成 -->
  55. </div>
  56. </div>
  57. {% endblock %}
  58. {% block extra_styles %}
  59. .summary-card {
  60. flex: 1;
  61. min-width: 150px;
  62. background: white;
  63. padding: 20px;
  64. border-radius: 10px;
  65. box-shadow: 0 4px 6px rgba(0,0,0,0.05);
  66. text-align: center;
  67. border: 1px solid #eee;
  68. }
  69. .summary-card .label { color: #888; margin-bottom: 5px; font-size: 0.9em; }
  70. .summary-card .value { font-size: 2em; font-weight: bold; color: #333; }
  71. .module-report {
  72. background: white;
  73. border-radius: 10px;
  74. box-shadow: 0 4px 6px rgba(0,0,0,0.05);
  75. margin-bottom: 25px;
  76. overflow: hidden;
  77. }
  78. .module-report-header {
  79. padding: 15px 20px;
  80. background: #f8f9fa;
  81. border-bottom: 1px solid #eee;
  82. display: flex;
  83. justify-content: space-between;
  84. align-items: center;
  85. }
  86. .module-title { font-weight: bold; font-size: 1.1em; color: #2c3e50; }
  87. .module-stats { font-size: 0.9em; color: #666; }
  88. .device-list { padding: 0; }
  89. .device-item {
  90. padding: 12px 20px;
  91. border-bottom: 1px solid #f0f0f0;
  92. display: flex;
  93. justify-content: space-between;
  94. align-items: center;
  95. }
  96. .device-item:last-child { border-bottom: none; }
  97. .device-info { flex: 1; }
  98. .device-name { font-weight: 600; color: #444; }
  99. .device-meta { font-size: 0.85em; color: #999; margin-top: 2px; }
  100. .device-status { font-weight: bold; display: flex; align-items: center; gap: 6px; font-size: 0.9em; }
  101. .status-online { color: #2ecc71; }
  102. .status-offline { color: #e74c3c; }
  103. {% endblock %}
  104. {% block scripts %}
  105. <script>
  106. async function startFullSelfCheck() {
  107. const btn = document.getElementById('btnStartCheck');
  108. const progressArea = document.getElementById('progressArea');
  109. const progressBar = document.getElementById('progressBar');
  110. const reportArea = document.getElementById('reportArea');
  111. btn.disabled = true;
  112. btn.style.opacity = '0.7';
  113. btn.innerHTML = '⏳ 检测进行中...';
  114. progressArea.style.display = 'block';
  115. reportArea.style.display = 'none';
  116. progressBar.style.width = '10%';
  117. // 定义检测任务
  118. const tasks = [
  119. { id: 'kodi', name: '电视系统 (Kodi)', url: '/api/mitv/self_check' },
  120. { id: 'door', name: '门禁系统', url: '/api/door/self_check' },
  121. { id: 'led', name: '展品灯座', url: '/api/led/self_check' },
  122. { id: 'ha', name: '展厅灯光 (HA)', url: '/api/ha/self_check' },
  123. { id: 'pc', name: '展厅PC', url: '/api/pc/self_check' }
  124. ];
  125. let results = {};
  126. let completed = 0;
  127. for (let i = 0; i < tasks.length; i++) {
  128. const task = tasks[i];
  129. try {
  130. const response = await fetch(task.url, {
  131. method: 'POST',
  132. headers: { 'Content-Type': 'application/json' }
  133. });
  134. const json = await response.json();
  135. results[task.id] = {
  136. success: json.success,
  137. data: json.data || [],
  138. error: json.success ? null : json.message
  139. };
  140. } catch (e) {
  141. results[task.id] = {
  142. success: false,
  143. data: [],
  144. error: e.message
  145. };
  146. }
  147. completed++;
  148. progressBar.style.width = `${10 + (completed / tasks.length) * 90}%`;
  149. }
  150. // 渲染报告
  151. renderReport(results, tasks);
  152. btn.disabled = false;
  153. btn.style.opacity = '1';
  154. btn.innerHTML = '🔄 重新开始自检';
  155. progressArea.style.display = 'none';
  156. reportArea.style.display = 'block';
  157. // 滚动到报告区域
  158. reportArea.scrollIntoView({ behavior: 'smooth' });
  159. }
  160. function renderReport(results, tasks) {
  161. const detailContainer = document.getElementById('detailResults');
  162. detailContainer.innerHTML = '';
  163. let totalDevices = 0;
  164. let totalOnline = 0;
  165. let totalOffline = 0;
  166. document.getElementById('checkTime').textContent = '检测时间: ' + new Date().toLocaleString();
  167. tasks.forEach(task => {
  168. const result = results[task.id];
  169. const deviceList = result.data;
  170. let moduleOnline = 0;
  171. let moduleTotal = deviceList.length;
  172. let contentHtml = '';
  173. if (!result.success) {
  174. contentHtml = `<div style="padding: 20px; color: #e74c3c; text-align: center;">检测失败: ${result.error || '未知错误'}</div>`;
  175. totalOffline++; // 整个模块失败算一个异常
  176. } else if (moduleTotal === 0) {
  177. contentHtml = `<div style="padding: 20px; color: #999; text-align: center;">未配置设备</div>`;
  178. } else {
  179. contentHtml = '<div class="device-list">';
  180. deviceList.forEach(device => {
  181. const isOnline = device.is_online;
  182. if (isOnline) {
  183. moduleOnline++;
  184. totalOnline++;
  185. } else {
  186. totalOffline++;
  187. }
  188. totalDevices++;
  189. let extraInfo = [];
  190. if (device.ip) extraInfo.push(`IP: ${device.ip}`);
  191. if (device.entity_id) extraInfo.push(`Entity: ${device.entity_id}`);
  192. if (device.state && device.state !== 'unknown') extraInfo.push(`State: ${device.state}`);
  193. if (device.error) extraInfo.push(`<span style="color: #e74c3c;">Err: ${device.error}</span>`);
  194. contentHtml += `
  195. <div class="device-item">
  196. <div class="device-info">
  197. <div class="device-name">${device.name || 'Unknown Device'}</div>
  198. <div class="device-meta">ID: ${device.id} ${extraInfo.length ? '| ' + extraInfo.join(' | ') : ''}</div>
  199. </div>
  200. <div class="device-status ${isOnline ? 'status-online' : 'status-offline'}">
  201. ${isOnline ? '✅ 在线' : '❌ 离线'}
  202. </div>
  203. </div>
  204. `;
  205. });
  206. contentHtml += '</div>';
  207. }
  208. const moduleHtml = `
  209. <div class="module-report">
  210. <div class="module-report-header">
  211. <div class="module-title">${task.name}</div>
  212. <div class="module-stats">
  213. ${result.success ? `在线: <span style="color: ${moduleOnline === moduleTotal && moduleTotal > 0 ? '#2ecc71' : '#666'}">${moduleOnline}</span> / ${moduleTotal}` : '<span style="color: #e74c3c;">检测失败</span>'}
  214. </div>
  215. </div>
  216. ${contentHtml}
  217. </div>
  218. `;
  219. detailContainer.innerHTML += moduleHtml;
  220. });
  221. // 更新概览
  222. document.getElementById('summaryTotal').querySelector('.value').textContent = totalDevices;
  223. document.getElementById('summaryOnline').querySelector('.value').textContent = totalOnline;
  224. document.getElementById('summaryOffline').querySelector('.value').textContent = totalOffline;
  225. }
  226. async function sendReport() {
  227. const email = document.getElementById('reportEmail').value.trim();
  228. const btn = document.querySelector('button[onclick="sendReport()"]');
  229. try {
  230. btn.disabled = true;
  231. btn.textContent = '发送中...';
  232. const response = await fetch('/api/send_report', {
  233. method: 'POST',
  234. headers: { 'Content-Type': 'application/json' },
  235. body: JSON.stringify({ email: email })
  236. });
  237. const result = await response.json();
  238. if (result.success) {
  239. showMessage(result.message);
  240. } else {
  241. showMessage(result.message, 'error');
  242. }
  243. } catch (error) {
  244. showMessage('网络错误: ' + error.message, 'error');
  245. } finally {
  246. btn.disabled = false;
  247. btn.textContent = '📤 发送报告';
  248. }
  249. }
  250. </script>
  251. {% endblock %}