self_check.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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 class="report-header-row">
  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 class="report-email-row">
  46. <div class="report-email-field">
  47. <label for="reportEmail" class="report-email-label">📧 发送报告到邮箱:</label>
  48. <input type="email" id="reportEmail" class="report-email-input" placeholder="留空则发送给默认接收者">
  49. </div>
  50. <button type="button" class="btn btn-info report-email-send" 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. .report-header-row {
  104. display: flex;
  105. justify-content: space-between;
  106. align-items: center;
  107. margin-bottom: 20px;
  108. flex-wrap: wrap;
  109. gap: 10px;
  110. }
  111. .report-email-row {
  112. background: #f8f9fa;
  113. padding: 15px;
  114. border-radius: 8px;
  115. border: 1px solid #eee;
  116. margin-bottom: 25px;
  117. display: flex;
  118. align-items: flex-end;
  119. justify-content: space-between;
  120. flex-wrap: wrap;
  121. gap: 15px;
  122. }
  123. .report-email-field {
  124. flex: 1;
  125. min-width: min(100%, 220px);
  126. }
  127. .report-email-label {
  128. display: block;
  129. font-weight: bold;
  130. color: #555;
  131. margin-bottom: 8px;
  132. }
  133. .report-email-input {
  134. width: 100%;
  135. max-width: 400px;
  136. padding: 10px 12px;
  137. border: 1px solid #ddd;
  138. border-radius: 8px;
  139. box-sizing: border-box;
  140. }
  141. .report-email-send {
  142. flex-shrink: 0;
  143. }
  144. @media (max-width: 768px) {
  145. .report-header-row {
  146. flex-direction: column;
  147. align-items: flex-start;
  148. }
  149. .report-email-row {
  150. flex-direction: column;
  151. align-items: stretch;
  152. }
  153. .report-email-input {
  154. max-width: none;
  155. }
  156. .report-email-send {
  157. width: 100%;
  158. }
  159. .module-report-header {
  160. flex-direction: column;
  161. align-items: flex-start;
  162. gap: 8px;
  163. }
  164. .device-item {
  165. flex-direction: column;
  166. align-items: flex-start;
  167. gap: 10px;
  168. }
  169. .device-status {
  170. align-self: stretch;
  171. justify-content: flex-end;
  172. }
  173. }
  174. {% endblock %}
  175. {% block scripts %}
  176. <script>
  177. async function startFullSelfCheck() {
  178. const btn = document.getElementById('btnStartCheck');
  179. const progressArea = document.getElementById('progressArea');
  180. const progressBar = document.getElementById('progressBar');
  181. const reportArea = document.getElementById('reportArea');
  182. btn.disabled = true;
  183. btn.style.opacity = '0.7';
  184. btn.innerHTML = '⏳ 检测进行中...';
  185. progressArea.style.display = 'block';
  186. reportArea.style.display = 'none';
  187. progressBar.style.width = '10%';
  188. // 定义检测任务
  189. const tasks = [
  190. { id: 'kodi', name: '电视系统 (Kodi)', url: '/api/mitv/self_check' },
  191. { id: 'door', name: '门禁系统', url: '/api/door/self_check' },
  192. { id: 'led', name: '展品灯座', url: '/api/led/self_check' },
  193. { id: 'ha', name: '展厅灯光 (HA)', url: '/api/ha/self_check' },
  194. { id: 'pc', name: '展厅PC', url: '/api/pc/self_check' }
  195. ];
  196. let results = {};
  197. let completed = 0;
  198. for (let i = 0; i < tasks.length; i++) {
  199. const task = tasks[i];
  200. try {
  201. const response = await fetch(task.url, {
  202. method: 'POST',
  203. headers: { 'Content-Type': 'application/json' }
  204. });
  205. const json = await response.json();
  206. results[task.id] = {
  207. success: json.success,
  208. data: json.data || [],
  209. error: json.success ? null : json.message
  210. };
  211. } catch (e) {
  212. results[task.id] = {
  213. success: false,
  214. data: [],
  215. error: e.message
  216. };
  217. }
  218. completed++;
  219. progressBar.style.width = `${10 + (completed / tasks.length) * 90}%`;
  220. }
  221. // 渲染报告
  222. renderReport(results, tasks);
  223. btn.disabled = false;
  224. btn.style.opacity = '1';
  225. btn.innerHTML = '🔄 重新开始自检';
  226. progressArea.style.display = 'none';
  227. reportArea.style.display = 'block';
  228. // 滚动到报告区域
  229. reportArea.scrollIntoView({ behavior: 'smooth' });
  230. }
  231. function renderReport(results, tasks) {
  232. const detailContainer = document.getElementById('detailResults');
  233. detailContainer.innerHTML = '';
  234. let totalDevices = 0;
  235. let totalOnline = 0;
  236. let totalOffline = 0;
  237. document.getElementById('checkTime').textContent = '检测时间: ' + new Date().toLocaleString();
  238. tasks.forEach(task => {
  239. const result = results[task.id];
  240. const deviceList = result.data;
  241. let moduleOnline = 0;
  242. let moduleTotal = deviceList.length;
  243. let contentHtml = '';
  244. if (!result.success) {
  245. contentHtml = `<div style="padding: 20px; color: #e74c3c; text-align: center;">检测失败: ${result.error || '未知错误'}</div>`;
  246. totalOffline++; // 整个模块失败算一个异常
  247. } else if (moduleTotal === 0) {
  248. contentHtml = `<div style="padding: 20px; color: #999; text-align: center;">未配置设备</div>`;
  249. } else {
  250. contentHtml = '<div class="device-list">';
  251. deviceList.forEach(device => {
  252. const isOnline = device.is_online;
  253. if (isOnline) {
  254. moduleOnline++;
  255. totalOnline++;
  256. } else {
  257. totalOffline++;
  258. }
  259. totalDevices++;
  260. let extraInfo = [];
  261. if (device.ip) extraInfo.push(`IP: ${device.ip}`);
  262. if (device.entity_id) extraInfo.push(`Entity: ${device.entity_id}`);
  263. if (device.state && device.state !== 'unknown') extraInfo.push(`State: ${device.state}`);
  264. if (device.error) extraInfo.push(`<span style="color: #e74c3c;">Err: ${device.error}</span>`);
  265. contentHtml += `
  266. <div class="device-item">
  267. <div class="device-info">
  268. <div class="device-name">${device.name || 'Unknown Device'}</div>
  269. <div class="device-meta">ID: ${device.id} ${extraInfo.length ? '| ' + extraInfo.join(' | ') : ''}</div>
  270. </div>
  271. <div class="device-status ${isOnline ? 'status-online' : 'status-offline'}">
  272. ${isOnline ? '✅ 在线' : '❌ 离线'}
  273. </div>
  274. </div>
  275. `;
  276. });
  277. contentHtml += '</div>';
  278. }
  279. const moduleHtml = `
  280. <div class="module-report">
  281. <div class="module-report-header">
  282. <div class="module-title">${task.name}</div>
  283. <div class="module-stats">
  284. ${result.success ? `在线: <span style="color: ${moduleOnline === moduleTotal && moduleTotal > 0 ? '#2ecc71' : '#666'}">${moduleOnline}</span> / ${moduleTotal}` : '<span style="color: #e74c3c;">检测失败</span>'}
  285. </div>
  286. </div>
  287. ${contentHtml}
  288. </div>
  289. `;
  290. detailContainer.innerHTML += moduleHtml;
  291. });
  292. // 更新概览
  293. document.getElementById('summaryTotal').querySelector('.value').textContent = totalDevices;
  294. document.getElementById('summaryOnline').querySelector('.value').textContent = totalOnline;
  295. document.getElementById('summaryOffline').querySelector('.value').textContent = totalOffline;
  296. }
  297. async function sendReport() {
  298. const email = document.getElementById('reportEmail').value.trim();
  299. const btn = document.querySelector('.report-email-send');
  300. try {
  301. btn.disabled = true;
  302. btn.textContent = '发送中...';
  303. const response = await fetch('/api/send_report', {
  304. method: 'POST',
  305. headers: { 'Content-Type': 'application/json' },
  306. body: JSON.stringify({ email: email })
  307. });
  308. const result = await response.json();
  309. if (result.success) {
  310. showMessage(result.message);
  311. } else {
  312. showMessage(result.message, 'error');
  313. }
  314. } catch (error) {
  315. showMessage('网络错误: ' + error.message, 'error');
  316. } finally {
  317. btn.disabled = false;
  318. btn.textContent = '📤 发送报告';
  319. }
  320. }
  321. </script>
  322. {% endblock %}