文件
workbuddy-portal/workbuddy_portal/web/templates/overview.html
T
wangchuanli 1bf961f6b3 feat(权限): 收敛普通账号写权限至本人凭证
将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 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。
2026-09-18 08:46:00 +08:00

128 行
6.8 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">存档 {{ '{:,}'.format(totals.records) }} 条记录,覆盖 {{ totals.firstDay }} ~ {{ totals.lastDay }}</p>
</div>
<div class="actions">
<a class="btn" href="{{ url_for('views.dashboard') }}">打开用量大屏</a>
<button class="btn primary" id="btnCollect" type="button">立即采集一次</button>
</div>
</div>
<div id="collectMsg" class="flash" style="display:none"></div>
<div class="kpis">
<div class="kpi" style="--c:var(--cyan)">
<span>累计积分</span><b>{{ '%.2f'|format(totals.credits) }}</b>
<i>{{ totals.days }} 个活跃日 · 单次均价 {{ '%.2f'|format(totals.credits / totals.calls if totals.calls else 0) }}</i>
</div>
<div class="kpi" style="--c:var(--violet)">
<span>调用次数</span><b>{{ '{:,}'.format(totals.calls) }}</b>
<i>计费 {{ '{:,}'.format(totals.billableCalls) }} · 免费 {{ '{:,}'.format(totals.freeCalls) }}
({{ '%.1f'|format(totals.freeCalls / totals.calls * 100 if totals.calls else 0) }}%)</i>
</div>
<div class="kpi" style="--c:var(--green)">
<span>今日积分</span><b>{{ '%.2f'|format(today_stat.credits) }}</b>
<i>{{ today_stat.calls }} 次调用{% if today_stat.partial %} · 残日截至 {{ today_stat.partial.hhmm }}{% endif %}
{% if yesterday.credits %}
· 昨日整日 {{ '%.2f'|format(yesterday.credits) }}
{% endif %}</i>
</div>
<div class="kpi" style="--c:var(--amber)">
<span>近 30 天积分</span><b>{{ '%.2f'|format(stat30.credits) }}</b>
<i>{% if stat30.delta %}<span class="delta {{ 'dn' if stat30.delta.credits < 0 }}">环比 {{ '%+.1f'|format(stat30.delta.credits) }}%</span>
· 前一区间 {{ '%.0f'|format(stat30.prev.credits) }}{% else %}无可比区间{% endif %}</i>
</div>
<div class="kpi" style="--c:var(--blue)">
<span>模型 / 客户端</span><b>{{ totals.models }} / {{ totals.clients }}</b>
<i>按积分排序见下表</i>
</div>
<div class="kpi" style="--c:{{ 'var(--green)' if mf.health.cookie else 'var(--red)' }}">
<span>采集健康</span><b>{{ '正常' if mf.health.cookie else '缺 Cookie' }}</b>
<i>{% if mf.health.lastRunAt %}最近采集 {{ mf.health.lastRunAt[5:16] }}{% else %}尚无采集记录{% endif %}</i>
</div>
</div>
<div class="grid2">
<section class="card">
<h2>调度状态</h2>
<table class="kv">
<tr><th>调度线程</th><td>{% if sch.running %}<span class="tag ok">运行中</span> 每 {{ sch.interval }}s 轮询{% else %}<span class="tag bad">未运行</span>{% endif %}</td></tr>
<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 '—' }}{% if sch.catch_up %} <span class="tag">含启动补跑</span>{% endif %}</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>Cookie</th><td>{% if mf.health.cookie %}<span class="tag ok">已配置</span>{% else %}<span class="tag bad">未配置</span> <a href="{{ url_for('views.config_page') }}">去配置</a>{% endif %}</td></tr>
<tr><th>服务器时间</th><td class="mono">{{ sch.now }}</td></tr>
</table>
<p class="hint">调度在 Web 进程内运行,不再需要计划任务或外部自动化。
{% if current_user().is_admin %}所有时刻与开关都在
<a href="{{ url_for('views.tasks') }}">任务管理</a>里改。{% else %}时刻由管理员统一设定,
你可以在<a href="{{ url_for('views.tasks') }}">任务管理</a>里查看,也可以随时手动采集本人数据。{% endif %}</p>
</section>
<section class="card">
<div class="cardhead">
<h2>模型消耗 TOP</h2>
<span class="hint">按积分降序,取前 {{ models|length }} 名</span>
</div>
<div class="tablewrap">
<table class="tbl">
<thead><tr><th>模型</th><th class="num">调用</th><th class="num">积分</th><th class="num">单次均价</th><th class="num">免费占比</th></tr></thead>
<tbody>
{% for m in models %}
<tr><td>{{ m.name }}</td><td class="num">{{ m.calls }}</td>
<td class="num">{{ '%.2f'|format(m.credits) }}</td>
<td class="num">{{ '%.2f'|format(m.avgPerCall) }}</td>
<td class="num">{{ '%.0f%%'|format(m.freeRate * 100) }}</td></tr>
{% else %}
<tr><td colspan="5" class="empty">还没有数据,先点右上角「立即采集一次」</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="hint">客户端构成:{% for c in clients %}{{ c.name }} {{ '%.1f%%'|format(c.credits / totals.credits * 100 if totals.credits else 0) }}{% if not loop.last %} · {% endif %}{% else %}—{% endfor %}</p>
</section>
</div>
<section class="card">
<div class="cardhead">
<h2>最近采集</h2>
<a class="btn ghost sm" href="{{ url_for('views.tasks') }}">查看全部</a>
</div>
<div class="tablewrap">
<table class="tbl">
<thead><tr><th>#</th><th>触发</th><th>开始</th><th class="num">耗时</th><th class="num">云端</th>
<th class="num">新增</th><th class="num">重复</th><th class="num">冲突</th><th class="num">总数</th><th>结论</th></tr></thead>
<tbody>
{% for r in runs %}
<tr>
{# 运行详情在「日志管理」里,而那页仅管理员可达 —— 普通用户不要给死链 #}
<td>{% if current_user().is_admin %}<a href="{{ url_for('views.logs', run=r.id) }}">{{ r.id }}</a>{% else %}{{ r.id }}{% endif %}</td>
<td><span class="tag {{ 'info' if r.trigger=='schedule' else ('accent' if r.trigger=='startup' else 'mute') }}">{{ r.trigger }}</span></td>
<td class="mono">{{ 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="num">{{ r.fetched if r.fetched is not none else '—' }}</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 class="num">{{ r.total }}</td>
<td>{{ r.message or '—' }}</td>
</tr>
{% else %}
<tr><td colspan="10" class="empty">还没有采集记录,点右上角「立即采集一次」</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% endblock %}