|
@@ -0,0 +1,266 @@
|
|
|
|
|
+<!DOCTYPE html>
|
|
|
|
|
+<html lang="zh-CN">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="UTF-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
+ <title>套利流程监控</title>
|
|
|
|
|
+ <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
|
|
|
|
+ <style>
|
|
|
|
|
+ body {
|
|
|
|
|
+ font-family: sans-serif;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background-color: #f4f7f6; /* Lighter background for the page */
|
|
|
|
|
+ }
|
|
|
|
|
+ .task-card {
|
|
|
|
|
+ margin-bottom: 15px; /* Slightly reduced margin */
|
|
|
|
|
+ /* border: 1px solid #ddd; */ /* Border is handled by Bootstrap card */
|
|
|
|
|
+ /* border-radius: 5px; */ /* Bootstrap card has rounded corners */
|
|
|
|
|
+ }
|
|
|
|
|
+ .task-card .card-header { /* Styling for the clickable header */
|
|
|
|
|
+ padding: 0; /* Remove padding to let button fill it */
|
|
|
|
|
+ }
|
|
|
|
|
+ .task-card .card-header button {
|
|
|
|
|
+ /* text-decoration: none !important; */ /* Remove underline from btn-link if used */
|
|
|
|
|
+ color: #333; /* Darker text for better readability on light bg */
|
|
|
|
|
+ }
|
|
|
|
|
+ .task-card .card-header button:hover {
|
|
|
|
|
+ background-color: #e9ecef; /* Slightly darker on hover */
|
|
|
|
|
+ }
|
|
|
|
|
+ .state-flow-table th, .state-flow-table td {
|
|
|
|
|
+ font-size: 0.9em;
|
|
|
|
|
+ vertical-align: middle;
|
|
|
|
|
+ }
|
|
|
|
|
+ .status-success { color: green; font-weight: bold; }
|
|
|
|
|
+ .status-fail { color: red; font-weight: bold; }
|
|
|
|
|
+ .status-pending { color: orange; }
|
|
|
|
|
+ .status-info { color: blue; }
|
|
|
|
|
+
|
|
|
|
|
+ .log-msg {
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ max-height: 150px; /* Increased max height for logs */
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ font-size: 0.85em;
|
|
|
|
|
+ background-color: #fdfdfd; /* Slightly off-white for log background */
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ border: 1px solid #eee;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .section-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 1rem;
|
|
|
|
|
+ }
|
|
|
|
|
+ .section-title h2 {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ margin-right: 0.5rem;
|
|
|
|
|
+ }
|
|
|
|
|
+ </style>
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+ <div class="container-fluid">
|
|
|
|
|
+ <h1 class="mb-4">套利流程监控</h1>
|
|
|
|
|
+ <button id="refreshButton" class="btn btn-primary mb-4">刷新数据</button>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="row">
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <div class="section-title">
|
|
|
|
|
+ <h2>正在处理</h2>
|
|
|
|
|
+ <span id="processing-count" class="badge badge-info badge-pill" style="font-size: 1rem;">0</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="processing-list" class="accordion">
|
|
|
|
|
+ <p>加载中...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <div class="section-title">
|
|
|
|
|
+ <h2>历史记录</h2>
|
|
|
|
|
+ <span id="history-count" class="badge badge-secondary badge-pill" style="font-size: 1rem;">0</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="history-list" class="accordion">
|
|
|
|
|
+ <p>加载中...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ const API_BASE_URL = 'http://localhost:5002'; // 修改为你的 Flask API 地址
|
|
|
|
|
+
|
|
|
|
|
+ function getStatusClass(status) { // For text color in stateFlow
|
|
|
|
|
+ status = status ? status.toLowerCase() : 'info';
|
|
|
|
|
+ if (status.includes('success') || status.includes('completed')) return 'status-success';
|
|
|
|
|
+ if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'status-fail';
|
|
|
|
|
+ if (status.includes('pending') || status.includes('waiting') || status.includes('buying') || status_includes_any(status, ['received', 'processing', 'starting', 'pending_start'])) return 'status-pending';
|
|
|
|
|
+ return 'status-info';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function status_includes_any(status, keywords) {
|
|
|
|
|
+ for (const keyword of keywords) {
|
|
|
|
|
+ if (status.includes(keyword)) return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getStatusBadgeClass(status) { // For Bootstrap badge component in card header
|
|
|
|
|
+ status = status ? status.toLowerCase() : 'info';
|
|
|
|
|
+ if (status.includes('success') || status.includes('completed')) return 'badge-success';
|
|
|
|
|
+ if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'badge-danger';
|
|
|
|
|
+ if (status_includes_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'badge-warning';
|
|
|
|
|
+ return 'badge-info';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function formatStateFlow(stateFlow) {
|
|
|
|
|
+ if (!stateFlow || stateFlow.length === 0) {
|
|
|
|
|
+ return '<p><em>无状态流转记录。</em></p>';
|
|
|
|
|
+ }
|
|
|
|
|
+ let tableHtml = `
|
|
|
|
|
+ <table class="table table-sm table-hover table-bordered state-flow-table mt-2">
|
|
|
|
|
+ <thead class="thead-light">
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>时间戳</th>
|
|
|
|
|
+ <th>状态名</th>
|
|
|
|
|
+ <th>消息</th>
|
|
|
|
|
+ <th>结果</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ `;
|
|
|
|
|
+ stateFlow.forEach(state => {
|
|
|
|
|
+ let actualStatus = state.status;
|
|
|
|
|
+ if (Array.isArray(state.status)) {
|
|
|
|
|
+ actualStatus = state.status[state.status.length-1];
|
|
|
|
|
+ }
|
|
|
|
|
+ tableHtml += `
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td style="min-width: 140px;">${state.timestamp || 'N/A'}</td>
|
|
|
|
|
+ <td>${state.stateName || 'N/A'}</td>
|
|
|
|
|
+ <td><div class="log-msg">${state.msg || ''}</div></td>
|
|
|
|
|
+ <td class="${getStatusClass(actualStatus)}">${actualStatus || 'N/A'}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ });
|
|
|
|
|
+ tableHtml += '</tbody></table>';
|
|
|
|
|
+ return tableHtml;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function createTaskCard(task, listIdForAccordion) {
|
|
|
|
|
+ // Generate a safer ID for HTML attributes, especially if task.id could have unusual chars
|
|
|
|
|
+ const safeId = `task-${task.id.replace(/[^a-zA-Z0-9-_]/g, '')}`;
|
|
|
|
|
+ const headingId = `heading-${safeId}`;
|
|
|
|
|
+ const collapseId = `collapse-${safeId}`;
|
|
|
|
|
+
|
|
|
|
|
+ const card = document.createElement('div');
|
|
|
|
|
+ card.className = 'card task-card shadow-sm'; // Added shadow-sm for a bit of depth
|
|
|
|
|
+
|
|
|
|
|
+ // Card Header (Clickable Toggle)
|
|
|
|
|
+ const cardHeader = document.createElement('div');
|
|
|
|
|
+ cardHeader.className = 'card-header'; // Bootstrap handles padding now
|
|
|
|
|
+ cardHeader.id = headingId;
|
|
|
|
|
+ cardHeader.innerHTML = `
|
|
|
|
|
+ <h2 class="mb-0">
|
|
|
|
|
+ <button class="btn btn-light btn-block text-left p-3" type="button" data-toggle="collapse" data-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
|
|
|
|
+ <div class="d-flex justify-content-between align-items-center w-100">
|
|
|
|
|
+ <span style="font-size: 1.1rem; font-weight: bold; color: #007bff;">${task.symbol || 'N/A'}</span>
|
|
|
|
|
+ <span class="badge ${getStatusBadgeClass(task.currentState)} p-2" style="font-size: 0.8rem;">${task.currentState || 'N/A'}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="font-size: 0.85em;" class="text-muted mt-2">
|
|
|
|
|
+ <span>创建: ${task.creationTime || 'N/A'}</span>
|
|
|
|
|
+ <span class="mx-2">|</span>
|
|
|
|
|
+ <span>利润: ${task.profit || 'N/A'} (阈值: ${task.profitLimit || 'N/A'})</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ // Card Body (Collapsible Content)
|
|
|
|
|
+ const collapseDiv = document.createElement('div');
|
|
|
|
|
+ collapseDiv.id = collapseId;
|
|
|
|
|
+ collapseDiv.className = 'collapse'; // Default collapsed
|
|
|
|
|
+ collapseDiv.setAttribute('aria-labelledby', headingId);
|
|
|
|
|
+ collapseDiv.setAttribute('data-parent', `#${listIdForAccordion}`); // For accordion behavior
|
|
|
|
|
+
|
|
|
|
|
+ collapseDiv.innerHTML = `
|
|
|
|
|
+ <div class="card-body">
|
|
|
|
|
+ <p class="mb-1"><small class="text-muted">ID: ${task.id}</small></p>
|
|
|
|
|
+ <hr class="my-2">
|
|
|
|
|
+ <p class="mb-1"><strong>来源代币:</strong> ${task.fromTokenAmountHuman || 'N/A'} ${task.fromToken || ''}</p>
|
|
|
|
|
+ <p><strong>目标代币:</strong> ${task.toTokenAmountHuman || 'N/A'} ${task.toToken || ''}</p>
|
|
|
|
|
+ <h6 class="mt-3">状态流转:</h6>
|
|
|
|
|
+ ${formatStateFlow(task.stateFlow)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ card.appendChild(cardHeader);
|
|
|
|
|
+ card.appendChild(collapseDiv);
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchData(endpoint, targetElementId, countElementId) {
|
|
|
|
|
+ const targetElement = document.getElementById(targetElementId);
|
|
|
|
|
+ const countElement = document.getElementById(countElementId);
|
|
|
|
|
+ if (!targetElement || !countElement) {
|
|
|
|
|
+ console.error(`Element not found: ${targetElementId} or ${countElementId}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ targetElement.innerHTML = '<p class="text-muted"><em>加载中...</em></p>';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`${API_BASE_URL}${endpoint}`);
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ // 对数据按 creationTime 倒序排序
|
|
|
|
|
+ if (data && data.length > 0) {
|
|
|
|
|
+ data.sort((a, b) => {
|
|
|
|
|
+ // 假设 creationTime 是 'YYYY-MM-DD HH:MM:SS,ms' 格式
|
|
|
|
|
+ // 如果不是有效字符串,则将其视为较早的时间
|
|
|
|
|
+ const timeA = a.creationTime || '0';
|
|
|
|
|
+ const timeB = b.creationTime || '0';
|
|
|
|
|
+ if (timeA < timeB) {
|
|
|
|
|
+ return 1; // b 在 a 之前,所以 b 应该排在前面(对于倒序)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (timeA > timeB) {
|
|
|
|
|
+ return -1; // a 在 b 之前,所以 a 应该排在前面(对于倒序)
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0; // 时间相同
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ targetElement.innerHTML = '';
|
|
|
|
|
+ if (data && data.length > 0) {
|
|
|
|
|
+ data.forEach(task => {
|
|
|
|
|
+ // Pass targetElementId to createTaskCard for the data-parent attribute of accordion
|
|
|
|
|
+ targetElement.appendChild(createTaskCard(task, targetElementId));
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ targetElement.innerHTML = '<p class="text-muted"><em>没有任务。</em></p>';
|
|
|
|
|
+ }
|
|
|
|
|
+ countElement.textContent = data.length;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`获取 ${endpoint} 数据失败:`, error);
|
|
|
|
|
+ targetElement.innerHTML = `<p class="text-danger">加载数据失败: ${error.message}</p>`;
|
|
|
|
|
+ countElement.textContent = 'ERR';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function refreshAllData() {
|
|
|
|
|
+ fetchData('/processing', 'processing-list', 'processing-count');
|
|
|
|
|
+ fetchData('/history', 'history-list', 'history-count');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('refreshButton').addEventListener('click', refreshAllData);
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
+ refreshAllData();
|
|
|
|
|
+ // setInterval(refreshAllData, 10000); // 每10秒刷新一次 (可选)
|
|
|
|
|
+ });
|
|
|
|
|
+ </script>
|
|
|
|
|
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
|
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
|
|
|
|
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>
|