<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>UProxier</title>
    <link href="/assets/css/bootstrap.min.css" rel="stylesheet">
    <style>
        /* 全局样式 */
        body {
            background: #f8fafc;
            min-height: 100vh;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        /* 导航栏样式 */
        .navbar {
            background: #1e293b !important;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }

        /* 顶栏图标按钮 */
        .nav-icon-btn {
            border: 0;
            background: transparent;
            color: #ffffff;
            padding: 6px 8px;
            border-radius: 8px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            opacity: 0.9;
            transition: all 0.2s ease;
        }

        .nav-icon-btn:hover {
            opacity: 1;
            background: rgba(255, 255, 255, 0.12);
        }

        .nav-icon-btn svg {
            width: 18px;
            height: 18px;
        }

        .navbar-brand svg {
            transition: transform 0.2s ease;
        }

        .navbar-brand:hover svg {
            transform: scale(1.05);
        }

        .navbar-brand {
            font-weight: 600;
        }

        /* 紧凑统计栏样式 */
        .stats-bar {
            background: white;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            padding: 0.75rem 1rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            flex-wrap: wrap;
            gap: 0.5rem;
        }

        .stats-item {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            white-space: nowrap;
        }

        .stats-label {
            font-size: 0.875rem;
            color: #64748b;
            font-weight: 500;
        }

        .stats-value {
            font-size: 1rem;
            font-weight: 600;
            color: #374151;
        }

        .stats-value.success {
            color: #10b981;
        }

        .stats-value.warning {
            color: #f59e0b;
        }

        .stats-value.danger {
            color: #ef4444;
        }

        /* 响应式调整 */
        @media (max-width: 768px) {
            .stats-bar {
                flex-direction: column;
                align-items: flex-start;
                gap: 0.75rem;
            }

            .stats-item {
                width: 100%;
                justify-content: space-between;
            }
        }

        /* 表格样式 */
        .traffic-table {
            font-size: 0.9rem;
            background: white;
            border: 1px solid #e2e8f0;
            border-radius: 12px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }

        .traffic-table .table {
            margin-bottom: 0;
        }

        .traffic-table .table-dark {
            background: #1e293b;
            border: none;
        }

        .url-cell {
            max-width: 300px;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .clickable-row {
            height: 30px;
            cursor: pointer;
            transition: all 0.2s ease;
        }

        .clickable-row:hover {
            background: #f1f5f9 !important;
        }

        .clickable-row td {
            height: 30px;
            vertical-align: middle;
            transition: all 0.2s ease;
            border: none;
            padding: 0.75rem;
        }

        .clickable-row td.url-cell {
            max-width: 300px;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
            word-break: break-all;
        }

        /* 按钮样式 */
        .btn {
            border-radius: 8px;
            font-weight: 500;
            transition: all 0.2s ease;
            border: 1px solid transparent;
            padding: 0.5rem 1rem;
        }

        .btn:hover {
            transform: translateY(-1px);
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
        }

        .btn-outline-primary {
            background: #3b82f6;
            color: white;
            border-color: #3b82f6;
        }

        .btn-outline-primary:hover {
            background: #2563eb;
            border-color: #2563eb;
            color: white;
        }

        .btn-outline-danger {
            background: #ef4444;
            color: white;
            border-color: #ef4444;
        }

        .btn-outline-danger:hover {
            background: #dc2626;
            border-color: #dc2626;
            color: white;
        }

        .btn-outline-secondary {
            background: #6b7280;
            color: white;
            border-color: #6b7280;
        }

        .btn-outline-secondary:hover {
            background: #4b5563;
            border-color: #4b5563;
            color: white;
        }

        /* 搜索框样式 */
        .form-control {
            border-radius: 8px;
            border: 1px solid #d1d5db;
            padding: 0.5rem 0.75rem;
            transition: all 0.2s ease;
            background: white;
        }

        .form-control:focus {
            border-color: #3b82f6;
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
            background: white;
        }

        /* 模态框样式 */
        .detail-modal {
            max-width: 80%;
        }

        .modal-content {
            border-radius: 12px;
            border: 1px solid #e2e8f0;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
        }

        .modal-header {
            background: #1e293b;
            color: white;
            border-radius: 12px 12px 0 0;
        }

        .modal-header .btn-close {
            filter: invert(1);
        }

        .modal-header .btn-outline-light {
            border-color: rgba(255, 255, 255, 0.3);
            color: white;
            background: rgba(255, 255, 255, 0.1);
        }

        .modal-header .btn-outline-light:hover {
            background: rgba(255, 255, 255, 0.2);
            border-color: rgba(255, 255, 255, 0.5);
            color: white;
        }

        .nav-tabs {
            border-bottom: 1px solid #e2e8f0;
        }

        .nav-tabs .nav-link {
            border: none;
            border-radius: 8px 8px 0 0;
            font-weight: 500;
            color: #6b7280;
            transition: all 0.2s ease;
        }

        .nav-tabs .nav-link.active {
            background: #3b82f6;
            color: white;
            border: none;
        }

        .nav-tabs .nav-link:hover {
            background: #f1f5f9;
            color: #374151;
        }

        /* 复制 curl 按钮样式 */
        .copy-curl-nav-btn {
            display: flex;
            align-items: center;
            padding: 0.5rem 1rem;
            font-size: 0.875rem;
            color: #6b7280;
            transition: all 0.2s ease;
            border-radius: 8px 8px 0 0;
        }

        .copy-curl-nav-btn:hover {
            background: #f1f5f9;
            color: #3b82f6;
        }

        .copy-curl-nav-btn svg {
            margin-right: 4px;
            opacity: 0.8;
        }

        .copy-curl-nav-btn:hover svg {
            opacity: 1;
        }

        .nav-tabs .ms-auto {
            margin-left: auto;
        }

        /* 内容区域样式 */
        .json-viewer {
            max-height: 300px;
            overflow-y: auto;
            overflow-x: hidden;
            background: #f8fafc;
            padding: 12px;
            border-radius: 8px;
            font-size: 0.85rem;
            border: 1px solid #e2e8f0;
            white-space: pre-wrap;
            word-break: break-all;
        }

        /* 专用于请求/响应头列表，不改变空白与布局 */
        .headers-viewer {
            max-height: 300px;
            overflow-y: auto;
            overflow-x: hidden;
            background: #f8fafc;
            padding: 10px;
            border-radius: 8px;
            font-size: 0.75rem;
            border: 1px solid #e2e8f0;
        }

        .tab-content {
            padding: 15px;
        }

        .header-item {
            margin-bottom: 4px;
        }

        .header-name {
            font-weight: 700;
            color: #1f2937;
            font-size: 0.75rem;
        }

        /* 请求头和响应头都加粗 */
        .request-headers .header-name,
        .response-headers .header-name {
            font-weight: 700;
            color: #1f2937;
        }

        /* 图片/视频预览统一容器，固定高度，确保等比缩小显示 */
        .media-preview {
            width: 100%;
            max-width: 100%;
            height: 360px; /* 固定预览高度 */
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
            border: 1px solid #eee;
            border-radius: 6px;
            background: #fafafa;
        }

        .media-preview img,
        .media-preview video {
            width: 100%;
            height: 100%;
            object-fit: contain;
            display: block;
        }

        .header-value {
            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
            background: #f3f4f6;
            padding: 2px 4px;
            border-radius: 4px;
            word-break: break-all;
            font-size: 0.7rem;
            border: 1px solid #edf2f7;
        }

        /* 高亮规则命中与延迟标记 */
        .kv-pill {
            display: inline-block;
            padding: 2px 6px;
            border-radius: 999px;
            font-weight: 600;
            font-size: 0.7rem;
        }

        .kv-pill.rule {
            background: #eef2ff;
            color: #3730a3;
            border: 1px solid #c7d2fe;
        }

        .kv-pill.delay {
            background: #ecfeff;
            color: #155e75;
            border: 1px solid #a5f3fc;
        }

        .content-preview {
            max-height: 250px;
            overflow-y: auto;
            overflow-x: hidden;
            font-size: 0.85rem;
            white-space: pre-wrap;
            word-break: break-all;
        }

        .raw-viewer {
            max-height: 320px;
            overflow-y: auto;
            overflow-x: hidden;
            background: #f8fafc;
            padding: 12px;
            border-radius: 8px;
            font-size: 0.85rem;
            border: 1px solid #e2e8f0;
            white-space: pre-wrap;
            word-break: break-all;
            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
        }

        /* 长 URL 自动换行/断行，避免溢出 */
        .url-break {
            word-break: break-all;
            overflow-wrap: anywhere;
            white-space: normal;
        }

        /* 概览信息分组卡片 */
        .section-card {
            background: #ffffff;
            border: 1px solid #e5e7eb;
            border-radius: 12px;
            padding: 12px 14px;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
        }

        .section-title {
            font-size: 0.95rem;
            font-weight: 700;
            color: #1f2937;
            margin: 0 0 10px 0;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .section-title .dot {
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background: #3b82f6;
            display: inline-block;
        }

        @media (min-width: 768px) {
            .overview-col {
                display: flex;
            }

            .overview-col .section-card {
                height: 100%;
                width: 100%;
            }
        }

        .method-badge {
            font-size: 0.75rem;
            border-radius: 8px;
            font-weight: 600;
        }

        .status-badge {
            font-size: 0.75rem;
            border-radius: 8px;
            font-weight: 600;
        }

        .resource-type-badge {
            font-size: 0.7rem;
            border-radius: 6px;
            font-weight: 500;
            padding: 2px 6px;
            margin-left: 4px;
        }

        .resource-type-badge.html { background-color: #e3f2fd; color: #1976d2; }
        .resource-type-badge.css { background-color: #f3e5f5; color: #7b1fa2; }
        .resource-type-badge.js { background-color: #fff3e0; color: #f57c00; }
        .resource-type-badge.json { background-color: #e8f5e8; color: #388e3c; }
        .resource-type-badge.image { background-color: #fce4ec; color: #c2185b; }
        .resource-type-badge.video { background-color: #e1f5fe; color: #0277bd; }
        .resource-type-badge.audio { background-color: #f3e5f5; color: #7b1fa2; }
        .resource-type-badge.font { background-color: #f1f8e9; color: #689f38; }
        .resource-type-badge.archive { background-color: #e8eaf6; color: #3f51b5; }
        .resource-type-badge.document { background-color: #f3e5f5; color: #7b1fa2; }
        .resource-type-badge.api { background-color: #fff8e1; color: #f9a825; }
        .resource-type-badge.static { background-color: #e8f5e8; color: #388e3c; }
        .resource-type-badge.other { background-color: #d0d0d0; color: #424242; }

        .modal-body {
            font-size: 0.9rem;
        }

        .modal-title {
            font-size: 1.2rem;
            font-weight: 600;
        }

        .h6 {
            font-size: 1rem;
            margin-bottom: 0.75rem;
            font-weight: 600;
            color: #374151;
        }

        .badge {
            font-size: 0.75rem;
            border-radius: 8px;
            font-weight: 600;
        }

        /* Toast 样式优化 */
        .toast-container {
            z-index: 9999;
        }

        .toast {
            min-width: 320px;
            border: none;
            border-radius: 8px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
        }

        .toast.bg-success {
            background: #10b981 !important;
            color: white;
        }

        .toast.bg-danger {
            background: #ef4444 !important;
            color: white;
        }

        .toast.bg-info {
            background: #3b82f6 !important;
            color: white;
        }

        .toast .toast-header {
            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
            background: transparent;
            color: white;
            padding: 0.75rem 1rem 0.5rem;
        }

        .toast .toast-body {
            padding: 0.5rem 1rem 0.75rem;
            font-weight: 500;
        }

        .toast .btn-close {
            filter: invert(1);
            opacity: 0.8;
        }

        .toast .btn-close:hover {
            opacity: 1;
        }

        /* 证书二维码样式：单码居中 */
        .qr-grid {
            display: flex;
            justify-content: center;
            gap: 16px;
        }

        .qr-box {
            background: #fff;
            border: 1px solid #e5e7eb;
            border-radius: 12px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 10px;
        }

        .qr-box .qr-title {
            font-size: 0.9rem;
            color: #6b7280;
        }

        .qr-box .qr-link {
            font-size: 0.8rem;
            color: #2563eb;
            text-decoration: none;
            word-break: break-all;
        }

        .qr-box .qr-link:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
<nav class="navbar navbar-dark bg-primary">
    <div class="container-fluid d-flex align-items-center justify-content-between">
        <span class="navbar-brand d-flex align-items-center">
            <svg width="35" height="35" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" class="me-2" style="flex-shrink: 0;">
                <rect width="32" height="32" rx="6" fill="#1e293b"/>
                <g fill="#3b82f6">
                    <circle cx="8" cy="16" r="4"/>
                    <circle cx="8" cy="16" r="2" fill="#ffffff"/>
                    <circle cx="24" cy="16" r="4"/>
                    <circle cx="24" cy="16" r="2" fill="#ffffff"/>
                    <rect x="12" y="14" width="12" height="4" rx="2"/>
                    <path d="M20 12 L24 16 L20 20 L22 16 Z" fill="#ffffff"/>
                </g>
                <circle cx="16" cy="8" r="1.5" fill="#10b981"/>
                <circle cx="16" cy="24" r="1.5" fill="#10b981"/>
            </svg>
            <div class="d-flex align-items-end">
                <span>UProxier</span>
                <small class="text-muted ms-2" style="font-size: 0.7rem;">v{{ version }}</small>
            </div>
        </span> 
        <div class="d-flex align-items-center gap-2">
            <div class="dropdown me-1">
                <button class="btn btn-sm btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown"
                        aria-expanded="false">导出
                </button>
                <ul class="dropdown-menu dropdown-menu-end">
                    <li><a class="dropdown-item" href="/api/export?format=json" target="_blank" rel="noopener">JSON</a>
                    </li>
                    <li><a class="dropdown-item" href="/api/export?format=jsonl" target="_blank"
                           rel="noopener">JSONL</a></li>
                    <li><a class="dropdown-item" href="/api/export?format=csv" target="_blank" rel="noopener">CSV</a>
                    </li>
                </ul>
            </div>
            <button class="nav-icon-btn" onclick="openCertQR()" data-bs-toggle="tooltip" data-bs-placement="bottom"
                    title="扫码下载证书">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                    <path d="M3 3h8v8H3V3zm2 2v4h4V5H5zm8-2h8v8h-8V3zm2 2v4h4V5h-4zM3 13h8v8H3v-8zm2 2v4h4v-4H5zm10 0h2v2h-2v-2zm4 0h2v2h-2v-2zm-4 4h2v2h-2v-2zm4 0h2v4h-4v-2h2v-2z"/>
                </svg>
            </button>
            <span class="navbar-text" id="server-status">
                    <span id="server-status-badge" class="badge bg-secondary">检测中…</span>
                </span>
        </div>
    </div>
</nav>

<div class="container-fluid mt-3">
    <!-- 紧凑统计栏 -->
    <div class="row mb-3" id="stats-bar">
        <div class="col-12">
            <div class="stats-bar">
                <div class="stats-item">
                    <span class="stats-label">总请求:</span>
                    <span class="stats-value" id="total-requests">0</span>
                </div>
                <div class="stats-item">
                    <span class="stats-label">已完成:</span>
                    <span class="stats-value success" id="completed-requests">0</span>
                </div>
                <div class="stats-item">
                    <span class="stats-label">进行中:</span>
                    <span class="stats-value warning" id="pending-requests">0</span>
                    <span class="stats-label">错误:</span>
                    <span class="stats-value danger" id="error-requests">0</span>
                </div>
            </div>
        </div>
    </div>

    <!-- 工具栏 -->
    <div class="row mb-3">
        <div class="col-md-6">
            <div class="input-group">
                <input type="text" class="form-control" id="search-input" placeholder="搜索请求...">
                <button class="btn btn-outline-secondary" type="button" onclick="searchTraffic()">搜索</button>
            </div>
        </div>
        <div class="col-md-6 text-end">
            <button class="btn btn-outline-primary" onclick="refreshData()">刷新</button>
            <button class="btn btn-outline-danger" onclick="clearTraffic()">清空</button>
        </div>
    </div>

    <!-- 流量表格 -->
    <div class="table-responsive">
        <table class="table table-striped table-hover traffic-table">
            <thead class="table-dark">
            <tr>
                <th>ID</th>
                <th>时间</th>
                <th>方法</th>
                <th>URL</th>
                <th>类型</th>
                <th>状态</th>
                <th>响应时间</th>
            </tr>
            </thead>
            <tbody id="traffic-tbody">
            </tbody>
        </table>
    </div>
</div>

<!-- 请求详情模态框 -->
<div class="modal fade" id="requestDetailModal" tabindex="-1" aria-labelledby="requestDetailModalLabel"
     aria-hidden="true">
    <div class="modal-dialog modal-lg detail-modal">
        <div class="modal-content">
            <div class="modal-header py-2">
                <h5 class="modal-title" id="requestDetailModalLabel">请求详情</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body py-2">
                <ul class="nav nav-tabs nav-tabs-sm" id="detailTabs" role="tablist">
                    <li class="nav-item" role="presentation">
                        <button class="nav-link active" id="overview-tab" data-bs-toggle="tab"
                                data-bs-target="#overview" type="button" role="tab">概览
                        </button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="request-tab" data-bs-toggle="tab" data-bs-target="#request"
                                type="button" role="tab">请求详情
                        </button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="response-tab" data-bs-toggle="tab" data-bs-target="#response"
                                type="button" role="tab">响应详情
                        </button>
                    </li>
                    <li class="nav-item ms-auto" role="presentation">
                        <button class="nav-link copy-curl-nav-btn" onclick="copyAsCurl()" id="copy-curl-btn" type="button" style="border: none; background: transparent; cursor: pointer;">
                            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" style="margin-right: 4px; vertical-align: middle;">
                                <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
                                <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
                            </svg>
                            <span>复制为 curl</span>
                        </button>
                    </li>
                </ul>
                <div class="tab-content" id="detailTabContent">
                    <div class="tab-pane fade show active" id="overview" role="tabpanel">
                        <div id="overview-content"></div>
                    </div>
                    <div class="tab-pane fade" id="request" role="tabpanel">
                        <div id="request-content"></div>
                    </div>
                    <div class="tab-pane fade" id="response" role="tabpanel">
                        <div id="response-content"></div>
                    </div>

                </div>
            </div>

        </div>
    </div>
</div>

<!-- 证书二维码模态框 -->
<div class="modal fade" id="certQRModal" tabindex="-1" aria-labelledby="certQRModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header py-2 justify-content-center">
                <h5 class="modal-title" id="certQRModalLabel">扫码下载 CA 证书</h5>
            </div>
            <div class="modal-body">
                <div class="qr-grid">
                    <div class="qr-box">
                        <div id="qr-cer"></div>
                        <a id="link-cer" href="#" target="_blank" class="qr-link"></a>
                    </div>
                    <!-- 仅保留一个二维码，无标题 -->
                </div>
                <div class="mt-3 text-muted" style="font-size: 0.85rem;">
                    请用手机扫码下载，下载后按照系统提示安装到“受信任的根证书”。
                </div>
            </div>

        </div>
    </div>
</div>

<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/qrcode.min.js"></script>

<!-- Toast 通知容器 -->
<div class="toast-container position-fixed top-0 end-0 p-3">
    <div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="toast-header">
            <strong class="me-auto" id="toast-title">通知</strong>
            <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body" id="toast-body">
            操作完成
        </div>
    </div>
</div>

<script>
    let currentData = [];
    let updateInterval;
    let currentRequestDetail = null;
    let isSearching = false;
    let searchQuery = '';

    // IndexedDB 本地缓存：仅在浏览器端使用，服务不可用时兜底展示
    const DB_NAME = 'uproxier-cache';
    const DB_VERSION = 2;
    const DB_STORE_REQUESTS = 'requests';
    const DB_STORE_META = 'meta';

    function isIDBSupported() {
        return typeof indexedDB !== 'undefined';
    }

    function openRequestDB() {
        return new Promise((resolve, reject) => {
            if (!isIDBSupported()) return resolve(null);
            const req = indexedDB.open(DB_NAME, DB_VERSION);
            req.onupgradeneeded = () => {
                const db = req.result;
                if (!db.objectStoreNames.contains(DB_STORE_REQUESTS)) {
                    db.createObjectStore(DB_STORE_REQUESTS, {keyPath: 'id'});
                }
                if (!db.objectStoreNames.contains(DB_STORE_META)) {
                    db.createObjectStore(DB_STORE_META, {keyPath: 'key'});
                }
            };
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }

    function waitTx(tx) {
        return new Promise((resolve, reject) => {
            tx.oncomplete = () => resolve();
            tx.onabort = tx.onerror = () => reject(tx.error);
        });
    }

    async function saveRequestsToDB(items) {
        if (!Array.isArray(items) || items.length === 0) return;
        try {
            const db = await openRequestDB();
            if (!db) return;
            const tx = db.transaction(DB_STORE_REQUESTS, 'readwrite');
            const store = tx.objectStore(DB_STORE_REQUESTS);
            for (const it of items) {
                if (it && it.id != null) {
                    store.put(it);
                }
            }
            await waitTx(tx);
        } catch (e) {
            console.debug('缓存请求到 IndexedDB 失败', e);
        }
    }

    async function getRequestFromDB(id) {
        try {
            const db = await openRequestDB();
            if (!db) return null;
            return await new Promise((resolve, reject) => {
                const tx = db.transaction(DB_STORE_REQUESTS, 'readonly');
                const store = tx.objectStore(DB_STORE_REQUESTS);
                const req = store.get(id);
                req.onsuccess = () => resolve(req.result || null);
                req.onerror = () => reject(req.error);
            });
        } catch (_) {
            return null;
        }
    }

    async function clearDBStores() {
        try {
            const db = await openRequestDB();
            if (!db) return;
            const tx = db.transaction([DB_STORE_REQUESTS, DB_STORE_META], 'readwrite');
            tx.objectStore(DB_STORE_REQUESTS).clear();
            tx.objectStore(DB_STORE_META).clear();
            await waitTx(tx);
        } catch (e) {
            console.debug('清空 IndexedDB 失败', e);
        }
    }

    function generateSessionId() {
        try {
            const rnd = crypto.getRandomValues(new Uint32Array(2));
            return `${Date.now()}-${rnd[0].toString(16)}-${rnd[1].toString(16)}`;
        } catch (_) {
            return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
        }
    }

    async function setSessionId(sessionId) {
        try {
            const db = await openRequestDB();
            if (!db) return;
            const tx = db.transaction(DB_STORE_META, 'readwrite');
            tx.objectStore(DB_STORE_META).put({key: 'sessionId', value: sessionId});
            await waitTx(tx);
        } catch (e) {
            console.debug('写入 sessionId 失败', e);
        }
    }

    async function getSessionId() {
        try {
            const db = await openRequestDB();
            if (!db) return null;
            return await new Promise((resolve, reject) => {
                const tx = db.transaction(DB_STORE_META, 'readonly');
                const store = tx.objectStore(DB_STORE_META);
                const req = store.get('sessionId');
                req.onsuccess = () => resolve((req.result && req.result.value) || null);
                req.onerror = () => reject(req.error);
            });
        } catch (_) {
            return null;
        }
    }

    async function ensureFreshSession() {
        await clearDBStores();

        // 生成本次会话 ID，存到 sessionStorage + IndexedDB
        const sessionId = generateSessionId();
        sessionStorage.setItem('uproxier-session', sessionId);
        await setSessionId(sessionId);
    }

    function getResourceType(url, content_type = null, method = 'GET') {
        const path = url.split('?')[0].toLowerCase();
        const extension = path.split('.').pop();
        
        if (content_type) {
            if (content_type.includes('application/json')) return { type: 'api', label: 'API' };
            if (content_type.includes('text/html')) return { type: 'html', label: 'HTML' };
            if (content_type.includes('text/css')) return { type: 'css', label: 'CSS' };
            if (content_type.includes('application/javascript') || content_type.includes('text/javascript')) return { type: 'js', label: 'JS' };
            if (content_type.includes('image/')) return { type: 'image', label: 'IMG' };
            if (content_type.includes('video/')) return { type: 'video', label: 'VIDEO' };
            if (content_type.includes('audio/')) return { type: 'audio', label: 'AUDIO' };
            if (content_type.includes('font/')) return { type: 'font', label: 'FONT' };
        }
        
        if (extension === 'html' || extension === 'htm') return { type: 'html', label: 'HTML' };
        if (extension === 'css') return { type: 'css', label: 'CSS' };
        if (extension === 'js') return { type: 'js', label: 'JS' };
        if (extension === 'json') return { type: 'json', label: 'JSON' };
        if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'ico'].includes(extension)) return { type: 'image', label: 'IMG' };
        if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v'].includes(extension)) return { type: 'video', label: 'VIDEO' };
        if (['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma'].includes(extension)) return { type: 'audio', label: 'AUDIO' };
        if (['woff', 'woff2', 'ttf', 'eot', 'otf'].includes(extension)) return { type: 'font', label: 'FONT' };
        if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) return { type: 'archive', label: 'ZIP' };
        if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(extension)) return { type: 'document', label: 'DOC' };
        
        if (path.includes('api') || path.includes('rpc') || path.includes('graphql') || path.includes('rest')) {
            return { type: 'api', label: 'API' };
        }
        
        if (path.includes('/static/') || path.includes('/assets/') || path.includes('/public/') ||
            path.includes('/css/') || path.includes('/js/') || path.includes('/images/') ||
            path.includes('/img/') || path.includes('/media/')) {
            return { type: 'static', label: 'STATIC' };
        }
        
        if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
            return { type: 'api', label: 'API' };
        }
        
        return { type: 'other', label: 'OTHER' };
    }
    let certQRModalInstance = null;

    // 显示 Toast 通知
    function showToast(title, message, type = 'success') {
        const toast = document.getElementById('toast');
        const toastTitle = document.getElementById('toast-title');
        const toastBody = document.getElementById('toast-body');

        // 设置标题和内容
        toastTitle.textContent = title;
        toastBody.textContent = message;

        // 设置样式
        toast.className = `toast ${type === 'success' ? 'bg-success text-white' : type === 'error' ? 'bg-danger text-white' : 'bg-info text-white'}`;

        // 显示 toast
        const bsToast = new bootstrap.Toast(toast);
        bsToast.show();

        // 3秒后自动隐藏
        setTimeout(() => {
            bsToast.hide();
        }, 3000);
    }

    // 页面加载完成后初始化：先清缓存并设置会话，再启动 SSE
    document.addEventListener('DOMContentLoaded', async function () {
        try {
            await ensureFreshSession();
        } catch (e) {
            console.debug('初始化本地缓存失败', e);
        }
        startHeartbeat();
        startStreams();
    });

    // 刷新数据
    async function refreshData() {
        try {
            const response = await fetch('/api/traffic?limit=100');
            const result = await response.json();
            currentData = result.data || [];
            updateTrafficTable(currentData);
            updateStats();
        } catch (error) {
            showToast('获取数据失败: ' + error.message, 'danger');
        }
    }

    function startStreams() {
        try {
            // Traffic stream
            const trafficSource = new EventSource('/api/stream/traffic');
            if (window.__onSSETrafficOpen) trafficSource.onopen = window.__onSSETrafficOpen;
            trafficSource.onmessage = (event) => {
                try {
                    const msg = JSON.parse(event.data);
                    if (msg.type === 'traffic') {
                        const items = msg.data || [];
                        if (items.length === 0) return;
                        upsertTraffic(items);
                        const filtered = isSearching ? currentData.filter(it => matchesSearch(it, searchQuery)) : currentData;
                        updateTrafficTable(filtered);
                        updateStats();
                    }
                } catch (e) {
                }
            };
            trafficSource.onerror = () => {
                if (window.__onSSETrafficError) window.__onSSETrafficError(); /* 断线由浏览器自动重连 */
            };

            // Stats stream
            const statsSource = new EventSource('/api/stream/stats');
            if (window.__onSSEStatsOpen) statsSource.onopen = window.__onSSEStatsOpen;
            statsSource.onmessage = (event) => {
                try {
                    const msg = JSON.parse(event.data);
                    if (msg.type === 'stats') {
                        const s = msg.data || {};
                        document.getElementById('total-requests').textContent = s.total_requests ?? 0;
                        document.getElementById('completed-requests').textContent = s.completed_requests ?? 0;
                        document.getElementById('error-requests').textContent = s.error_requests ?? 0;
                        document.getElementById('pending-requests').textContent = s.pending_requests ?? 0;
                    }
                } catch (e) {
                }
            };
            statsSource.onerror = () => {
                if (window.__onSSEStatsError) window.__onSSEStatsError();
            };
        } catch (e) {
            console.error(e);
        }
    }

    function upsertTraffic(items) {
        // 将同 ID 的记录合并更新，不新增重复行
        if (!Array.isArray(items) || items.length === 0) return;
        const indexById = new Map();
        for (let i = 0; i < currentData.length; i++) {
            const id = currentData[i] && currentData[i].id;
            if (id != null) indexById.set(id, i);
        }
        for (const it of items) {
            if (!it) continue;
            const id = it.id;
            if (id != null && indexById.has(id)) {
                const idx = indexById.get(id);
                // 合并字段（后到数据覆盖先前）
                currentData[idx] = {...currentData[idx], ...it};
            } else {
                currentData.push(it);
                if (id != null) indexById.set(id, currentData.length - 1);
            }
        }
        // 仅保留最近 100 条
        if (currentData.length > 100) {
            currentData = currentData.slice(-100);
        }
        saveRequestsToDB(items);
    }

    function matchesSearch(item, keyword) {
        if (!keyword) return true;
        const k = keyword.toLowerCase();
        return (
            (item.method || '').toLowerCase().includes(k) ||
            (item.url || '').toLowerCase().includes(k) ||
            String(item.status_code || '').toLowerCase().includes(k)
        );
    }

    // 打开证书下载二维码
    async function openCertQR() {
        try {
            // 优先使用 /api/hosts 返回的局域网可访问地址
            let base = window.location.origin;
            try {
                const resp = await fetch('/api/hosts', {cache: 'no-store'});
                if (resp.ok) {
                    const data = await resp.json();
                    const bases = data.bases || [];
                    // 优先选带局域网 IP 的 base
                    const preferred = data.preferred;
                    base = (bases.find(b => preferred && b.includes(preferred)) || bases[0] || base);
                }
            } catch (_) {
            }

            // 仅保留 DER(.cer) 下载
            const cerUrl = new URL('/cert/download/cer', base).toString();

            // 构建并展示模态框
            const modalEl = document.getElementById('certQRModal');
            if (!certQRModalInstance) {
                certQRModalInstance = new bootstrap.Modal(modalEl);
            }

            // 清空旧二维码
            const cerBox = document.getElementById('qr-cer');
            cerBox.innerHTML = '';

            // 生成二维码：仅 cer(DER)
            new QRCode(cerBox, {
                text: cerUrl,
                width: 180,
                height: 180,
                colorDark: '#000000',
                colorLight: '#ffffff',
                correctLevel: QRCode.CorrectLevel.M
            });

            // 同步直链文本，便于“在 Safari 打开”或复制
            const aCer = document.getElementById('link-cer');
            if (aCer) {
                aCer.href = cerUrl;
                aCer.textContent = cerUrl;
            }

            certQRModalInstance.show();
        } catch (e) {
            console.error('生成证书二维码失败:', e);
            showToast('错误', '生成证书二维码失败', 'error');
        }
    }

    // 心跳检测服务器状态改为依赖 SSE 连接状态
    function startHeartbeat() {
        const badge = document.getElementById('server-status-badge');
        // 状态由 SSE 维护：traffic 和 stats 任意一条连接 open 表示运行中；两者都 error 表示断开
        let trafficUp = false, statsUp = false;
        let wasUp = false;

        function updateBadge() {
            const up = trafficUp || statsUp;
            // 从断开 -> 连接 的跃迁：清空旧数据，等待 SSE 首帧快照
            if (!wasUp && up) {
                resetOnReconnect();
            }
            wasUp = up;
            badge.className = up ? 'badge bg-success' : 'badge bg-danger';
            badge.textContent = up ? '运行中' : '已断开';
        }

        // 在 startStreams 中会暴露 source 句柄
        window.__onSSETrafficOpen = () => {
            trafficUp = true;
            updateBadge();
        };
        window.__onSSETrafficError = () => {
            trafficUp = false;
            updateBadge();
        };
        window.__onSSEStatsOpen = () => {
            statsUp = true;
            updateBadge();
        };
        window.__onSSEStatsError = () => {
            statsUp = false;
            updateBadge();
        };
        updateBadge();
    }

    function resetOnReconnect() {
        try {
            // 记录是否有旧数据，清空本地缓存与表格，等待 SSE 首帧快照回填
            const hadData = Array.isArray(currentData) && currentData.length > 0;
            currentData = [];
            updateTrafficTable([]);
            // 先重置统计，随后由 /api/stream/stats 快速覆盖
            document.getElementById('total-requests').textContent = '0';
            document.getElementById('completed-requests').textContent = '0';
            document.getElementById('error-requests').textContent = '0';
            document.getElementById('pending-requests').textContent = '0';
            // 友好提示：仅当原本有数据时提示；只提示“已重新连接”
            if (hadData && typeof showToast === 'function') {
                try {
                    showToast('已重新连接', '', 'success');
                } catch (e) {
                }
            }
        } catch (e) { /* ignore */
        }
    }

    // 更新流量表格
    function updateTrafficTable(data) {
        const tbody = document.getElementById('traffic-tbody');
        tbody.innerHTML = '';

        data.forEach(req => {
            const row = document.createElement('tr');
            row.className = 'clickable-row';

            const statusClass = req.status === 'completed' ? 'success' :
                req.status === 'error' ? 'danger' : 'warning';
            const statusText = req.status === 'completed' ? '完成' :
                req.status === 'error' ? '错误' : '进行中';

            const responseTime = req.response_time ? `${(req.response_time * 1000).toFixed(0)}ms` : '--';
            const timestamp = new Date(req.timestamp).toLocaleTimeString();
            
            const resourceType = getResourceType(req.url, req.content_type, req.method, req.response_data);
            console.log('Resource type for', req.url, ':', resourceType);

            row.innerHTML = `
                    <td>${req.id}</td>
                    <td>${timestamp}</td>
                    <td><span class="badge bg-secondary method-badge">${req.method}</span></td>
                    <td class="url-cell" title="${req.url}">${req.url}</td>
                    <td><span class="resource-type-badge ${resourceType.type}">${resourceType.label}</span></td>
                    <td><span class="badge bg-${statusClass} status-badge">${statusText}</span></td>
                    <td>${responseTime}</td>
                `;

            tbody.appendChild(row);

            // 添加点击行的事件监听器
            row.addEventListener('click', function () {
                showRequestDetail(req.id);
            });
        });
    }

    // 显示请求详情
    async function showRequestDetail(requestId) {
        let requestDetail = null;
        let apiOk = false;
        try {
            const response = await fetch(`/api/request/${requestId}`);
            const json = await response.json();

            if (response.ok) {
                requestDetail = json;
                apiOk = true;
                // 缓存最新的详情数据
                saveRequestsToDB([requestDetail]);
            } else {
                throw new Error(json.error || '获取请求详情失败');
            }
        } catch (error) {
            console.error('获取请求详情失败:', error);
            // API 失败时从本地缓存兜底
            requestDetail = await getRequestFromDB(requestId);
            if (!requestDetail) {
                showToast('错误', '获取请求详情失败', 'error');
                return;
            }
        }

        try {
            currentRequestDetail = requestDetail;
            displayRequestDetail(requestDetail);
            const modal = new bootstrap.Modal(document.getElementById('requestDetailModal'));
            modal.show();
        } catch (e) {
            console.error('展示请求详情失败:', e);
            showToast('错误', '展示请求详情失败', 'error');
        }
    }

    // 显示请求详情内容
    function displayRequestDetail(requestDetail) {
        // 概览标签页（包含原始数据预览）
        const rawSafe = escapeHtml(JSON.stringify(requestDetail, null, 2));
        document.getElementById('overview-content').innerHTML = `
                <div class="row g-3">
                    <div class="col-md-6 overview-col">
                        <div class="section-card">
                            <div class="section-title"><span class="dot"></span> 基本信息</div>
                            <div class="row">
                                <div class="col-6"><strong>ID:</strong> ${requestDetail.id}</div>
                                <div class="col-6"><strong>方法:</strong> <span class="badge bg-secondary">${requestDetail.method}</span></div>
                            </div>
                            <div class="row">
                                <div class="col-6"><strong>协议:</strong> ${requestDetail.scheme}</div>
                                <div class="col-6"><strong>端口:</strong> ${requestDetail.port}</div>
                            </div>
                            <div class="row">
                                <div class="col-12"><strong>主机:</strong> ${requestDetail.host}</div>
                            </div>
                            <div class="row">
                                <div class="col-12"><strong>URL:</strong> <small class="text-muted url-break">${requestDetail.url}</small></div>
                            </div>
                            <div class="row">
                                <div class="col-12"><strong>时间:</strong> <small>${new Date(requestDetail.timestamp).toLocaleString()}</small></div>
                            </div>
                        </div>
                    </div>
                    <div class="col-md-6 overview-col">
                        <div class="section-card">
                            <div class="section-title"><span class="dot"></span> 状态信息</div>
                            <div class="row mb-2">
                                <div class="col-6"><strong>状态:</strong> <span class="badge bg-${requestDetail.status === 'completed' ? 'success' : requestDetail.status === 'error' ? 'danger' : 'warning'}">${requestDetail.status}</span></div>
                                ${requestDetail.response_status ? `<div class="col-6"><strong>响应:</strong> <span class="badge bg-info">${requestDetail.response_status}</span></div>` : '<div class="col-6"></div>'}
                            </div>
                            ${requestDetail.response_time ? `<div class="row mb-2"><div class="col-12"><strong>响应时间:</strong> ${(requestDetail.response_time * 1000).toFixed(0)}ms</div></div>` : ''}
                            <div class="row">
                                ${requestDetail.content_size ? `<div class="col-6"><strong>请求:</strong> ${requestDetail.content_size} bytes</div>` : '<div class="col-6"></div>'}
                                ${requestDetail.response_content_size ? `<div class="col-6"><strong>响应:</strong> ${requestDetail.response_content_size} bytes</div>` : '<div class="col-6"></div>'}
                            </div>
                        </div>
                    </div>
                </div>
                
                <div class="row g-3 mt-2">
                    <div class="col-12">
                        <div class="section-card">
                            <div class="section-title"><span class="dot"></span> 完整数据（JSON）</div>
                            <div class="raw-viewer">${rawSafe}</div>
                        </div>
                    </div>
                </div>
                
            `;

        // 请求详情：只展示最终（规则应用后）结果；若无修改则回退原始。顶部展示请求阶段命中（参考响应详情的规则/延迟展示）
        const finalReqHeaders = requestDetail.modified_headers || requestDetail.headers || {};
        let finalReqContent = (requestDetail.modified_content != null && requestDetail.modified_content !== '') ? requestDetail.modified_content : (requestDetail.content || '');
        // GET 等无请求体时，展示 URL 查询参数
        if ((!finalReqContent || finalReqContent === '') && requestDetail && requestDetail.url) {
            try {
                const u = new URL(requestDetail.url);
                if (u.search && u.search.length > 1) {
                    const params = {};
                    u.searchParams.forEach((v, k) => {
                        params[k] = v;
                    });
                    finalReqContent = JSON.stringify({query: params}, null, 2);
                }
            } catch (e) {
            }
        }
        const reqRulePills = requestDetail.request_rules_applied && requestDetail.request_rules_applied.length
            ? requestDetail.request_rules_applied.map(r => `<span class="kv-pill rule" title="命中规则">Rule: ${escapeHtml(r.trim())}</span>`).join(' ')
            : '';
        const reqMarks = reqRulePills ? `<div class="mb-2">${reqRulePills}</div>` : '';
        const modifiedUrlLine = requestDetail.modified_url ? `<div class="mb-2"><strong>改写后 URL:</strong> <small class="text-muted url-break">${escapeHtml(requestDetail.modified_url)}</small></div>` : '';
        document.getElementById('request-content').innerHTML = `
                ${reqMarks}
                ${modifiedUrlLine}
                <h6 class="mb-2">请求头</h6>
                <div class="headers-viewer request-headers">${formatHeaders(finalReqHeaders)}</div>
                <h6 class="mt-3 mb-2">请求内容</h6>
                <div class="content-preview">${formatContent(finalReqContent, requestDetail.is_binary)}</div>
            `;

        // 响应详情：只展示最终（规则应用后）结果
        if (requestDetail.response_status) {
            const rh = requestDetail.response_headers || {};
            const ruleName = rh['X-Rule-Name'] || rh['x-rule-name'];
            const delayApplied = (rh['X-Delay-Applied'] || rh['x-delay-applied']) === 'true';
            const delayMs = rh['X-Delay-Effective'] || rh['x-delay-effective'];
            // 处理规则名显示
            let rulePills = '';
            if (ruleName) {
                const rules = ruleName.split('\n').filter(rule => rule.trim());
                rulePills = rules.map(rule => 
                    `<span class="kv-pill rule" title="命中规则">Rule: ${escapeHtml(rule.trim())}</span>`
                ).join(' ');
            }
            
            const marks = `
                  ${rulePills}
                  ${delayApplied ? `<span class="kv-pill delay" title="延迟生效">Delay: ${escapeHtml(String(delayMs || ''))}ms</span>` : ''}
                `;
            document.getElementById('response-content').innerHTML = `
                    <div class="mb-2">${marks}</div>
                    <h6 class="mb-2">响应头</h6>
                    <div class="headers-viewer response-headers">${formatHeaders(requestDetail.response_headers || {})}</div>
                    <h6 class="mt-3 mb-2">响应内容</h6>
                    ${(() => {
                const url = requestDetail.response_preview_url || '';
                const mime = requestDetail.response_preview_mime || '';
                const isImg = requestDetail.is_response_image;
                const isVid = requestDetail.is_response_video;
                if (url && (isImg || isVid)) {
                    if (isImg) {
                        return `<div class="media-preview mb-2" id="media-preview"><img id="media-el" src="${url}" alt="image"/></div>`;
                    }
                    if (isVid) {
                        const typeAttr = mime ? ` type="${mime}"` : '';
                        return `<div class="media-preview mb-2" id="media-preview"><video id="media-el" controls><source src="${url}"${typeAttr}>您的浏览器不支持视频播放</video></div>`;
                    }
                }
                return `<div class="content-preview">${formatContent(requestDetail.response_content, requestDetail.is_response_binary)}</div>`;
            })()}
                `;
            // 渲染后根据宽高比设置容器类，兼容竖屏
            (function () {
                const box = document.getElementById('media-preview');
                const el = document.getElementById('media-el');
                if (!box || !el) return;
                const force = () => {
                    box.style.height = '360px';
                };
                if (el.tagName === 'VIDEO') {
                    el.addEventListener('loadedmetadata', force, {once: true});
                } else {
                    if (el.complete) force(); else el.addEventListener('load', force, {once: true});
                }
                window.addEventListener('resize', force);
            })();
        } else {
            document.getElementById('response-content').innerHTML = '<p class="text-muted">暂无响应数据</p>';
        }
    }

    // 格式化请求头
    function formatHeaders(headers) {
        if (!headers || Object.keys(headers).length === 0) {
            return '<p class="text-muted">无请求头</p>';
        }

        let html = '<div class="row">';
        let count = 0;
        for (const [name, value] of Object.entries(headers)) {
            if (count % 2 === 0 && count > 0) {
                html += '</div><div class="row">';
            }
            
            // 特殊处理 X-Rule-Name，将换行符替换为逗号
            let displayValue = value;
            if (name === 'X-Rule-Name' || name === 'x-rule-name') {
                displayValue = value.replace(/\n/g, ', ');
            }
            
            html += `<div class="col-6">
                    <div class="header-item">
                        <span class="header-name">${name}:</span>
                        <span class="header-value">${displayValue}</span>
                    </div>
                </div>`;
            count++;
        }
        html += '</div>';
        return html;
    }

    // 格式化内容
    function formatContent(content, isBinary) {
        if (!content) {
            return '<p class="text-muted">无内容</p>';
        }

        if (isBinary || content.includes('[BINARY]') || content.includes('[STREAMING]') || content.includes('[LARGE_FILE]')) {
            return `<p class="text-info">${escapeHtml(content)}</p>`;
        }

        // 尝试格式化 JSON
        try {
            const parsed = JSON.parse(content);
            const jsonText = JSON.stringify(parsed, null, 2);
            return `<pre class="json-viewer">${escapeHtml(jsonText)}</pre>`;
        } catch (e) {
            // 非 JSON：对潜在 HTML 做转义，防止样式/脚本污染页面
            const looksHtml = /<\s*(!doctype|html|head|body|script|style|div|span|a|p|h[1-6]|meta|link|title)/i.test(content);
            const safe = escapeHtml(content);
            const hint = looksHtml ? '<div class="text-muted mb-1" style="font-size:0.8rem;">HTML 内容已安全转义预览</div>' : '';
            return `${hint}<pre class="content-preview">${safe}</pre>`;
        }
    }

    // HTML 转义，避免注入影响页面样式/脚本
    function escapeHtml(str) {
        try {
            return String(str)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#39;');
        } catch (_) {
            return '';
        }
    }

    // 生成 curl 命令
    function generateCurlCommand(requestDetail) {
        if (!requestDetail) {
            return '';
        }

        const method = requestDetail.method || 'GET';
        const url = requestDetail.url || '';
        
        // 获取请求头（优先使用修改后的）
        const headers = requestDetail.modified_headers || requestDetail.headers || {};
        
        // 获取请求体（优先使用修改后的）
        let body = '';
        if (requestDetail.modified_content != null && requestDetail.modified_content !== '') {
            body = requestDetail.modified_content;
        } else if (requestDetail.content) {
            body = requestDetail.content;
        }

        let curlCmd = `curl -X ${method}`;

        const escapedUrl = url.replace(/'/g, "'\\''");
        curlCmd += ` '${escapedUrl}'`;

        const excludeHeaders = ['host', 'content-length', 'connection', 'transfer-encoding'];
        for (const [name, value] of Object.entries(headers)) {
            const headerName = String(name).toLowerCase();
            if (!excludeHeaders.includes(headerName)) {
                const escapedValue = String(value).replace(/'/g, "'\\''");
                curlCmd += ` \\\n  -H '${name}: ${escapedValue}'`;
            }
        }

        if (body && method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
            // 检查是否是二进制内容
            if (requestDetail.is_binary || body.includes('[BINARY]') || body.includes('[STREAMING]') || body.includes('[LARGE_FILE]')) {
                curlCmd += ` \\\n  --data-binary '[BINARY_DATA]'`;
            } else {
                let escapedBody = String(body);
                escapedBody = escapedBody.replace(/'/g, "'\\''");
                escapedBody = escapedBody.replace(/\n/g, '\\n');
                curlCmd += ` \\\n  --data-raw '${escapedBody}'`;
            }
        }

        return curlCmd;
    }

    // 复制为 curl 命令
    async function copyAsCurl() {
        if (!currentRequestDetail) {
            showToast('错误', '没有可用的请求详情', 'error');
            return;
        }

        try {
            const curlCmd = generateCurlCommand(currentRequestDetail);
            if (!curlCmd) {
                showToast('错误', '无法生成 curl 命令', 'error');
                return;
            }

            console.log('生成的 curl 命令:', curlCmd);
            console.log('命令长度:', curlCmd.length);

            const textArea = document.createElement('textarea');
            textArea.value = curlCmd;
            textArea.style.position = 'fixed';
            textArea.style.left = '0';
            textArea.style.top = '0';
            textArea.style.width = '2em';
            textArea.style.height = '2em';
            textArea.style.padding = '0';
            textArea.style.border = 'none';
            textArea.style.outline = 'none';
            textArea.style.boxShadow = 'none';
            textArea.style.background = 'transparent';
            textArea.setAttribute('readonly', '');
            document.body.appendChild(textArea);
            
            textArea.focus();
            textArea.select();
            textArea.setSelectionRange(0, curlCmd.length);
            
            let copied = false;
            let copyMethod = '';
            
            // 优先尝试 Clipboard API
            if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
                try {
                    await navigator.clipboard.writeText(curlCmd);
                    // 验证复制是否成功
                    try {
                        const clipboardText = await navigator.clipboard.readText();
                        if (clipboardText === curlCmd) {
                            copied = true;
                            copyMethod = 'Clipboard API';
                            console.log('使用 Clipboard API 复制成功，已验证');
                        } else {
                            console.warn('Clipboard API 复制后验证失败，内容不匹配');
                        }
                    } catch (verifyError) {
                        copied = true;
                        copyMethod = 'Clipboard API (未验证)';
                        console.log('使用 Clipboard API 复制（无法验证）');
                    }
                } catch (clipboardError) {
                    console.warn('Clipboard API 失败，尝试 execCommand:', clipboardError);
                }
            }
            
            if (!copied) {
                try {
                    // 确保 textarea 仍然被选中
                    textArea.focus();
                    textArea.select();
                    textArea.setSelectionRange(0, curlCmd.length);
                    
                    const successful = document.execCommand('copy');
                    if (successful) {
                        copied = true;
                        copyMethod = 'execCommand';
                        console.log('使用 execCommand 复制成功');
                    } else {
                        throw new Error('execCommand 返回 false');
                    }
                } catch (execError) {
                    console.error('execCommand 复制失败:', execError);
                    document.body.removeChild(textArea);
                    const modal = document.createElement('div');
                    modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:20px;border:2px solid #333;border-radius:8px;z-index:10000;max-width:90%;max-height:80%;overflow:auto;box-shadow:0 4px 20px rgba(0,0,0,0.3);';
                    modal.innerHTML = `
                        <h4 style="margin-top:0;margin-bottom:15px;">curl 命令（请手动复制）</h4>
                        <textarea readonly id="curl-cmd-textarea" style="width:100%;min-height:200px;font-family:monospace;padding:10px;border:1px solid #ccc;border-radius:4px;font-size:12px;white-space:pre-wrap;word-wrap:break-word;">${escapeHtml(curlCmd)}</textarea>
                        <div style="margin-top:15px;text-align:right;">
                            <button onclick="this.closest('div').remove()" style="padding:8px 16px;background:#3b82f6;color:white;border:none;border-radius:4px;cursor:pointer;">关闭</button>
                        </div>
                    `;
                    document.body.appendChild(modal);
                    const textarea = modal.querySelector('#curl-cmd-textarea');
                    setTimeout(() => {
                        textarea.focus();
                        textarea.select();
                    }, 100);
                    showToast('提示', '请手动复制上方显示的 curl 命令', 'info');
                    return;
                }
            }
            
            document.body.removeChild(textArea);
            
            if (copied) {
                console.log(`复制成功，使用方法: ${copyMethod}`);
                showToast('成功', 'curl 命令已复制到剪贴板', 'success');
                
                const btn = document.getElementById('copy-curl-btn');
                if (btn) {
                    const btnSpan = btn.querySelector('span');
                    if (btnSpan) {
                        const originalText = btnSpan.textContent;
                        btnSpan.textContent = '已复制！';
                        btn.style.color = '#10b981';
                        setTimeout(() => {
                            btnSpan.textContent = originalText;
                            btn.style.color = '';
                        }, 2000);
                    }
                }
            } else {
                showToast('错误', '复制失败，请检查浏览器权限', 'error');
            }
        } catch (error) {
            console.error('复制 curl 命令失败:', error);
            showToast('错误', '复制失败: ' + error.message, 'error');
        }
    }

    // 更新统计信息
    function updateStats() {
        // 统计信息现在由 SSE 流式更新，此函数不再需要手动调用
    }

    // 过滤数据
    function filterData(data, query) {
        return data.filter(req =>
            req.url.toLowerCase().includes(query) ||
            req.method.toLowerCase().includes(query) ||
            req.host.toLowerCase().includes(query)
        );
    }

    // 搜索流量
    function searchTraffic() {
        const query = document.getElementById('search-input').value.toLowerCase();
        searchQuery = query;

        if (query.trim() === '') {
            // 清空搜索
            isSearching = false;
            searchQuery = '';
            updateTrafficTable(currentData);
        } else {
            // 执行搜索
            isSearching = true;
            const filteredData = filterData(currentData, query);
            updateTrafficTable(filteredData);
        }
    }

    async function resetLocalTrafficState() {
        // 清空前端状态
        currentData = [];
        isSearching = false;
        searchQuery = '';
        document.getElementById('search-input').value = '';

        // 清空浏览器缓存（IndexedDB）并重置会话
        await clearDBStores();
        try {
            const sessionId = generateSessionId();
            sessionStorage.setItem('uproxier-session', sessionId);
            await setSessionId(sessionId);
        } catch (e) {
            console.debug('重置会话 ID 失败', e);
        }

        // 清空表格和统计
        updateTrafficTable([]);
        updateStats({
            total_requests: 0,
            completed_requests: 0,
            pending_requests: 0,
            error_requests: 0
        });
    }

    // 清空流量
    async function clearTraffic() {
        if (!confirm('确定要清空所有流量数据吗？')) return;

        let serverCleared = false;
        try {
            const response = await fetch('/api/clear', {method: 'POST'});
            const result = await response.json();
            serverCleared = !!(response.ok && result.success);
        } catch (error) {
            console.debug('服务端清空失败，转为本地清空', error);
        }

        // 无论服务端是否可达，先清本地
        await resetLocalTrafficState();

        if (serverCleared) {
            showToast('成功', '流量数据已清空！', 'success');
        } else {
            showToast('成功', '本地数据已清空', 'success');
        }
    }

    // 搜索框回车事件
    document.getElementById('search-input').addEventListener('keypress', function (e) {
        if (e.key === 'Enter') {
            searchTraffic();
        }
    });

    // 搜索框输入事件，实时搜索
    document.getElementById('search-input').addEventListener('input', function (e) {
        const query = e.target.value.toLowerCase();
        if (query.trim() === '') {
            // 清空搜索
            isSearching = false;
            searchQuery = '';
            updateTrafficTable(currentData);
        }
    });
</script>
</body>
</html>