将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 Cookie 与 User-Agent。 新增 config.writable_by 作为唯一写权限入口,set_setting 强制全局键落到 user_id=0, 消除「管理员改了只有自己生效」的静默缺陷。新增 tools/check_docs.py 文档自检, smoke 断言扩至 215 项、check_live 扩至 122 项并支持普通账号越权验收, 忽略 backups/、data/*.bak* 与 legacy-v1/,版本升至 v1.3.0。
150 行
7.5 KiB
HTML
150 行
7.5 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>)。
|
||
保存后立即生效;已经过去且不再存在的时刻会被清掉,新的时刻当天就会接管。</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] }}"></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> 去重,不会重复计入。
|
||
区间越大耗时越长(云端按天分页拉取)。</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 %}
|