Browse Source

新版网页

skyfffire 2 tháng trước cách đây
mục cha
commit
3b5a2cc8fb
3 tập tin đã thay đổi với 263 bổ sung367 xóa
  1. 1 0
      .gitignore
  2. 44 1
      as.py
  3. 218 366
      monitor.html

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 /.idea
+.vscode
 signals/
 __pycache__/
 logs/

+ 44 - 1
as.py

@@ -15,9 +15,11 @@ import web3_py_client
 import traceback
 import copy
 import sys
+import pandas as pd
+import io
 
 from decimal import Decimal, ROUND_DOWN
-from flask import Flask, request, jsonify
+from flask import Flask, request, jsonify, send_file
 from flask_cors import CORS # 导入
 from as_utils import get_formatted_timestamp
 from as_utils import add_state_flow_entry
@@ -369,6 +371,47 @@ def get_status():
             "last_process_info": last_process_info # 最后处理信息 (如果使用)
         })
 
+@app.route('/download_history_excel', methods=['GET'])
+def download_history_excel():
+    """下载历史记录的 Excel 文件"""
+    with list_lock:
+        # 为了线程安全,复制一份列表进行处理
+        history_data = list(history_process_list)
+
+    if not history_data:
+        # 如果没有数据,可以返回一个空文件或一个提示信息
+        return "没有历史记录可以下载。", 404
+
+    # 1. 使用 pandas 将列表数据转换成 DataFrame
+    #    假设您的列表中的每个元素都是一个字典,例如:
+    #    {'交易对': 'DORKY_USDT', '利润': 1.23, '创建时间': '...'}
+    df = pd.DataFrame(history_data)
+    
+    # (可选) 整理列的顺序或重命名,让 Excel 更美观
+    # df = df[['创建时间', '交易对', '利润', '最终状态']] 
+    # df.rename(columns={'创建时间': '交易发生时间'}, inplace=True)
+
+    # 2. 在内存中创建一个 Excel 文件
+    output = io.BytesIO()
+    # 使用 to_excel 方法,并用 openpyxl 引擎
+    # index=False 表示不把 DataFrame 的索引写入 Excel
+    with pd.ExcelWriter(output, engine='openpyxl') as writer:
+        df.to_excel(writer, index=False, sheet_name='History')
+    
+    # 移动到二进制流的开头
+    output.seek(0)
+
+    # 3. 使用 send_file 将内存中的文件作为附件发送给浏览器
+    return send_file(
+        output,
+        # attachment_filename 是浏览器下载时默认显示的文件名
+        download_name='history_records.xlsx',
+        # as_attachment=True 表示作为附件下载,而不是在浏览器中打开
+        as_attachment=True,
+        # mimetype 告诉浏览器这是一个 Excel 文件
+        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    )
+
 if __name__ == "__main__":
     logger.info("启动核心数据更新线程...")
     updater_thread = threading.Thread(target=update_core_data_periodically, daemon=True)

+ 218 - 366
monitor.html

@@ -3,419 +3,271 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>套利流程监控 (Tailwind)</title>
+    <title>套利流程监控</title>
     <script src="https://cdn.tailwindcss.com"></script>
+    <link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
+    <script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
     <style>
-        body {
-            font-family: sans-serif;
-            background-color: #f3f4f6; /* Tailwind bg-gray-100 */
-        }
-        .modal {
-            transition: opacity 0.25s ease;
-        }
-        .modal-active {
-            overflow-x: hidden;
-            overflow-y: auto;
-        }
-        /* MODAL CONTENT: 调整最大高度和溢出处理 */
-        .modal-content {
-            max-height: calc(100vh - 4rem); /* 模态框内容最大高度,留出上下边距 */
-            overflow-y: auto;
-        }
-        .log-msg {
-            white-space: pre-wrap;
-            word-break: break-all;
-            /* max-height is now set via inline style in JS for easier adjustment */
-            /* overflow-y: auto; */
-            font-size: 0.82em; /* Slightly larger log font */
-            line-height: 1.45; /* Increased line height for log messages */
-            background-color: #f9fafb;
-            padding: 6px 8px;
-            border: 1px solid #e5e7eb;
-            border-radius: 4px;
-            margin-top: 3px;
-        }
-        .table-responsive-container {
-            overflow-x: auto;
-            -webkit-overflow-scrolling: touch;
-        }
-        .status-success { color: #10b981; font-weight: 600; } /* Tailwind green-600 semibold */
-        .status-fail { color: #ef4444; font-weight: 600; }    /* Tailwind red-500 semibold */
-        .status-pending { color: #f59e0b; font-weight: 500; } /* Tailwind amber-500 medium */
-        .status-info { color: #3b82f6; font-weight: 500; }    /* Tailwind blue-500 medium */
+        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,"Helvetica Neue", Arial, "Noto Sans", sans-serif,"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: #f7fafc; }
+        .modal { transition: opacity 0.25s ease; }
+        .modal-active { overflow: hidden; }
+        .modal-content { height: calc(100vh - 2rem); width: calc(100vw - 2rem); max-width: none; max-height: none; overflow-y: auto; }
+        .log-msg { white-space: pre-wrap; word-break: break-all; font-size: 0.75rem; line-height: 1.5; font-family: 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", monospace; background-color: #f9fafb; padding: 4px 6px; border: 1px solid #e5e7eb; border-radius: 4px; margin-top: 2px; }
+        .table-responsive-container { overflow-x: auto; }
+        .status-success { color: #10b981; } .status-fail { color: #ef4444; } .status-pending { color: #f59e0b; } .status-info { color: #3b82f6; }
+        .datatable-wrapper { padding: 0; }
+        .datatable-container { border: 1px solid #e2e8f0; border-radius: 0.375rem; }
+        .datatable-top { padding: 0.5rem; }
+        .datatable-input { padding: 0.3rem 0.6rem; font-size: 0.875rem; border-radius: 0.375rem; border: 1px solid #d1d5db; }
+        .datatable-table { font-size: 0.875rem; }
+        .datatable-table th, .datatable-table td { padding: 0.6rem 0.8rem; border-bottom: 1px solid #e2e8f0; }
+        .datatable-bottom { display: none; }
+        .datatable-sorter { color: #4b5563; text-decoration: none; }
+        .datatable-sorter::before, .datatable-sorter::after { border-color: #9ca3af; }
     </style>
 </head>
-<body class="text-gray-800">
-    <div class="container mx-auto p-4 md:p-6">
-        <!-- Header and Tabs remain the same -->
-        <header class="mb-6">
-            <h1 class="text-3xl font-bold text-gray-700">套利流程监控</h1>
-            <div class="mt-4 space-x-2">
-                <button id="refreshButton" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition duration-150">手动刷新</button>
-                <button id="toggleAutoRefreshButton" class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition duration-150">暂停自动刷新</button>
+<body class="text-gray-800 antialiased">
+    <div class="container mx-auto px-2 py-4 sm:px-4 sm:py-6">
+        <header class="mb-4">
+            <h1 class="text-2xl font-bold text-gray-700">套利流程监控</h1>
+            <div class="mt-3 space-x-2">
+                <button id="refreshButton" class="px-3 py-1.5 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition">手动刷新</button>
+                <button id="toggleAutoRefreshButton" class="px-3 py-1.5 text-sm bg-gray-500 text-white rounded hover:bg-gray-600 transition">暂停自动刷新</button>
+                <a id="downloadExcelLink" href="http://localhost:1888/download_history_excel" target="_blank" class="inline-block">
+                    <button class="px-3 py-1.5 text-sm bg-green-600 text-white rounded hover:bg-green-700 transition">下载历史(Excel)</button>
+                </a>
             </div>
         </header>
 
         <div class="mb-4 border-b border-gray-200">
             <ul class="flex flex-wrap -mb-px text-sm font-medium text-center" id="myTab" role="tablist">
-                <li class="mr-2" role="presentation">
-                    <button class="tab-button inline-block p-4 border-b-2 rounded-t-lg" id="processing-tab" type="button" role="tab" aria-controls="processing" aria-selected="true">
-                        正在处理 <span id="processing-count-badge" class="ml-1 px-2 py-0.5 bg-blue-100 text-blue-800 text-xs font-semibold rounded-full">0</span>
-                    </button>
-                </li>
-                <li class="mr-2" role="presentation">
-                    <button class="tab-button inline-block p-4 border-b-2 rounded-t-lg border-transparent hover:text-gray-600 hover:border-gray-300" id="history-tab" type="button" role="tab" aria-controls="history" aria-selected="false">
-                        历史记录 <span id="history-count-badge" class="ml-1 px-2 py-0.5 bg-gray-100 text-gray-800 text-xs font-semibold rounded-full">0</span>
-                    </button>
-                </li>
+                <li class="mr-2" role="presentation"><button class="tab-button inline-block p-3 border-b-2 rounded-t-lg" id="processing-tab" type="button">正在处理 <span id="processing-count-badge" class="ml-1 px-2 py-0.5 bg-blue-100 text-blue-800 text-xs font-semibold rounded-full">0</span></button></li>
+                <li class="mr-2" role="presentation"><button class="tab-button inline-block p-3 border-b-2 rounded-t-lg border-transparent hover:text-gray-600 hover:border-gray-300" id="history-tab" type="button">历史记录 <span id="history-count-badge" class="ml-1 px-2 py-0.5 bg-gray-100 text-gray-800 text-xs font-semibold rounded-full">0</span></button></li>
             </ul>
         </div>
-
+        
         <div id="tabContent">
-            <div class="tab-pane p-1 bg-white rounded-lg shadow" id="processing" role="tabpanel" aria-labelledby="processing-tab">
+            <div class="tab-pane bg-white rounded-lg shadow-sm" id="processing" role="tabpanel">
                 <div class="table-responsive-container">
-                    <table class="min-w-full divide-y divide-gray-200">
-                        <thead class="bg-gray-50">
-                            <tr>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易对</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">当前状态</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">利润</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">创建时间</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
-                            </tr>
-                        </thead>
-                        <tbody id="processing-list" class="bg-white divide-y divide-gray-200">
-                             <tr><td colspan="5" class="text-center p-4 text-gray-500">加载中...</td></tr>
-                        </tbody>
+                    <table class="min-w-full text-sm">
+                        <thead class="bg-gray-50"><tr><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易对</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">利润</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">创建时间</th><th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead>
+                        <tbody id="processing-list" class="divide-y divide-gray-200"></tbody>
                     </table>
                 </div>
             </div>
-            <div class="tab-pane hidden p-1 bg-white rounded-lg shadow" id="history" role="tabpanel" aria-labelledby="history-tab">
-                 <div class="table-responsive-container">
-                    <table class="min-w-full divide-y divide-gray-200">
-                        <thead class="bg-gray-50">
-                           <tr>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">交易对</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最终状态</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">利润</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">创建时间</th>
-                                <th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
-                            </tr>
-                        </thead>
-                        <tbody id="history-list" class="bg-white divide-y divide-gray-200">
-                             <tr><td colspan="5" class="text-center p-4 text-gray-500">加载中...</td></tr>
-                        </tbody>
-                    </table>
+            <div class="tab-pane hidden bg-white rounded-lg shadow-sm" id="history" role="tabpanel">
+                <div class="table-responsive-container">
+                    <table id="history-table" class="min-w-full"></table>
                 </div>
             </div>
         </div>
     </div>
 
-    <!-- Modal: INCREASED MAX-WIDTH, PADDING, and added outer padding for small screens -->
-    <div id="taskDetailModal" class="modal fixed inset-0 bg-gray-600 bg-opacity-75 overflow-y-auto h-full w-full flex items-center justify-center hidden z-50 p-4 sm:p-6 md:p-8">
-        <div class="modal-content relative mx-auto p-5 sm:p-6 border w-full max-w-xl md:max-w-3xl lg:max-w-5xl shadow-xl rounded-lg bg-white">
-            <div class="flex justify-between items-center pb-4 border-b border-gray-200">
-                <h3 class="text-xl sm:text-2xl font-semibold text-gray-800">任务详情</h3>
+    <!-- Modal -->
+    <div id="taskDetailModal" class="modal fixed inset-0 bg-gray-600 bg-opacity-75 h-full w-full flex items-center justify-center hidden z-50">
+        <div class="modal-content relative mx-auto p-4 sm:p-5 border w-full shadow-xl rounded-lg bg-white">
+            <div class="flex justify-between items-center pb-3 border-b border-gray-200 sticky top-0 bg-white z-10 -mt-4 -mx-4 sm:-mt-5 sm:-mx-5 px-4 sm:px-5 pt-4 sm:pt-5 rounded-t-lg">
+                <h3 class="text-lg sm:text-xl font-semibold text-gray-800">任务详情</h3>
                 <button id="closeModalButton" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center">
-                    <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
-                    <span class="sr-only">关闭</span>
+                    <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
                 </button>
             </div>
-            <div id="modalBody" class="pt-4 text-sm leading-relaxed space-y-3">
-                <!-- Modal content will be injected here -->
-            </div>
+            <div id="modalBody" class="pt-3 text-sm leading-relaxed space-y-3"></div>
         </div>
     </div>
 
     <script>
-        // Constants and global variables (API_BASE_URL, etc.) remain the same
-        const API_BASE_URL = 'http://localhost:1888';
-        const REFRESH_INTERVAL_MS = 10000;
-        let autoRefreshIntervalId = null;
-        let isAutoRefreshPaused = false;
-        let allTasksData = {};
-
-        const refreshButton = document.getElementById('refreshButton');
-        const toggleAutoRefreshButton = document.getElementById('toggleAutoRefreshButton');
-        const processingTab = document.getElementById('processing-tab');
-        const historyTab = document.getElementById('history-tab');
-        const processingPane = document.getElementById('processing');
-        const historyPane = document.getElementById('history');
-        const processingCountBadge = document.getElementById('processing-count-badge');
-        const historyCountBadge = document.getElementById('history-count-badge');
-        const taskDetailModal = document.getElementById('taskDetailModal');
-        const modalBody = document.getElementById('modalBody');
-        const closeModalButton = document.getElementById('closeModalButton');
-
-        // Helper functions (getStatusTextClass, getStatusBadgeClasses, status_includes_any) remain the same
-        function getStatusTextClass(status) {
-            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_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'status-pending';
-            return 'status-info';
-        }
-
-        function getStatusBadgeClasses(status) {
-            status = status ? status.toLowerCase() : 'info';
-            if (status.includes('success') || status.includes('completed')) return 'bg-green-100 text-green-800';
-            if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'bg-red-100 text-red-800';
-            if (status_includes_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'bg-yellow-100 text-yellow-800';
-            return 'bg-blue-100 text-blue-800';
-        }
-
-        function status_includes_any(status, keywords) {
-            for (const keyword of keywords) {
-                if (status.includes(keyword)) return true;
+        document.addEventListener('DOMContentLoaded', () => {
+            const API_BASE_URL = 'http://localhost:1888';
+            const REFRESH_INTERVAL_MS = 10000;
+            let autoRefreshIntervalId = null;
+            let isAutoRefreshPaused = false;
+            let allTasksData = {};
+            let historyDataTable = null;
+
+            const refreshButton = document.getElementById('refreshButton');
+            const toggleAutoRefreshButton = document.getElementById('toggleAutoRefreshButton');
+            const tabButtons = document.querySelectorAll('.tab-button');
+            const processingCountBadge = document.getElementById('processing-count-badge');
+            const historyCountBadge = document.getElementById('history-count-badge');
+            const taskDetailModal = document.getElementById('taskDetailModal');
+            const modalBody = document.getElementById('modalBody');
+            const closeModalButton = document.getElementById('closeModalButton');
+
+            // --- Helper Functions ---
+            function getStatusTextClass(status) {
+                status = status ? status.toLowerCase() : '';
+                if (status_includes_any(status, ['fail', 'error'])) return 'status-fail';
+                if (status_includes_any(status, ['success', 'completed'])) return 'status-success';
+                if (status_includes_any(status, ['pending', 'waiting', 'processing'])) return 'status-pending';
+                return 'status-info';
             }
-            return false;
-        }
-
-        // MODIFIED: formatStateFlowForModal - Adjusted column widths and log-msg max-height
-        function formatStateFlowForModal(stateFlow) {
-            if (!stateFlow || stateFlow.length === 0) {
-                return '<p class="text-gray-500 italic">无状态流转记录。</p>';
+            function getStatusBadgeClasses(status) {
+                status = status ? status.toLowerCase() : '';
+                if (status_includes_any(status, ['fail', 'error'])) return 'bg-red-100 text-red-800';
+                if (status_includes_any(status, ['success', 'completed'])) return 'bg-green-100 text-green-800';
+                if (status_includes_any(status, ['pending', 'waiting', 'processing'])) return 'bg-yellow-100 text-yellow-800';
+                return 'bg-blue-100 text-blue-800';
             }
-            // Increased padding, adjusted font, and widths for better readability
-            let tableHtml = `
-                <div class="overflow-x-auto border border-gray-200 rounded-md shadow-sm">
-                    <table class="min-w-full divide-y divide-gray-200 text-xs sm:text-sm">
-                        <thead class="bg-gray-100">
-                            <tr>
-                                <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 130px;">时间戳</th>
-                                <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 100px;">状态名</th>
-                                <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 250px;">消息</th>
-                                <th class="px-3 py-2.5 sm:px-4 text-left font-semibold text-gray-600 uppercase tracking-wider" style="min-width: 80px;">结果</th>
-                            </tr>
-                        </thead>
-                        <tbody class="bg-white divide-y divide-gray-200">
-            `;
-            stateFlow.forEach(state => {
-                let actualStatus = state.status;
-                if (Array.isArray(state.status)) {
-                    actualStatus = state.status[state.status.length - 1];
-                }
-                // Increased log-msg max-height
-                tableHtml += `
-                    <tr>
-                        <td class="px-3 py-2 sm:px-4 whitespace-nowrap text-gray-700">${state.timestamp || 'N/A'}</td>
-                        <td class="px-3 py-2 sm:px-4 text-gray-700">${state.stateName || 'N/A'}</td>
-                        <td class="px-3 py-2 sm:px-4 text-gray-700">
-                            <div class="log-msg">${state.msg || ''}</div>
-                        </td>
-                        <td class="px-3 py-2 sm:px-4 ${getStatusTextClass(actualStatus)}">${actualStatus || 'N/A'}</td>
-                    </tr>
-                `;
-            });
-            tableHtml += '</tbody></table></div>';
-            return tableHtml;
-        }
-
-        // createTableRow function remains the same
-        function createTableRow(task) {
-            const tr = document.createElement('tr');
-            tr.className = 'hover:bg-gray-50 transition duration-150';
-            const profitText = task.profit ? `${task.profit}` : ('N/A');
-            
-            tr.innerHTML = `
-                <td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${task.symbol || 'N/A'}</td>
-                <td class="px-4 py-3 whitespace-nowrap text-sm">
-                    <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusBadgeClasses(task.currentState)}">
-                        ${task.currentState || 'N/A'}
-                    </span>
-                </td>
-                <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 hidden md:table-cell">${profitText}</td>
-                <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 hidden sm:table-cell">${task.creationTime || 'N/A'}</td>
-                <td class="px-4 py-3 whitespace-nowrap text-sm font-medium">
-                    <button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button>
-                </td>
-            `;
-            return tr;
-        }
-
-        // renderTable and fetchDataForTab functions remain the same
-        function renderTable(data, targetTableBodyId, countBadgeElement) {
-            const tableBody = document.getElementById(targetTableBodyId);
-            if (!tableBody || !countBadgeElement) return;
-
-            tableBody.innerHTML = ''; 
-
-            if (data && data.length > 0) {
-                data.sort((a, b) => (a.creationTime || '0') < (b.creationTime || '0') ? 1 : -1); 
-                data.forEach(task => {
-                    allTasksData[task.id] = task; 
-                    tableBody.appendChild(createTableRow(task));
+            function status_includes_any(status, keywords) { for (const k of keywords) if (status.includes(k)) return true; return false; }
+            function formatStateFlowForModal(stateFlow) {
+                if (!stateFlow || stateFlow.length === 0) return '<p class="text-gray-500 italic">无状态流转记录。</p>';
+                let tableHtml = '<div class="overflow-x-auto border border-gray-200 rounded-md shadow-sm"><table class="min-w-full divide-y divide-gray-200 text-xs"><thead class="bg-gray-100"><tr><th class="px-2 py-2 text-left font-semibold text-gray-600">时间戳</th><th class="px-2 py-2 text-left font-semibold text-gray-600">状态名</th><th class="px-2 py-2 text-left font-semibold text-gray-600">消息</th><th class="px-2 py-2 text-left font-semibold text-gray-600">结果</th></tr></thead><tbody class="bg-white divide-y divide-gray-200">';
+                stateFlow.forEach(state => {
+                    let actualStatus = Array.isArray(state.status) ? state.status[state.status.length - 1] : state.status;
+                    tableHtml += `<tr><td class="px-2 py-2 whitespace-nowrap text-gray-700">${state.timestamp || 'N/A'}</td><td class="px-2 py-2 text-gray-800 font-medium">${state.stateName || 'N/A'}</td><td class="px-2 py-2 text-gray-700"><div class="log-msg">${state.msg || ''}</div></td><td class="px-2 py-2 font-semibold ${getStatusTextClass(actualStatus)}">${actualStatus || 'N/A'}</td></tr>`;
                 });
-            } else {
-                tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">没有任务。</td></tr>`;
-            }
-            countBadgeElement.textContent = data.length;
-        }
-        
-        async function fetchDataForTab(endpoint, tableBodyId, countBadgeElement) {
-            const tableBody = document.getElementById(tableBodyId);
-            const isInitialLoad = tableBody.querySelector('td[colspan="5"]');
-             if (isInitialLoad) {
-                 tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">加载中...</td></tr>`;
+                return tableHtml += '</tbody></table></div>';
             }
 
-            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();
-                renderTable(data, tableBodyId, countBadgeElement);
-            } catch (error) {
-                console.error(`获取 ${endpoint} 数据失败:`, error);
-                 if (isInitialLoad) {
-                    tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败: ${error.message}</td></tr>`;
+            // --- Table Rendering Functions ---
+            function renderProcessingTable(data) {
+                const tableBody = document.getElementById('processing-list');
+                tableBody.innerHTML = '';
+                if (data && data.length > 0) {
+                    data.sort((a, b) => (a.creationTime || '0') < (b.creationTime || '0') ? 1 : -1);
+                    data.forEach(task => {
+                        allTasksData[task.id] = task;
+                        const tr = document.createElement('tr');
+                        tr.className = 'hover:bg-gray-50';
+                        const profitText = task.profit !== null && !isNaN(parseFloat(task.profit)) ? `${parseFloat(task.profit).toFixed(4)}` : 'N/A';
+                        tr.innerHTML = `<td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900">${task.symbol||'N/A'}</td><td class="px-3 py-2 whitespace-nowrap"><span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusBadgeClasses(task.currentState)}">${task.currentState||'N/A'}</span></td><td class="px-3 py-2 whitespace-nowrap text-gray-600 hidden md:table-cell">${profitText}</td><td class="px-3 py-2 whitespace-nowrap text-gray-600 hidden sm:table-cell">${task.creationTime||'N/A'}</td><td class="px-3 py-2 whitespace-nowrap text-sm font-medium"><button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button></td>`;
+                        tableBody.appendChild(tr);
+                    });
                 } else {
-                     console.warn("获取更新数据失败,保留旧数据。")
+                    tableBody.innerHTML = `<tr><td colspan="5" class="text-center p-4 text-gray-500">没有正在处理的任务。</td></tr>`;
                 }
-                countBadgeElement.textContent = 'ERR';
+                processingCountBadge.textContent = data.length;
             }
-        }
-
-        // refreshAllData, startAutoRefresh, stopAutoRefresh functions remain the same
-        function refreshAllData() {
-            console.log("Refreshing data...");
-            fetchDataForTab('/processing', 'processing-list', processingCountBadge);
-            fetchDataForTab('/history', 'history-list', historyCountBadge);
-        }
-
-        function startAutoRefresh() {
-            if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId);
-            autoRefreshIntervalId = setInterval(refreshAllData, REFRESH_INTERVAL_MS);
-            isAutoRefreshPaused = false;
-            toggleAutoRefreshButton.textContent = '暂停自动刷新';
-            toggleAutoRefreshButton.classList.remove('bg-green-500', 'hover:bg-green-600');
-            toggleAutoRefreshButton.classList.add('bg-gray-500', 'hover:bg-gray-600');
-            console.log("自动刷新已启动。");
-        }
 
-        function stopAutoRefresh() {
-            if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId);
-            autoRefreshIntervalId = null;
-            isAutoRefreshPaused = true;
-            toggleAutoRefreshButton.textContent = '恢复自动刷新';
-            toggleAutoRefreshButton.classList.remove('bg-gray-500', 'hover:bg-gray-600');
-            toggleAutoRefreshButton.classList.add('bg-green-500', 'hover:bg-green-600');
-            console.log("自动刷新已暂停。");
-        }
-
-        // Tab switching logic remains the same
-         function switchTab(activeTab, activePane, inactiveTab, inactivePane) {
-            activeTab.setAttribute('aria-selected', 'true');
-            activeTab.classList.remove('border-transparent', 'hover:text-gray-600', 'hover:border-gray-300');
-            activeTab.classList.add('border-blue-500', 'text-blue-600'); 
-
-            inactiveTab.setAttribute('aria-selected', 'false');
-            inactiveTab.classList.remove('border-blue-500', 'text-blue-600');
-            inactiveTab.classList.add('border-transparent', 'hover:text-gray-600', 'hover:border-gray-300');
-
-            activePane.classList.remove('hidden');
-            inactivePane.classList.add('hidden');
-        }
+            function renderHistoryDataTable(data) {
+                const tableContainer = document.getElementById('history-table');
+                
+                if (historyDataTable && !document.body.contains(historyDataTable.table)) {
+                    historyDataTable = null;
+                }
+                if (historyDataTable) {
+                    historyDataTable.destroy(); 
+                }
 
-        processingTab.addEventListener('click', () => switchTab(processingTab, processingPane, historyTab, historyPane));
-        historyTab.addEventListener('click', () => switchTab(historyTab, historyPane, processingTab, processingPane));
+                if (!data || data.length === 0) {
+                    tableContainer.innerHTML = '<thead><tr><th>交易对</th><th>状态</th><th>利润</th><th>创建时间</th><th>操作</th></tr></thead><tbody><tr><td colspan="5" class="text-center p-4 text-gray-500">没有历史记录。</td></tr></tbody>';
+                    historyCountBadge.textContent = 0;
+                    return;
+                }
+                
+                data.forEach(task => allTasksData[task.id] = task); 
+
+                const headings = ['交易对', '状态', '利润', '创建时间', '操作'];
+                const tableData = data.map(task => {
+                    let creationTimeForSort = task.creationTime ? String(task.creationTime).replace(/,/g, '.') : "N/A";
+                    return [
+                        task.symbol || 'N/A',
+                        `<span class="text-xs font-semibold ${getStatusTextClass(task.currentState)}">${task.currentState || 'N/A'}</span>`,
+                        task.profit !== null && !isNaN(parseFloat(task.profit)) ? parseFloat(task.profit) : null,
+                        creationTimeForSort,
+                        `<button class="text-indigo-600 hover:text-indigo-900 view-details-btn" data-task-id="${task.id}">详情</button>`
+                    ];
+                });
 
-        // MODIFIED: openModal - Basic info section uses a more structured layout (definition list style)
-        function openModal(taskId) {
-            const task = allTasksData[taskId];
-            if (!task) {
-                console.error("Task data not found for ID:", taskId);
-                modalBody.innerHTML = '<p class="text-red-500">无法加载任务详情。</p>';
-                taskDetailModal.classList.remove('hidden');
-                document.body.classList.add('modal-active');
-                return;
+                historyCountBadge.textContent = data.length;
+                
+                historyDataTable = new simpleDatatables.DataTable(tableContainer, {
+                    data: { headings, data: tableData },
+                    paging: false,
+                    perPageSelect: false,
+                    searchable: true,
+                    labels: { placeholder: "搜索...", noRows: "未找到记录" },
+                    columns: [ 
+                        { select: 2, type: 'number' },
+                        { select: 3, type: 'date', format: "YYYY-MM-DD HH:mm:ss.SSS" },
+                        { select: 4, sortable: false }
+                    ]
+                });
+                
+                historyDataTable.on('datatable.init', () => {
+                    historyDataTable.columns.sort(3, 'desc');
+                });
             }
 
-            // Improved basic info layout: using a definition list style with grid for responsiveness
-            let basicInfoHtml = '<dl class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4">'; // gap-y-4 added for more vertical space
-
-            const infoMap = new Map([
-                ["ID", task.id], // ID is often long, let it span more columns if needed.
-                ["交易对", task.symbol || 'N/A'],
-                ["当前状态", `<span class="${getStatusTextClass(task.currentState)} font-semibold">${task.currentState || 'N/A'}</span>`],
-                ["来源代币", `${task.fromTokenAmountHuman || 'N/A'} ${task.fromToken || ''}`],
-                ["目标代币", `${task.toTokenAmountHuman || 'N/A'} ${task.toToken || ''}`],
-                ["创建时间", task.creationTime || 'N/A'],
-                ["利润", task.profit || 'N/A'],
-                // ["利润阈值", task.profitLimit || 'N/A']
-            ]);
-            
-            infoMap.forEach((value, label) => {
-                let itemClass = "sm:col-span-1"; // Default span
-                if (label === "ID") { // ID can span more
-                    itemClass = "sm:col-span-2 lg:col-span-3 break-all"; // break-all for very long IDs
-                } else  if (label === "来源代币" || label === "目标代币") { // These might also be long
-                     itemClass = "sm:col-span-1 lg:col-span-1 break-words";
+            // --- Main Logic Functions ---
+            async function fetchData() {
+                try {
+                    const [processingRes, historyRes] = await Promise.all([
+                        fetch(`${API_BASE_URL}/processing`),
+                        fetch(`${API_BASE_URL}/history`)
+                    ]);
+                    if (!processingRes.ok) throw new Error(`Processing fetch failed: ${processingRes.status}`);
+                    if (!historyRes.ok) throw new Error(`History fetch failed: ${historyRes.status}`);
+                    
+                    const processingData = await processingRes.json();
+                    const historyData = await historyRes.json();
+
+                    renderProcessingTable(processingData);
+                    renderHistoryDataTable(historyData);
+                } catch (error) {
+                    console.error("获取数据失败:", error);
+                    document.getElementById('processing-list').innerHTML = `<tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败</td></tr>`;
+                    document.getElementById('history-table').innerHTML = `<tbody><tr><td colspan="5" class="text-center p-4 text-red-500">加载数据失败</td></tr></tbody>`;
+                    processingCountBadge.textContent = 'ERR';
+                    historyCountBadge.textContent = 'ERR';
                 }
-
-                basicInfoHtml += `
-                    <div class="${itemClass}">
-                        <dt class="text-xs font-medium text-gray-500 uppercase">${label}</dt>
-                        <dd class="mt-1 text-gray-900">${value}</dd>
-                    </div>
-                `;
-            });
-            basicInfoHtml += '</dl>';
-
-            modalBody.innerHTML = `
-                ${basicInfoHtml}
-                <hr class="my-4 sm:my-6">
-                <div>
-                  <h4 class="text-base sm:text-lg font-semibold text-gray-800 mb-2">状态流转:</h4>
-                  ${formatStateFlowForModal(task.stateFlow)}
-                </div>
-            `;
-            taskDetailModal.classList.remove('hidden');
-            document.body.classList.add('modal-active');
-        }
-
-        // closeModal, event listeners for modal, and initial setup remain the same
-        function closeModal() {
-            taskDetailModal.classList.add('hidden');
-            document.body.classList.remove('modal-active');
-        }
-
-        closeModalButton.addEventListener('click', closeModal);
-        taskDetailModal.addEventListener('click', (event) => { 
-            if (event.target === taskDetailModal) {
-                closeModal();
-            }
-        });
-        document.addEventListener('keydown', (event) => { 
-            if (event.key === 'Escape' && !taskDetailModal.classList.contains('hidden')) {
-                closeModal();
             }
-        });
+            function refreshAllData() { console.log("Refreshing data..."); fetchData(); }
+            function startAutoRefresh() { if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = setInterval(refreshAllData, REFRESH_INTERVAL_MS); toggleAutoRefreshButton.textContent = '暂停自动刷新'; toggleAutoRefreshButton.classList.remove('bg-green-500');toggleAutoRefreshButton.classList.add('bg-gray-500'); isAutoRefreshPaused = false; }
+            function stopAutoRefresh() { if (autoRefreshIntervalId) clearInterval(autoRefreshIntervalId); autoRefreshIntervalId = null; toggleAutoRefreshButton.textContent = '恢复自动刷新'; toggleAutoRefreshButton.classList.remove('bg-gray-500');toggleAutoRefreshButton.classList.add('bg-green-500'); isAutoRefreshPaused = true; }
+            function switchTab(activeTab) {
+                tabButtons.forEach(tab => {
+                    tab.classList.remove('border-blue-500', 'text-blue-600');
+                    tab.classList.add('border-transparent','hover:text-gray-600', 'hover:border-gray-300');
+                    const paneId = tab.id.replace('-tab', '');
+                    document.getElementById(paneId).classList.add('hidden');
+                });
+                activeTab.classList.add('border-blue-500', 'text-blue-600');
+                activeTab.classList.remove('border-transparent','hover:text-gray-600', 'hover:border-gray-300');
 
-        document.getElementById('tabContent').addEventListener('click', function(event) {
-            if (event.target.classList.contains('view-details-btn')) {
-                const taskId = event.target.dataset.taskId;
-                openModal(taskId);
+                const paneId = activeTab.id.replace('-tab', '');
+                document.getElementById(paneId).classList.remove('hidden');
             }
-        });
-
-        refreshButton.addEventListener('click', refreshAllData);
-        toggleAutoRefreshButton.addEventListener('click', () => {
-            if (isAutoRefreshPaused) {
-                startAutoRefresh();
-                refreshAllData(); 
-            } else {
-                stopAutoRefresh();
+            function openModal(taskId) {
+                const task = allTasksData[taskId];
+                if (!task) { modalBody.innerHTML = '<p class="text-red-500">无法加载任务详情。</p>'; return; }
+                let basicInfoHtml = '<dl class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-4 gap-y-3 text-xs">';
+                const infoMap = new Map([
+                    ["交易对", `<b>${task.symbol || 'N/A'}</b>`],
+                    ["状态", `<span class="font-semibold ${getStatusTextClass(task.currentState)}">${task.currentState || 'N/A'}</span>`],
+                    ["利润", task.profit !== null && !isNaN(parseFloat(task.profit)) ? `<b>${parseFloat(task.profit).toFixed(4)}</b>` : 'N/A'],
+                    ["ID", `<span class="text-gray-500 break-all">${task.id || 'N/A'}</span>`],
+                    ["创建时间", task.creationTime || 'N/A'],
+                    ["来源", `${task.fromTokenAmountHuman || ''} ${task.fromToken || ''}`],
+                    ["目标", `${task.toTokenAmountHuman || ''} ${task.toToken || ''}`],
+                ]);
+                infoMap.forEach((value, label) => {
+                    let itemClass = "sm:col-span-1";
+                    if (label === "ID") itemClass = "col-span-2 sm:col-span-3 lg:col-span-4";
+                    basicInfoHtml += `<div class="${itemClass}"><dt class="font-medium text-gray-500">${label}</dt><dd class="mt-1 text-gray-900">${value}</dd></div>`;
+                });
+                modalBody.innerHTML = `${basicInfoHtml}</dl><hr class="my-3"><h4 class="text-base font-semibold text-gray-800 mb-2">状态流转:</h4>${formatStateFlowForModal(task.stateFlow)}`;
+                taskDetailModal.classList.remove('hidden');
+                document.body.classList.add('modal-active');
             }
-        });
-
-        document.addEventListener('DOMContentLoaded', () => {
-            processingTab.setAttribute('aria-selected', 'true');
-            processingTab.classList.add('border-blue-500', 'text-blue-600');
-            processingPane.classList.remove('hidden');
-            
-            historyTab.classList.add('border-transparent', 'hover:text-gray-600', 'hover:border-gray-300');
-            historyPane.classList.add('hidden');
+            function closeModal() { taskDetailModal.classList.add('hidden'); document.body.classList.remove('modal-active'); }
+
+            // --- Event Listeners ---
+            refreshButton.addEventListener('click', refreshAllData);
+            toggleAutoRefreshButton.addEventListener('click', () => isAutoRefreshPaused ? startAutoRefresh() : stopAutoRefresh());
+            tabButtons.forEach(tab => tab.addEventListener('click', () => switchTab(tab)));
+            closeModalButton.addEventListener('click', closeModal);
+            taskDetailModal.addEventListener('click', e => (e.target === taskDetailModal) && closeModal());
+            document.addEventListener('keydown', e => (e.key === 'Escape' && !taskDetailModal.classList.contains('hidden')) && closeModal());
+            document.getElementById('tabContent').addEventListener('click', e => {
+                if (e.target?.classList.contains('view-details-btn')) {
+                    openModal(e.target.dataset.taskId);
+                }
+            });
 
+            // --- Initial Load ---
+            switchTab(document.getElementById('processing-tab'));
             refreshAllData();
             startAutoRefresh();
         });