Commit a845315f authored by 郭阔's avatar 郭阔

feat(panel): 新增「最近抓取」页——各源最近一次抓取的文章标题与状态

- 导航「站点登录」后新增入口 /latest:每源一张可折叠卡片,
  摘要行=源名+运行时间+✓n/✗m+已取尽标记,展开为文章标题(链接原文)+成功/失败徽标
- 数据取自各源最新运行 JSON 的业务结果(.result.articles),按运行时间倒序
parent 60ce94e1
......@@ -1288,6 +1288,46 @@ def _next_fire(times, now):
return min(cands) if cands else None
@app.route("/latest")
@require_auth
def latest_content():
"""最近抓取:每个源最近一次运行抓到的文章标题与状态,快速盘点内容。"""
skills = pl.skill_sources()
names = sorted((set(skills) | set(pl.list_schedules())) - {"digest"})
names = [n for n in names
if skills.get(n, {}).get("enabled", False)
or pl.schedule_state(n) == "active"]
cards = []
for n in names:
runs = get_history_runs(n, include_data=True)
if not runs:
cards.append({"name": n, "display": skills.get(n, {}).get("name") or n,
"run_at": None, "articles": [], "succeeded": None,
"failed": None, "exhausted": False})
continue
run = runs[0]
biz, _legacy = get_business_result(run["data"])
articles = []
if biz:
for a in biz.get("articles", []):
articles.append({
"title": a.get("title") or a.get("url", ""),
"url": a.get("url", ""),
"status": a.get("status", ""),
})
cards.append({
"name": n,
"display": skills.get(n, {}).get("name") or n,
"run_at": run["run_at"],
"articles": articles,
"succeeded": run["succeeded_count"],
"failed": run["failed_count"],
"exhausted": bool(biz.get("source_exhausted")) if biz else False,
})
cards.sort(key=lambda c: c["run_at"] or "", reverse=True)
return render_template("latest.html", cards=cards)
@app.route("/schedules")
@require_auth
def schedule_board():
......
......@@ -220,6 +220,7 @@
<a href="{{ url_for('add_source') }}">添加栏目</a>
<a href="{{ url_for('provider_list') }}">Provider</a>
<a href="{{ url_for('login_list') }}">站点登录</a>
<a href="{{ url_for('latest_content') }}">最近抓取</a>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
......
{% extends "base.html" %}
{% block title %}最近抓取 — News Harvester{% endblock %}
{% block extra_head %}
<style>
details.lcard { background:#161923; border:1px solid #2a2d37; border-radius:6px; margin:10px 0; padding:0; }
details.lcard > summary { cursor:pointer; padding:10px 14px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; list-style:none; }
details.lcard > summary::-webkit-details-marker { display:none; }
details.lcard > summary::before { content:"▸"; color:#666; }
details.lcard[open] > summary::before { content:"▾"; }
details.lcard .runinfo { color:#888; font-size:12px; }
ul.arts { margin:0; padding:6px 14px 12px 28px; }
ul.arts li { padding:4px 0; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
ul.arts li .t { flex:1 1 auto; min-width:200px; }
</style>
{% endblock %}
{% block content %}
<h1>最近抓取</h1>
<p style="color:#666;font-size:13px">
每个源最近一次运行抓到的文章标题与状态,按运行时间倒序;点击标题打开原文链接。
带已取尽标记=该源近期无新文章(正常),空列表=本次未交付(看上次运行详情排查)。
</p>
{% for c in cards %}
<details class="lcard" {% if loop.first %}open{% endif %}>
<summary>
<code>{{ c.name }}</code>
{% if c.display and c.display != c.name %}<span style="color:#888">{{ c.display }}</span>{% endif %}
<span class="runinfo">{{ c.run_at or '从未运行' }}</span>
{% if c.succeeded is not none %}
<span class="badge ok">✓ {{ c.succeeded }}</span>
{% if c.failed %}<span class="badge fail">✗ {{ c.failed }}</span>{% endif %}
{% endif %}
{% if c.exhausted %}<span class="badge unknown">已取尽</span>{% endif %}
</summary>
<ul class="arts">
{% for a in c.articles %}
<li>
<span class="t">
{% if a.url and (a.url.startswith('http://') or a.url.startswith('https://')) %}
<a class="source-link" href="{{ a.url }}" target="_blank" rel="noopener noreferrer">{{ a.title }}</a>
{% else %}{{ a.title }}{% endif %}
</span>
<span class="badge {{ 'ok' if a.status == 'SUCCESS' else 'fail' }}">{{ '成功' if a.status == 'SUCCESS' else ('失败' if a.status else '—') }}</span>
</li>
{% else %}
<li style="color:#666">本次无文章交付{% if not c.exhausted and c.succeeded is not none %}(未取尽,建议看运行详情){% endif %}</li>
{% endfor %}
</ul>
</details>
{% endfor %}
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment