文件
workbuddy-portal/workbuddy_portal/web/templates/tasks.html
T
wangchuanli 3751dffef9 feat: 新增备份恢复与公网加固
- 新增备份管理页与 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 配置
2026-09-18 08:46:34 +08:00

158 行
8.2 KiB
HTML
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
{% 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 %}