- 新增备份管理页与 API:在线快照、自动周期备份、按份数清理、下载、一键恢复(恢复前自动兜底) - 新增 /profile/export,普通用户可导出本人全部数据(不含 Cookie 明文) - 修复 X-Forwarded-For 可伪造导致三道 IP 防线失效,统一走 client_ip() 取客户端地址 - 取消 admin123 硬编码默认口令,留空则生成随机初始口令并仅打印一次 - .dockerignore 排除 backups/ 并加构建期断言,防止密钥随镜像分发 - 新增会话版本号,改密/停用/删除及恢复备份后其他会话立即失效 - 新增容器资源上限、采集跨度硬顶 31 天、重操作最小间隔与并发 409 - 新增访问日志、HSTS 条件下发、口令黑名单、验证码抗模板匹配、instance.json 0600 - 版本号升至 1.4.0,同步更新 README、SECURITY、.env.example 与 compose 配置
158 行
8.2 KiB
HTML
158 行
8.2 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}任务管理 · {{ project_title }}{% endblock %}
|
||
{% block body %}
|
||
|
||
<div class="pagehead">
|
||
<div>
|
||
<h1>任务管理</h1>
|
||
<p class="lead">调度在 Web 进程内执行,采集互斥由文件锁保证;这里能看到本人账号的运行历史,也能手动触发与按区间回填</p>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn primary" id="btnCollect" type="button">立即采集一次</button>
|
||
</div>
|
||
</div>
|
||
<div id="collectMsg" class="flash" style="display:none"></div>
|
||
|
||
<div class="grid2">
|
||
<section class="card">
|
||
<div class="cardhead">
|
||
<h2>采集调度</h2>
|
||
{% if not can_edit %}<span class="tag mute">只读 · 仅管理员可改</span>{% endif %}
|
||
</div>
|
||
{% if can_edit %}
|
||
<form id="formTask">
|
||
<label class="row"><span>启用调度</span>
|
||
<select name="schedule_enabled">
|
||
<option value="1" {{ 'selected' if sch.enabled }}>启用</option>
|
||
<option value="0" {{ 'selected' if not sch.enabled }}>停用</option>
|
||
</select>
|
||
</label>
|
||
<label class="row"><span>每日时刻</span>
|
||
<input name="schedule_times" value="{{ s_times }}" placeholder="09:00,17:00" spellcheck="false">
|
||
</label>
|
||
<p class="hint">本地时区,逗号分隔,支持 <code>HH:MM</code>(也可只写 <code>9</code>)。
|
||
保存后立即生效;已经过去且不再存在的时刻会被清掉,新的时刻当天就会接管。
|
||
<br><b>最多 {{ max_slots }} 个时刻</b>(当前 {{ sch.times|length }} 个)—— 时刻数量直接决定
|
||
采集频次,是对外提供服务时控制云端压力的旋钮。需要更多请先到「配置管理」
|
||
把「每日调度时刻上限」调大。</p>
|
||
<label class="row"><span>启动补跑</span>
|
||
<select name="catch_up">
|
||
<option value="1" {{ 'selected' if sch.catch_up }}>开启(错过的时刻在宽限期内补跑)</option>
|
||
<option value="0" {{ 'selected' if not sch.catch_up }}>关闭(只在到点时执行)</option>
|
||
</select>
|
||
</label>
|
||
<label class="row"><span>补跑宽限</span>
|
||
<input name="catch_up_grace_hours" value="{{ s_grace }}" type="number" min="1" max="168">
|
||
<em class="unit">小时(超过就不补,避免开机狂刷)</em>
|
||
</label>
|
||
<button class="btn primary" type="submit">保存调度配置</button>
|
||
<p class="hint">这套时刻是<b>实例级</b>的:本机上所有账号在同一个时刻各自采集自己的数据。</p>
|
||
</form>
|
||
{% else %}
|
||
<table class="kv">
|
||
<tr><th>启用调度</th><td>{% if sch.enabled %}<span class="tag ok">已启用</span>{% else %}<span class="tag bad">已停用</span>{% endif %}</td></tr>
|
||
<tr><th>每日时刻</th><td>{{ sch.times | join(' · ') if sch.times else '—' }}</td></tr>
|
||
<tr><th>启动补跑</th><td>{% if sch.catch_up %}<span class="tag">开启</span>(宽限 {{ s_grace }} 小时){% else %}关闭{% endif %}</td></tr>
|
||
<tr><th>下次执行</th><td class="mono">{{ sch.next_run or '—' }}</td></tr>
|
||
</table>
|
||
<p class="hint">
|
||
调度时刻与采集参数由<b>管理员统一设定</b>,普通账号只读。
|
||
你随时可以在右上角点「立即采集一次」拉取本人账号的最新用量,
|
||
也可以在下面按区间补采自己的历史数据 —— 采集始终只用<b>你自己的</b> Cookie。
|
||
</p>
|
||
{% endif %}
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2>运行状态</h2>
|
||
<table class="kv">
|
||
<tr><th>服务器时间</th><td class="mono">{{ sch.now }}</td></tr>
|
||
<tr><th>调度线程</th><td>{% if sch.running %}<span class="tag ok">运行中</span>{% else %}<span class="tag bad">未运行</span>{% endif %}
|
||
<span class="hint">({{ sch.interval }}s 轮询)</span></td></tr>
|
||
<tr><th>下次执行</th><td class="mono">{{ sch.next_run or '—' }}</td></tr>
|
||
<tr><th>互斥锁</th><td>{% if sch.lock %}<span class="tag warn">有采集在跑</span>{% else %}<span class="tag ok">空闲</span>{% endif %}</td></tr>
|
||
<tr><th>最近采集</th><td>{% if sch.last %}<span class="tag {{ 'ok' if sch.last.status=='ok' else ('warn' if sch.last.status=='warn' else 'bad') }}">#{{ sch.last.id }} {{ sch.last.status }}</span>
|
||
<span class="hint">{{ sch.last.message or '' }}</span>{% else %}—{% endif %}</td></tr>
|
||
</table>
|
||
|
||
<hr class="sect-divider">
|
||
<h3>历史回填</h3>
|
||
<form id="formBackfill">
|
||
<label class="row"><span>起始日期</span>
|
||
<input type="date" name="from" max="{{ sch.now[:10] }}"
|
||
data-min-days="{{ max_days }}"></label>
|
||
<label class="row"><span>结束日期</span><input type="date" name="to" value="{{ sch.now[:10] }}" max="{{ sch.now[:10] }}"></label>
|
||
<button class="btn" type="submit">按区间补采</button>
|
||
<p class="hint">指定区间重新拉取云端明细,已存在的记录按 <code>RequestID</code> 去重,不会重复计入。
|
||
<br><b>单次最长 {{ max_days }} 天</b>(超过会被服务端拒绝,请分批补),
|
||
且<b>同一账号两次采集之间需间隔 {{ min_gap }} 秒</b>、<b>有任务在跑时不能再发起</b> ——
|
||
这三条是为了避免把云端接口与本站线程池打满。
|
||
手动点「立即采集一次」只走增量(从最后一条记录续拉),通常几秒完成。</p>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
|
||
<section class="card">
|
||
<div class="cardhead">
|
||
<h2>采集运行历史</h2>
|
||
<span class="hint">共 {{ '{:,}'.format(total) }} 次</span>
|
||
</div>
|
||
<div class="tablewrap">
|
||
<table class="tbl">
|
||
<thead><tr><th>#</th><th>触发</th><th>状态</th><th>开始</th><th class="num">耗时</th>
|
||
<th>同步区间</th><th class="num">云端</th><th class="num">新增</th><th class="num">重复</th>
|
||
<th class="num">冲突</th><th>结论</th><th></th></tr></thead>
|
||
<tbody>
|
||
{% for r in runs %}
|
||
<tr>
|
||
<td>{{ r.id }}</td>
|
||
<td><span class="tag {{ 'info' if r.trigger=='schedule' else ('accent' if r.trigger=='startup' else 'mute') }}">{{ r.trigger }}</span></td>
|
||
<td class="nowrap">
|
||
{% if r.status=='ok' %}<span class="tag ok">成功</span>
|
||
{% elif r.status=='warn' %}<span class="tag warn">有警告</span>
|
||
{% elif r.status=='running' %}<span class="tag info">进行中</span>
|
||
{% else %}<span class="tag bad">失败</span>{% endif %}
|
||
{% if r.exit_code %}<span class="tag mute">exit {{ r.exit_code }}</span>{% endif %}
|
||
</td>
|
||
<td class="mono nowrap">{{ r.started_at[5:] if r.started_at else '—' }}</td>
|
||
<td class="num">{{ ((r.duration_ms or 0) / 1000) | round(1) }}s</td>
|
||
<td class="mono">{{ r.win_from or '—' }}<br>{{ r.win_to or '' }}</td>
|
||
<td class="num">{{ r.fetched }}</td>
|
||
<td class="num">{{ r.added }}</td>
|
||
<td class="num">{{ r.dup }}</td>
|
||
<td class="num">{% if r.conflicts %}<span class="tag warn">{{ r.conflicts }}</span>{% else %}0{% endif %}</td>
|
||
<td>{{ r.message or '—' }}</td>
|
||
<td>
|
||
{% if can_edit %}<a href="{{ url_for('views.logs', run=r.id) }}">日志</a>
|
||
{% else %}<span class="muted">—</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="12" class="empty">暂无运行记录</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% if pages > 1 %}
|
||
<div class="pager">
|
||
{% if page > 1 %}<a href="{{ url_for('views.tasks', page=page-1) }}">‹</a>{% endif %}
|
||
{% for p in page_window %}
|
||
{% if p == page %}<span class="cur">{{ p }}</span>
|
||
{% else %}<a href="{{ url_for('views.tasks', page=p) }}">{{ p }}</a>{% endif %}
|
||
{% endfor %}
|
||
{% if page < pages %}<a href="{{ url_for('views.tasks', page=page+1) }}">›</a>{% endif %}
|
||
<span class="meta">第 {{ page }} / {{ pages }} 页</span>
|
||
</div>
|
||
{% endif %}
|
||
</section>
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||
<script>
|
||
WBU.bindForm('#formTask', '/api/settings');
|
||
WBU.bindForm('#formBackfill', '/api/collect', {confirmText: '确定按该区间重新采集吗?区间较大时会耗时较久。'});
|
||
</script>
|
||
{% endblock %}
|