Files
胡绍康 01d1dc0874 feat: implement external API security architecture (public mode, IP whitelist, rate limit)
- Add ExternalApiGuard for IP whitelisting, rate limiting, and public mode enforcement
- Update wait-message to support async mode
- Fix bugs in verification code extraction (BUG-00014 to BUG-00017)
- Update documentation and architectural diagrams
2026-03-13 11:27:34 +08:00

539 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Outlook 邮件管理</title>
<link rel="icon" type="image/png" href="/img/ico.png">
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<div id="app">
<!-- ===== Sidebar ===== -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-logo">
<div class="logo-mark">📬</div>
<div>
<span>Outlook 邮件</span>
<small>v{{ APP_VERSION }}</small>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section">概览</div>
<button class="nav-item active" data-page="dashboard" onclick="navigate('dashboard')">
<span class="nav-icon">🏠</span><span>仪表盘</span>
</button>
<div class="nav-section">邮箱管理</div>
<button class="nav-item" data-page="mailbox" onclick="navigate('mailbox')">
<span class="nav-icon">📬</span><span>账号管理</span>
</button>
<button class="nav-item" data-page="temp-emails" onclick="navigate('temp-emails')">
<span class="nav-icon">⚡</span><span>临时邮箱</span>
</button>
<div class="nav-section">Token</div>
<button class="nav-item" data-page="refresh-log" onclick="navigate('refresh-log')">
<span class="nav-icon">🔄</span><span>刷新日志</span>
</button>
<div class="nav-section">系统</div>
<button class="nav-item" data-page="settings" onclick="navigate('settings')">
<span class="nav-icon">⚙️</span><span>系统设置</span>
</button>
<button class="nav-item" data-page="audit" onclick="navigate('audit')">
<span class="nav-icon">📋</span><span>审计日志</span>
</button>
</nav>
<div class="sidebar-bottom">
<div class="user-chip">
<div class="user-avatar">管</div>
<div class="user-chip-info">
<div class="user-chip-name">管理员</div>
<div class="user-chip-role">Outlook 管理</div>
</div>
</div>
<a href="https://github.com/ZeroPointSix/outlookEmailPlus" target="_blank" class="btn-github" title="访问 GitHub 仓库">
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span>GitHub</span>
</a>
<button class="btn-theme" id="themeToggleBtn" onclick="toggleTheme()">☀ 浅色模式</button>
<button class="btn-logout" onclick="logout()">退出登录</button>
</div>
</aside>
<!-- Mobile sidebar backdrop -->
<div class="sidebar-backdrop" id="sidebarBackdrop" onclick="closeSidebar()"></div>
<!-- ===== Content ===== -->
<div class="content">
<!-- Topbar -->
<header class="topbar" id="topbar">
<div class="topbar-left">
<button class="hamburger-btn" onclick="toggleSidebar()">☰</button>
<div>
<div class="topbar-title" id="topbarTitle">仪表盘</div>
<div class="topbar-subtitle" id="topbarSubtitle">系统概览</div>
</div>
</div>
<div id="topbar-actions"></div>
</header>
<!-- Refresh progress bar (hidden by default) -->
<div class="refresh-progress-bar" id="refreshProgressBar" style="display:none;">
<span>🔄</span>
<span class="progress-text" id="refreshProgressText">正在刷新 Token…</span>
<div class="progress-track">
<div class="progress-fill" id="refreshProgressFill" style="width:0%"></div>
</div>
<span class="progress-text" id="refreshProgressCount">0 / 0</span>
<button class="btn btn-sm btn-ghost" onclick="stopRefresh()">停止</button>
</div>
<!-- ===== Page: Dashboard ===== -->
<div class="page" id="page-dashboard">
<div class="stat-grid" id="dashboardStats">
<div class="stat-card">
<div class="stat-label">总账号数</div>
<div class="stat-value" id="statTotalAccounts">-</div>
</div>
<div class="stat-card stat-success">
<div class="stat-label">Token 有效</div>
<div class="stat-value" id="statValidTokens">-</div>
</div>
<div class="stat-card stat-danger">
<div class="stat-label">Token 过期</div>
<div class="stat-value" id="statExpiredTokens">-</div>
</div>
<div class="stat-card stat-accent">
<div class="stat-label">临时邮箱</div>
<div class="stat-value" id="statTempEmails">-</div>
</div>
</div>
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<div class="card-title">📂 分组概览</div>
</div>
<div class="card-body" style="padding:0;">
<ul class="dashboard-list" id="dashboardGroupList">
<li><span class="loading-overlay"><span class="spinner"></span> 加载中…</span></li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">🔄 最近刷新</div>
</div>
<div class="card-body" style="padding:0;">
<ul class="dashboard-list" id="dashboardRefreshList">
<li><span class="loading-overlay"><span class="spinner"></span> 加载中…</span></li>
</ul>
</div>
</div>
</div>
</div>
<!-- ===== Page: Mailbox (3-column) ===== -->
<div class="page page-hidden page-no-padding" id="page-mailbox">
<div class="mailbox-layout">
<!-- Groups Column -->
<div class="groups-column" id="groupPanel">
<div class="column-header">
<span class="column-title">分组</span>
<button class="btn-icon" onclick="showAddGroupModal()" title="添加分组">+</button>
</div>
<div class="column-search">
<input type="text" id="groupSearchInput" placeholder="搜索分组…" oninput="filterGroups(this.value)">
</div>
<div class="column-body" id="groupList">
<div class="loading-overlay"><span class="spinner"></span> 加载中…</div>
</div>
</div>
<!-- Resize Handle 1 -->
<div class="resize-handle" id="resizeHandle1" data-left="groupPanel" data-right="accountPanel"></div>
<!-- Accounts Column -->
<div class="accounts-column" id="accountPanel">
<div class="column-header">
<span class="column-title">
<span class="group-color-dot" id="currentGroupColor" style="background-color:#666;"></span>
<span id="currentGroupName">选择分组</span>
</span>
<div style="display:flex;gap:4px;align-items:center;">
<button id="registerOutlookBtn" class="btn btn-sm" onclick="showGetRefreshTokenModal()" title="通过 OAuth 获取 Token 注册 Outlook 账号" style="display:none;background-color:#0078d4;color:#fff;font-size:0.75rem;padding:3px 8px;border:none;border-radius:4px;cursor:pointer;">🔑 注册</button>
<button class="btn-icon" onclick="showTagManagementModal()" title="管理标签">🏷️</button>
<button class="btn-icon" onclick="showAddAccountModal()" title="导入账号">+</button>
</div>
</div>
<div class="column-search">
<input type="text" id="globalSearch" placeholder="搜索邮箱地址…" autocomplete="off">
</div>
<div style="padding:6px 12px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:8px;font-size:0.78rem;flex-shrink:0;">
<label style="color:var(--text-muted);font-weight:500;">排序:</label>
<button class="btn btn-sm sort-btn active" data-sort="refresh_time" onclick="sortAccounts('refresh_time')">🕐 刷新时间</button>
<button class="btn btn-sm sort-btn" data-sort="email" onclick="sortAccounts('email')">📧 邮箱名</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:4px;">
<input type="checkbox" id="selectAllCheckbox" onclick="toggleSelectAll()" style="cursor:pointer;">
<label for="selectAllCheckbox" style="font-size:0.72rem;color:var(--text-muted);cursor:pointer;">全选</label>
</div>
</div>
<div id="tagFilterContainer" style="padding:6px 12px;border-bottom:1px solid var(--border-light);display:none;flex-wrap:wrap;gap:6px;"></div>
<div class="column-body" id="accountList">
<div class="empty-state">
<span class="empty-icon">📁</span>
<p>请从左侧选择一个分组</p>
</div>
</div>
<div id="batchActionBar" class="batch-action-bar" style="display:none;">
<span id="selectedCount" style="font-size:0.78rem;color:var(--text-muted);">已选 0 项</span>
<div style="flex:1;"></div>
<button class="btn btn-sm btn-ghost" onclick="showBatchTagModal('add')">打标签</button>
<button class="btn btn-sm btn-ghost" onclick="showBatchTagModal('remove')">去标签</button>
<button class="btn btn-sm btn-ghost" onclick="showBatchMoveGroupModal()">移动分组</button>
<button class="btn btn-sm btn-danger" onclick="showBatchDeleteConfirm()">删除</button>
</div>
</div>
<!-- Resize Handle 2 -->
<div class="resize-handle" id="resizeHandle2" data-left="accountPanel" data-right="emailListPanel"></div>
<!-- Emails Column -->
<div class="emails-column" id="emailListPanel">
<div class="column-header">
<div style="display:flex;align-items:center;gap:8px;">
<div class="email-tabs" id="folderTabs" style="display:none;border:none;">
<button class="email-tab active" data-folder="inbox" onclick="switchFolder('inbox')">📨 收件箱</button>
<button class="email-tab" data-folder="junkemail" onclick="switchFolder('junkemail')">⚠️ 垃圾邮件</button>
</div>
<span style="font-size:0.82rem;color:var(--text-muted);" id="emailCount"></span>
<span id="methodTag" style="display:none;" class="badge badge-gray"></span>
</div>
<div style="display:flex;gap:4px;">
<button class="btn btn-sm btn-primary" onclick="refreshEmails()">获取邮件</button>
</div>
</div>
<!-- Current account info bar -->
<div id="currentAccountBar" style="display:none;padding:0.5rem 1rem;background:var(--bg-card);border-bottom:1px solid var(--border-light);font-size:0.82rem;">
<span id="currentAccount" onclick="copyCurrentEmail()" title="点击复制邮箱地址" style="cursor:pointer;">
当前邮箱:<strong id="currentAccountEmail"></strong>
</span>
</div>
<div class="column-body" id="emailList">
<div class="empty-state">
<span class="empty-icon">📬</span>
<p>请从左侧选择一个邮箱账号</p>
</div>
</div>
<div id="emailBatchActionBar" class="batch-action-bar" style="display:none;">
<span id="emailSelectedCount" style="font-size:0.78rem;color:var(--text-muted);">已选 0 项</span>
<div style="flex:1;"></div>
<button class="btn btn-sm btn-danger" onclick="confirmBatchDeleteEmails()">🗑️ 删除</button>
</div>
<!-- Email Detail (inline below email list) -->
<div id="emailDetailSection" style="display:none;border-top:1px solid var(--border);">
<div class="email-detail-toolbar" id="emailDetailToolbar">
<button class="btn btn-sm btn-ghost" onclick="showEmailList()">← 返回</button>
<button class="btn btn-sm btn-ghost" onclick="openFullscreenEmail()">🔍 全屏查看</button>
<button class="btn btn-sm btn-accent" onclick="extractVerificationFromDetail(this)" title="从邮件正文提取验证码/链接" style="font-size:0.72rem;padding:2px 8px;">🔑 验证码</button>
<label style="display:flex;align-items:center;gap:4px;font-size:0.78rem;margin-left:8px;cursor:pointer;">
<input type="checkbox" id="trustEmailCheckbox" onchange="toggleTrustMode(this)">
<span style="user-select:none;">信任此邮件</span>
</label>
<div style="flex:1;"></div>
<button class="btn btn-sm btn-danger" onclick="confirmDeleteCurrentEmail()">🗑️ 删除</button>
</div>
<div class="email-detail-container" id="emailDetail">
<div class="empty-state">
<span class="empty-icon">📄</span>
<p>选择一封邮件查看详情</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Page: Temp Emails ===== -->
<div class="page page-hidden page-no-padding" id="page-temp-emails">
<div class="mailbox-layout">
<!-- Temp Email List Column -->
<div class="accounts-column" id="tempEmailPanel" style="width:300px;">
<div class="column-header">
<span class="column-title">⚡ 临时邮箱</span>
<button class="btn btn-sm btn-primary" onclick="generateTempEmail()">+ 创建</button>
</div>
<div class="column-body" id="tempEmailContainer">
<div class="empty-state">
<span class="empty-icon">📭</span>
<p>暂无临时邮箱</p>
<button class="btn btn-primary" onclick="generateTempEmail()">创建第一个临时邮箱</button>
</div>
</div>
</div>
<!-- Temp Email Messages Column -->
<div class="emails-column">
<div class="column-header">
<span class="column-title" id="tempEmailCurrentName" onclick="copyTempEmailCurrent()" title="点击复制邮箱地址" style="cursor:pointer;">选择一个临时邮箱</span>
<div style="display:flex;gap:4px;">
<button class="btn btn-sm btn-ghost" id="tempEmailRefreshBtn" onclick="if(currentAccount && isTempEmailGroup) loadTempEmailMessages(currentAccount)" style="display:none;">🔄 获取邮件</button>
</div>
</div>
<div class="column-body" id="tempEmailMessageList">
<div class="empty-state">
<span class="empty-icon">📬</span>
<p>选择一个临时邮箱查看邮件</p>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Page: Refresh Log ===== -->
<div class="page page-hidden" id="page-refresh-log">
<div class="card">
<div class="card-header">
<div class="card-title">🔄 刷新日志</div>
</div>
<div class="card-body" id="refreshLogContainer">
<div class="loading-overlay"><span class="spinner"></span> 加载中…</div>
</div>
</div>
</div>
<!-- ===== Page: Settings ===== -->
<div class="page page-hidden" id="page-settings" style="padding: 1.5rem; max-width: 720px;">
<div class="card">
<div class="card-header">
<div class="card-title">⚙️ 系统设置</div>
</div>
<div class="card-body" style="padding: 1.5rem;">
<!-- 基础设置 -->
<div class="form-group">
<label class="form-label">登录密码</label>
<input type="password" class="form-input" id="settingsPassword" placeholder="输入新密码(留空则不修改)">
<div class="form-hint">用于登录系统的密码</div>
</div>
<div class="form-group">
<label class="form-label">GPTMail API Key</label>
<input type="text" class="form-input" id="settingsApiKey" placeholder="输入 GPTMail API Key">
<div class="form-hint">用于临时邮箱功能,可从 <a href="https://mail.chatgpt.org.uk/api" target="_blank" style="color: var(--clr-primary);">gptmail.io</a> 获取</div>
</div>
<div class="form-group">
<label class="form-label">对外开放 API Key</label>
<input type="password" class="form-input" id="settingsExternalApiKey" placeholder="用于 /api/external/* 的 X-API-Key" autocomplete="off">
<div class="form-hint" id="externalApiKeyHint">
用于对外开放接口鉴权(请求头:X-API-Key)。如需禁用对外开放接口,可清空后保存。
</div>
</div>
<div class="form-group">
<label class="form-label">对外开放 API 多 Key 配置(JSON)</label>
<textarea class="form-input" id="settingsExternalApiKeysJson" rows="10" placeholder='[
{
"name": "partner-a",
"api_key": "your-key",
"enabled": true,
"allowed_emails": ["user@example.com"]
}
]' style="font-family: monospace; font-size: 0.84rem;"></textarea>
<div class="form-hint" id="externalApiKeysJsonHint">
用于按调用方维护多个 Key、邮箱范围授权和启停状态。保留已有脱敏 `api_key` 表示不修改该 Key;清空后保存表示清空全部多 Key。
</div>
</div>
<!-- P1:对外 API 公网安全配置 -->
<div id="externalApiAdvancedSettings" style="margin-top: 0.75rem; padding: 1rem; background: var(--bg-secondary); border-radius: 8px; border: 1px solid var(--border-light);">
<h5 style="font-size: 0.85rem; font-weight: 600; margin-bottom: 0.75rem; color: var(--text-muted);">🛡️ 公网安全配置(P1)</h5>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="externalApiPublicMode">
<span class="form-label" style="margin: 0;">启用公网模式</span>
</label>
<div class="form-hint">关闭时(默认)仅做 API Key 鉴权;开启后额外启用 IP 白名单、限流、高风险端点禁用等安全策略。</div>
</div>
<div class="form-group">
<label class="form-label">IP 白名单 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(仅公网模式生效,留空则不限制)</span></label>
<textarea class="form-input" id="externalApiIpWhitelist" rows="2" placeholder='每行一个 IP 或 CIDR,如 192.168.1.0/24' style="font-family: monospace; font-size: 0.85rem;"></textarea>
<div class="form-hint">支持精确 IP 和 CIDR 格式。每行一个,保存时自动转为 JSON 数组。</div>
</div>
<div class="form-group">
<label class="form-label">每分钟每 IP 限流 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(仅公网模式生效)</span></label>
<input type="number" class="form-input" id="externalApiRateLimit" min="1" max="10000" value="60" style="width: 120px;">
<div class="form-hint">每个 IP 每分钟最大请求数(默认 60)。</div>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="externalApiDisableRaw">
<span class="form-label" style="margin: 0;">禁用 raw(原始内容)端点</span>
</label>
<div class="form-hint">公网模式下建议禁用,防止泄露完整邮件原文。</div>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="externalApiDisableWait">
<span class="form-label" style="margin: 0;">禁用 wait-message(等待新邮件)端点</span>
</label>
<div class="form-hint">公网模式下建议禁用,防止长连接资源耗尽(Slowloris 风险)。</div>
</div>
</div>
<!-- Token 刷新设置 -->
<div style="border-top: 1px solid var(--border-light); margin: 1.5rem 0; padding-top: 1.5rem;">
<h4 style="font-size: 0.95rem; font-weight: 600; margin-bottom: 1rem; color: var(--text);">🔄 Token 刷新设置</h4>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="enableScheduledRefresh" checked>
<span class="form-label" style="margin: 0;">启用定时刷新</span>
</label>
<div class="form-hint">关闭后将不会自动执行定时刷新任务</div>
</div>
<div class="form-group">
<label class="form-label">刷新策略</label>
<div style="display: flex; gap: 16px; margin-bottom: 12px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="radio" name="refreshStrategy" value="days" checked onchange="toggleRefreshStrategy()">
<span>按天数</span>
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="radio" name="refreshStrategy" value="cron" onchange="toggleRefreshStrategy()">
<span>Cron 表达式</span>
</label>
</div>
</div>
<div id="daysStrategyContainer">
<div class="form-group">
<label class="form-label">定时刷新周期 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(每隔 X 天自动刷新所有账号)</span></label>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number" class="form-input" id="refreshIntervalDays" min="1" max="90" value="30" placeholder="30" style="max-width: 120px;">
<span style="font-size: 0.88rem; color: var(--text-muted); font-weight: 500;">天</span>
</div>
<div class="form-hint">建议设置为 30 天,防止 Token 因 90 天不使用而过期</div>
</div>
</div>
<div id="cronStrategyContainer" style="display: none;">
<div class="form-group">
<label class="form-label">Cron 表达式</label>
<input type="text" class="form-input" id="refreshCron" placeholder="0 2 * * *" value="0 2 * * *">
<div class="form-hint">格式:分 时 日 月 星期(例如:0 2 * * * 表示每天凌晨 2:00)</div>
</div>
<div class="form-group">
<label class="form-label">常用样例</label>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px;">
<button class="btn btn-sm btn-secondary" onclick="selectCronExample('0 2 * * *')">每天凌晨 2:00</button>
<button class="btn btn-sm btn-secondary" onclick="selectCronExample('0 2 * * 1')">每周一凌晨 2:00</button>
<button class="btn btn-sm btn-secondary" onclick="selectCronExample('0 2 1 * *')">每月 1 号凌晨 2:00</button>
<button class="btn btn-sm btn-secondary" onclick="selectCronExample('0 2 */3 * *')">每 3 天凌晨 2:00</button>
<button class="btn btn-sm btn-secondary" onclick="selectCronExample('0 */12 * * *')">每 12 小时</button>
</div>
</div>
<div class="form-group">
<button class="btn btn-secondary" onclick="validateCronExpression()">验证表达式</button>
<div id="cronValidationResult" style="margin-top: 8px; font-size: 0.82rem;"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">邮箱间刷新间隔 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(每个邮箱刷新之间的等待时间)</span></label>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number" class="form-input" id="refreshDelaySeconds" min="0" max="60" value="5" placeholder="5" style="max-width: 120px;">
<span style="font-size: 0.88rem; color: var(--text-muted); font-weight: 500;">秒</span>
</div>
<div class="form-hint">建议设置为 5-10 秒,避免频繁请求触发 API 限流</div>
</div>
</div>
<!-- 自动轮询设置 -->
<div style="border-top: 1px solid var(--border-light); margin: 1.5rem 0; padding-top: 1.5rem;">
<h4 style="font-size: 0.95rem; font-weight: 600; margin-bottom: 1rem; color: var(--text);">🔔 自动轮询设置</h4>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="enableAutoPolling">
<span class="form-label" style="margin: 0;">启用自动轮询</span>
</label>
<div class="form-hint">开启后自动检查当前账号是否有新邮件</div>
</div>
<div class="form-group">
<label class="form-label">轮询间隔 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(检查新邮件的时间间隔)</span></label>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number" class="form-input" id="pollingInterval" min="5" max="300" value="10" placeholder="10" style="max-width: 120px;">
<span style="font-size: 0.88rem; color: var(--text-muted); font-weight: 500;">秒</span>
</div>
<div class="form-hint">范围:5-300 秒,建议设置为 10-30 秒</div>
</div>
<div class="form-group">
<label class="form-label">轮询次数 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(最多轮询多少次后停止)</span></label>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number" class="form-input" id="pollingCount" min="1" max="100" value="5" placeholder="5" style="max-width: 120px;">
<span style="font-size: 0.88rem; color: var(--text-muted); font-weight: 500;">次</span>
</div>
<div class="form-hint">范围:1-100 次,设置为 0 表示持续轮询</div>
</div>
</div>
<!-- Telegram 推送设置 -->
<div style="border-top: 1px solid var(--border-light); margin: 1.5rem 0; padding-top: 1.5rem;">
<h4 style="font-size: 0.95rem; font-weight: 600; margin-bottom: 1rem; color: var(--text);">📬 Telegram 推送</h4>
<div class="form-group">
<label class="form-label">Bot Token <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(从 @BotFather 获取)</span></label>
<input type="password" class="form-input" id="telegramBotToken" placeholder="输入 Bot Token" autocomplete="off">
<div class="form-hint">格式:1234567890:AAxxxxxx(留空则禁用推送)</div>
</div>
<div class="form-group">
<label class="form-label">Chat ID <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(接收推送的用户/群组 ID)</span></label>
<input type="text" class="form-input" id="telegramChatId" placeholder="输入 Chat ID" style="max-width: 300px;">
<div class="form-hint">可用 @userinfobot 获取你的 Chat ID</div>
</div>
<div class="form-group">
<label class="form-label">轮询间隔 <span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 400;">(检查新邮件的时间间隔)</span></label>
<div style="display: flex; align-items: center; gap: 8px;">
<input type="number" class="form-input" id="telegramPollInterval" min="10" max="86400" value="600" placeholder="600" style="max-width: 120px;">
<span style="font-size: 0.88rem; color: var(--text-muted); font-weight: 500;">秒</span>
</div>
<div class="form-hint">范围:60-3600 秒,默认 600 秒(10 分钟)</div>
</div>
<div class="form-group" style="margin-top: 0.5rem;">
<button class="btn btn-outline" onclick="testTelegramPush()" id="btnTestTelegram" style="font-size: 0.85rem;">📨 发送测试消息</button>
<div class="form-hint">验证 Bot Token 和 Chat ID 是否配置正确</div>
</div>
</div>
<!-- 保存按钮 -->
<div style="display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-light);">
<button class="btn btn-primary" onclick="saveSettings()">💾 保存设置</button>
</div>
</div>
</div>
</div>
<!-- ===== Page: Audit ===== -->
<div class="page page-hidden" id="page-audit">
<div class="card">
<div class="card-header">
<div class="card-title">📋 审计日志</div>
</div>
<div class="card-body" id="auditLogContainer">
<div class="loading-overlay"><span class="spinner"></span> 加载中…</div>
</div>
</div>
</div>
</div>
</div>
<!-- Toast container -->
<div id="toast-container" aria-live="polite"></div>
{% include 'partials/modals.html' %}
{% include 'partials/scripts.html' %}
</body>
</html>