Commit ed91787c authored by 郭阔's avatar 郭阔

feat: macOS platform adaptation, concurrency guard and detail page redesign

- web/app.py: IS_DARWIN branches (launchctl kickstart for manual trigger,
  direct execution for digest/continue without sudo); _active_harvests()
  concurrency guard on /run and /continue; normalized detail state
  (last run time from log filename, next trigger computed from schedule)
- detail.html: layout redesign — status badge header, real overview cards,
  merged duplicate run metadata, collapsible logs/config, history moved up
- gitignore: exclude .zcode/
parent 4e3e086a
...@@ -37,3 +37,4 @@ __pycache__/ ...@@ -37,3 +37,4 @@ __pycache__/
# Claude Provider 配置(含敏感 token,不提交;只保留 .example.json) # Claude Provider 配置(含敏感 token,不提交;只保留 .example.json)
config/providers/*.json config/providers/*.json
!config/providers/*.example.json !config/providers/*.example.json
.zcode/
...@@ -21,7 +21,7 @@ import re ...@@ -21,7 +21,7 @@ import re
import subprocess import subprocess
import sys import sys
import threading import threading
from datetime import datetime from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from flask import ( from flask import (
...@@ -44,6 +44,11 @@ MODIFY_SOURCE = str(WORK_DIR / "bin" / "modify-source.sh") ...@@ -44,6 +44,11 @@ MODIFY_SOURCE = str(WORK_DIR / "bin" / "modify-source.sh")
CONTINUE_SCRIPT = str(WORK_DIR / "bin" / "continue.sh") CONTINUE_SCRIPT = str(WORK_DIR / "bin" / "continue.sh")
DIGEST_SCRIPT = str(WORK_DIR / "bin" / "daily-digest.sh") DIGEST_SCRIPT = str(WORK_DIR / "bin" / "daily-digest.sh")
# ── 平台适配 ──────────────────────────────────────────────
# macOS 上调度走 launchd(标签 com.newsharvester.<source>),无 systemctl/sudo -u。
IS_DARWIN = sys.platform == "darwin"
LAUNCHD_GUI = f"gui/{os.getuid()}"
# ── Flask 应用 ───────────────────────────────────────────── # ── Flask 应用 ─────────────────────────────────────────────
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get("WEB_SECRET", "news-harvester-web-change-me") app.secret_key = os.environ.get("WEB_SECRET", "news-harvester-web-change-me")
...@@ -502,6 +507,31 @@ def run_script(cmd, timeout=30): ...@@ -502,6 +507,31 @@ def run_script(cmd, timeout=30):
return 1, "", str(e) return 1, "", str(e)
def _active_harvests():
"""返回正在运行的采集源名列表(并发保护用)。
macOS 查 launchd agent 状态;Linux 查 systemd service is-active。
单个源查询失败视为未运行,不影响页面。"""
names = []
for timer in get_timer_files():
name = timer.name[len("news-harvester@"):-len(".timer")]
if IS_DARWIN:
rc, out, _ = run_script(
["launchctl", "print", f"{LAUNCHD_GUI}/com.newsharvester.{name}"],
timeout=5,
)
if rc == 0 and "state = running" in out:
names.append(name)
else:
rc, out, _ = run_script(
["systemctl", "is-active", f"news-harvester@{name}.service"],
timeout=5,
)
if rc == 0 and out.strip() == "active":
names.append(name)
return names
# ── 路由 ─────────────────────────────────────────────────── # ── 路由 ───────────────────────────────────────────────────
@app.route("/") @app.route("/")
...@@ -578,11 +608,56 @@ def source_detail(name): ...@@ -578,11 +608,56 @@ def source_detail(name):
business_result, legacy_result = get_business_result(json_info["data"]) if json_info else (None, None) business_result, legacy_result = get_business_result(json_info["data"]) if json_info else (None, None)
history_runs = get_history_runs(name) history_runs = get_history_runs(name)
# 采集结果的运行时间:文件名编码开始时间,JSON 里有总耗时
result_time = None
result_start = None
if json_info:
m = re.search(r"\.(\d{8})\.(\d{6})\.json$", json_info["path"])
if m:
try:
start = datetime.strptime(m.group(1) + m.group(2), "%Y%m%d%H%M%S")
result_start = start.strftime("%Y-%m-%d %H:%M:%S")
result_time = "{} 开始 · 耗时 {}".format(
result_start,
format_duration_ms(json_info["data"].get("duration_ms")),
)
except ValueError:
pass
# 调度状态归一化:macOS 读 launchd,Linux 读 systemd;下次触发从排期计算兜底
agent_running = name in _active_harvests()
if agent_running:
state_label = "运行中"
elif props.get("active") == "active":
state_label = "active"
else:
state_label = "空闲"
last_trigger = result_start or props.get("last") or "-"
next_trigger = props.get("next") if props.get("next") not in (None, "-") else None
if not next_trigger:
cands = []
for t in (run_time or "").split(","):
t = t.strip()
if ":" not in t:
continue
try:
hh, mm = t.split(":")[:2]
cand = datetime.now().replace(hour=int(hh), minute=int(mm), second=0, microsecond=0)
except ValueError:
continue
if cand <= datetime.now():
cand += timedelta(days=1)
cands.append(cand)
next_trigger = min(cands).strftime("%m-%d %H:%M") if cands else "-"
return render_template("detail.html", name=name, run_time=run_time, return render_template("detail.html", name=name, run_time=run_time,
props=props, log_info=log_info, json_info=json_info, props=props, log_info=log_info, json_info=json_info,
json_meta=json_meta, conf=conf, business_result=business_result, legacy_result=legacy_result, json_meta=json_meta, conf=conf, business_result=business_result, legacy_result=legacy_result,
continue_status=continue_status, auto_refresh=auto_refresh, continue_status=continue_status, auto_refresh=auto_refresh,
providers=get_providers(), history_runs=history_runs) providers=get_providers(), history_runs=history_runs,
result_time=result_time, result_start=result_start,
agent_running=agent_running, state_label=state_label,
last_trigger=last_trigger, next_trigger=next_trigger)
@app.route("/source/<name>/history/<run_file>") @app.route("/source/<name>/history/<run_file>")
...@@ -704,9 +779,18 @@ def edit_source(name): ...@@ -704,9 +779,18 @@ def edit_source(name):
def run_source(name): def run_source(name):
if not re.match(r"^[a-zA-Z0-9_-]+$", name): if not re.match(r"^[a-zA-Z0-9_-]+$", name):
abort(404) abort(404)
rc, stdout, stderr = run_script( active = _active_harvests()
["systemctl", "start", f"news-harvester@{name}.service"], timeout=10 if active:
flash(
f"已有采集任务在运行({('、'.join(active))}),为避免并发冲突已拒绝触发,请等其完成后再试。",
"error",
) )
return redirect(url_for("source_detail", name=name))
if IS_DARWIN:
trigger = ["launchctl", "kickstart", f"{LAUNCHD_GUI}/com.newsharvester.{name}"]
else:
trigger = ["systemctl", "start", f"news-harvester@{name}.service"]
rc, stdout, stderr = run_script(trigger, timeout=10)
if rc == 0: if rc == 0:
flash(f"已触发 {name} 抓取", "success") flash(f"已触发 {name} 抓取", "success")
else: else:
...@@ -722,9 +806,10 @@ def run_digest(): ...@@ -722,9 +806,10 @@ def run_digest():
return redirect(url_for("index")) return redirect(url_for("index"))
# 后台启动 daily-digest.sh(非阻塞) # 后台启动 daily-digest.sh(非阻塞)
# 以 yichu 用户执行,确保文件权限/邮件/PATH 全部正确 # Linux 以 yichu 用户执行,确保文件权限/邮件/PATH 全部正确;macOS 直接当前用户
digest_cmd = [DIGEST_SCRIPT] if IS_DARWIN else ["sudo", "-u", "yichu", DIGEST_SCRIPT]
proc = subprocess.Popen( proc = subprocess.Popen(
["sudo", "-u", "yichu", DIGEST_SCRIPT], digest_cmd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
) )
...@@ -758,6 +843,14 @@ def continue_source(name): ...@@ -758,6 +843,14 @@ def continue_source(name):
flash(f"{name} 已有续采/修复任务在运行中,请等待完成", "error") flash(f"{name} 已有续采/修复任务在运行中,请等待完成", "error")
return redirect(url_for("source_detail", name=name)) return redirect(url_for("source_detail", name=name))
active = _active_harvests()
if active:
flash(
f"已有采集任务在运行({('、'.join(active))}),续采/修复会与其争用浏览器,已拒绝启动。",
"error",
)
return redirect(url_for("source_detail", name=name))
mode = request.form.get("mode", "resume").strip() mode = request.form.get("mode", "resume").strip()
if mode not in ("resume", "repair"): if mode not in ("resume", "repair"):
mode = "resume" mode = "resume"
...@@ -765,8 +858,9 @@ def continue_source(name): ...@@ -765,8 +858,9 @@ def continue_source(name):
provider = request.form.get("provider", "").strip() provider = request.form.get("provider", "").strip()
# 后台启动 continue.sh(非阻塞) # 后台启动 continue.sh(非阻塞)
# 以 yichu 用户执行(通过 sudo -u),确保文件权限/邮件/PATH 全部正确 # Linux 以 yichu 用户执行(通过 sudo -u);macOS 直接以当前用户执行(继承面板环境)
cmd = ["sudo", "-u", "yichu", CONTINUE_SCRIPT, name, "--mode", mode, "--yes"] cmd = ([CONTINUE_SCRIPT] if IS_DARWIN else ["sudo", "-u", "yichu", CONTINUE_SCRIPT]) \
+ [name, "--mode", mode, "--yes"]
if debug_flag: if debug_flag:
cmd.append("--debug") cmd.append("--debug")
if provider: if provider:
......
...@@ -7,7 +7,22 @@ ...@@ -7,7 +7,22 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{{ name }}</h1> {# ── 页头:源名 + 状态 + 排期 ───────────────────────────── #}
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:4px">
<h1 style="margin:0">{{ name }}</h1>
{% if agent_running %}
<span class="badge ok" style="font-size:13px">● 运行中</span>
{% elif business_result %}
<span class="badge {{ 'ok' if business_result.status == 'SUCCESS' else 'fail' }}" style="font-size:13px">
{{ '上次成功' if business_result.status == 'SUCCESS' else '上次失败' }}
</span>
{% else %}
<span class="badge unknown" style="font-size:13px">无运行记录</span>
{% endif %}
{% if run_time and run_time != '-' %}
<span style="color:#888;font-size:13px">每天 {{ run_time }} 自动采集</span>
{% endif %}
</div>
{% if continue_status and continue_status.running %} {% if continue_status and continue_status.running %}
<div class="running-banner"> <div class="running-banner">
...@@ -17,11 +32,13 @@ ...@@ -17,11 +32,13 @@
</div> </div>
{% endif %} {% endif %}
{# ── 主要操作 ──────────────────────────────────────────── #}
<div class="actions"> <div class="actions">
<a class="btn" href="{{ url_for('edit_source', name=name) }}">编辑配置</a>
<form method="POST" action="{{ url_for('run_source', name=name) }}" style="display:inline" <form method="POST" action="{{ url_for('run_source', name=name) }}" style="display:inline"
onsubmit="return confirm('确定手动触发 {{ name }} 抓取?')"> onsubmit="return confirm('确定手动触发 {{ name }} 抓取?')">
<button type="submit" class="btn btn-primary" {% if continue_status and continue_status.running %}disabled{% endif %}>手动触发</button> <button type="submit" class="btn btn-primary" {% if (continue_status and continue_status.running) or agent_running %}disabled{% endif %}>
{% if agent_running %}采集运行中…{% else %}手动触发{% endif %}
</button>
</form> </form>
<form method="POST" action="{{ url_for('continue_source', name=name) }}" style="display:inline" <form method="POST" action="{{ url_for('continue_source', name=name) }}" style="display:inline"
onsubmit="return confirmContinue('resume', '{{ name }}', '{{ conf.get("FALLBACK_PROVIDER", "") }}', '{{ conf.get("PROVIDER", "") }}')"> onsubmit="return confirmContinue('resume', '{{ name }}', '{{ conf.get("FALLBACK_PROVIDER", "") }}', '{{ conf.get("PROVIDER", "") }}')">
...@@ -49,15 +66,15 @@ ...@@ -49,15 +66,15 @@
{% if continue_status and continue_status.running and continue_status.mode == 'repair' %}修复中…{% else %}采集修复{% endif %} {% if continue_status and continue_status.running and continue_status.mode == 'repair' %}修复中…{% else %}采集修复{% endif %}
</button> </button>
</form> </form>
<script> </div>
function confirmContinue(mode, source, fallbackProvider, currentProvider) {
var selects = document.querySelectorAll('select[name="provider"]'); <script>
function confirmContinue(mode, source, fallbackProvider, currentProvider) {
var form = event.target.closest('form'); var form = event.target.closest('form');
var sel = form.querySelector('select[name="provider"]'); var sel = form.querySelector('select[name="provider"]');
var selectedProvider = sel ? sel.value : ''; var selectedProvider = sel ? sel.value : '';
var modeLabel = mode === 'repair' ? '采集修复' : '继续采集'; var modeLabel = mode === 'repair' ? '采集修复' : '继续采集';
// 没有选 provider (用默认),且没有 fallback
if (!selectedProvider && !fallbackProvider) { if (!selectedProvider && !fallbackProvider) {
var msg = '确定对 ' + source + ' 执行' + modeLabel + '?\n' + var msg = '确定对 ' + source + ' 执行' + modeLabel + '?\n' +
'将恢复上次 Claude 对话' + '将恢复上次 Claude 对话' +
...@@ -72,59 +89,49 @@ ...@@ -72,59 +89,49 @@
'3. 重新完整采集并 force 推送\n\n此操作会删除之前本次采集的文章,无法撤销!'); '3. 重新完整采集并 force 推送\n\n此操作会删除之前本次采集的文章,无法撤销!');
} }
return confirm('确定对 ' + source + ' 执行继续采集?\n将恢复上次 Claude 对话,在已有基础上继续完成中断的采集。'); return confirm('确定对 ' + source + ' 执行继续采集?\n将恢复上次 Claude 对话,在已有基础上继续完成中断的采集。');
} }
</script> </script>
<a class="btn" href="{{ url_for('index') }}">返回列表</a>
<form method="POST" action="{{ url_for('disable_source', name=name) }}" style="display:inline"
onsubmit="return confirm('确定停用栏目 {{ name }}?\n\n这将:\n1. 停止并 disable 定时器\n2. 清空运行时间(不再触发)\n\n文件保留,可随时通过编辑重新设置时间来启用。')">
<button type="submit" class="btn btn-danger" {% if continue_status and continue_status.running %}disabled{% endif %}>停用栏目</button>
</form>
</div>
<h2>Timer 信息</h2> {# ── 运行概览(合并原 Timer 信息 / 最近运行 / JSON 元数据 的高频字段) ── #}
<h2>运行概览</h2>
<div class="cards"> <div class="cards">
<div class="card"> <div class="card">
<div class="label">运行时间</div> <div class="label">状态</div>
<div class="value"><code>{{ run_time }}</code></div> <div class="value {{ 'green' if state_label in ('运行中', 'active') else '' }}">{{ state_label }}</div>
</div> </div>
<div class="card"> <div class="card">
<div class="label">状态</div> <div class="label">上次运行</div>
<div class="value"> <div class="value" style="font-size:14px">{{ last_trigger }}</div>
{% if props.active == 'active' %}
<span class="green">active</span>
{% else %}
<span class="red">{{ props.active }}</span>
{% endif %}
</div> </div>
<div class="card">
<div class="label">下次触发</div>
<div class="value" style="font-size:14px">{{ next_trigger }}</div>
</div> </div>
{% if log_info %}
<div class="card"> <div class="card">
<div class="label">上次触发</div> <div class="label">上次成本</div>
<div class="value" style="font-size:14px">{{ props.last }}</div> <div class="value">{{ log_info.get('cost', '-') }}</div>
</div> </div>
<div class="card"> <div class="card">
<div class="label">下次触发</div> <div class="label">上次耗时</div>
<div class="value" style="font-size:14px">{{ props.next }}</div> <div class="value" style="font-size:14px">{{ log_info.get('duration', '-') }}</div>
</div> </div>
<div class="card">
<div class="label">Provider</div>
<div class="value" style="font-size:14px">{{ log_info.get('provider', '-') }}</div>
</div>
{% endif %}
</div> </div>
{% if log_info and log_info.get('session') %}
{% if conf %} <p style="color:#666;font-size:12px;margin:6px 0 0">Session: <span class="session-id">{{ log_info.session }}</span>
<h2>栏目配置 ({{ name }}.conf)</h2> {% if log_info.get('turns') %} · Turns {{ log_info.get('turns') }}{% endif %}
<div class="table-wrap"> {% if json_meta and json_meta.get('duration_api') %} · API {{ json_meta.duration_api }} ms{% endif %}
<table> </p>
<tbody>
{% for k, v in conf.items() %}
<tr><td><code>{{ k }}</code></td><td>{{ v }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<h2>栏目配置</h2>
<p style="color:#666">无独立配置,继承 <code>default.conf</code></p>
{% endif %} {% endif %}
{# ── 采集结果 ──────────────────────────────────────────── #}
{% if business_result %} {% if business_result %}
<h2>采集结果</h2> <h2>采集结果{% if result_time %} <span style="font-size:13px;color:#666;font-weight:normal">{{ result_time }}</span>{% endif %}</h2>
<div class="cards"> <div class="cards">
<div class="card"><div class="label">结果状态</div><div class="value {{ 'green' if business_result.status == 'SUCCESS' else 'red' }}">{{ '成功' if business_result.status == 'SUCCESS' else '失败' }}</div></div> <div class="card"><div class="label">结果状态</div><div class="value {{ 'green' if business_result.status == 'SUCCESS' else 'red' }}">{{ '成功' if business_result.status == 'SUCCESS' else '失败' }}</div></div>
<div class="card"><div class="label">成功 / 失败 / 目标</div><div class="value">{{ business_result.succeeded_count }} / {{ business_result.failed_count }} / {{ business_result.requested_count }}</div></div> <div class="card"><div class="label">成功 / 失败 / 目标</div><div class="value">{{ business_result.succeeded_count }} / {{ business_result.failed_count }} / {{ business_result.requested_count }}</div></div>
...@@ -167,98 +174,67 @@ ...@@ -167,98 +174,67 @@
<pre>{{ legacy_result }}</pre> <pre>{{ legacy_result }}</pre>
{% endif %} {% endif %}
{% if log_info %} {# ── 历史执行记录(高频,提前) ──────────────────────────── #}
<h2>最近运行</h2> <h2>历史执行记录</h2>
<div class="cards"> {% if history_runs %}
<div class="card"> <div class="table-wrap">
<div class="label">状态</div> <table>
<div class="value <thead>
{% if '成功' in log_info.get('status', '') %}green <tr><th>运行时间</th><th>类型</th><th>状态</th><th>文章(成功 / 失败 / 目标)</th><th>Provider</th><th>耗时</th><th></th></tr>
{% elif '失败' in log_info.get('status', '') %}red{% endif %}"> </thead>
{{ log_info.get('status', '?') }} <tbody>
</div> {% for run in history_runs %}
</div> <tr>
<div class="card"> <td><code>{{ run.run_at }}</code></td>
<div class="label">成本</div> <td>{{ run.mode }}</td>
<div class="value">{{ log_info.get('cost', '-') }}</div> <td><span class="badge {{ 'ok' if run.status == 'SUCCESS' else 'fail' if run.status == 'FAILED' else 'unknown' }}">
</div> {{ '成功' if run.status == 'SUCCESS' else '失败' if run.status == 'FAILED' else '旧版记录' if run.status == 'LEGACY' else run.status }}
<div class="card"> </span></td>
<div class="label">耗时</div> <td>{% if run.requested_count is not none %}{{ run.succeeded_count }} / {{ run.failed_count }} / {{ run.requested_count }}{% else %}—{% endif %}</td>
<div class="value" style="font-size:14px">{{ log_info.get('duration', '-') }}</div> <td>{{ run.provider }}</td>
</div> <td>{{ run.duration }}</td>
<div class="card"> <td><a class="btn btn-sm" href="{{ url_for('source_history_detail', name=name, run_file=run.filename) }}">查看</a></td>
<div class="label">Turns</div> </tr>
<div class="value">{{ log_info.get('turns', '-') }}</div> {% endfor %}
</div> </tbody>
<div class="card"> </table>
<div class="label">Provider</div>
<div class="value" style="font-size:14px">{{ log_info.get('provider', '-') }}</div>
</div>
<div class="card">
<div class="label">修改时间</div>
<div class="value" style="font-size:14px">{{ log_info.get('mtime', '?') }}</div>
</div>
</div> </div>
{% else %}
<p style="color:#666">暂无历史执行记录</p>
{% endif %}
{% if log_info.get('session') %} {# ── 次要操作 ──────────────────────────────────────────── #}
<div class="card" style="margin-bottom:16px"> <div class="actions" style="margin-top:16px">
<div class="label">Claude Session ID</div> <a class="btn" href="{{ url_for('edit_source', name=name) }}">编辑配置</a>
<div class="value" style="font-size:13px"> <form method="POST" action="{{ url_for('disable_source', name=name) }}" style="display:inline"
<span class="session-id">{{ log_info.session }}</span> onsubmit="return confirm('确定停用栏目 {{ name }}?\n\n这将:\n1. 停止并 disable 定时器\n2. 清空运行时间(不再触发)\n\n文件保留,可随时通过编辑重新设置时间来启用。')">
</div> <button type="submit" class="btn btn-danger">停用栏目</button>
</form>
<a class="btn" href="{{ url_for('index') }}">返回列表</a>
</div> </div>
{% endif %}
<h2>运行日志 (.last.log)</h2> {# ── 折叠区:日志 / Claude 明细 / 配置 ───────────────────── #}
{% if log_info %}
<details style="margin-top:16px">
<summary><strong>运行日志 (.last.log)</strong></summary>
<pre>{{ log_info.raw }}</pre> <pre>{{ log_info.raw }}</pre>
{% else %} </details>
<h2>最近运行</h2>
<p style="color:#666">暂无运行记录</p>
{% endif %} {% endif %}
{% if json_meta %} {% if json_meta %}
<h2>Claude JSON 元数据</h2> <details style="margin-top:8px">
<div class="cards"> <summary><strong>Claude 运行明细</strong></summary>
<div class="card"> <div class="cards" style="margin-top:8px">
<div class="label">总成本</div> <div class="card"><div class="label">is_error</div><div class="value {{ 'green' if not json_meta.is_error else 'red' }}">{{ json_meta.is_error }}</div></div>
<div class="value">${{ json_meta.cost }}</div> <div class="card"><div class="label">Terminal</div><div class="value" style="font-size:14px">{{ json_meta.terminal }}</div></div>
</div> <div class="card"><div class="label">Turns</div><div class="value">{{ json_meta.turns }}</div></div>
<div class="card"> <div class="card"><div class="label">API 耗时</div><div class="value" style="font-size:14px">{{ json_meta.duration_api }} ms</div></div>
<div class="label">Turns</div>
<div class="value">{{ json_meta.turns }}</div>
</div>
<div class="card">
<div class="label">API 耗时</div>
<div class="value" style="font-size:14px">{{ json_meta.duration_api }} ms</div>
</div>
<div class="card">
<div class="label">is_error</div>
<div class="value {% if not json_meta.is_error %}green{% else %}red{% endif %}">
{{ json_meta.is_error }}
</div>
</div>
<div class="card">
<div class="label">Terminal</div>
<div class="value" style="font-size:14px">{{ json_meta.terminal }}</div>
</div>
</div> </div>
{% if json_meta.session_id and json_meta.session_id != '-' %}
<div class="card" style="margin-bottom:16px">
<div class="label">Session ID</div>
<div class="value" style="font-size:13px">
<span class="session-id">{{ json_meta.session_id }}</span>
</div>
</div>
{% endif %}
{% if json_meta.usage %} {% if json_meta.usage %}
<h3>Token 用量</h3> <h3>Token 用量</h3>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead><tr><th>类型</th><th>Token 数</th></tr></thead>
<tr><th>类型</th><th>Token 数</th></tr>
</thead>
<tbody> <tbody>
{% for k, v in json_meta.usage.items() %} {% for k, v in json_meta.usage.items() %}
<tr><td><code>{{ k }}</code></td><td>{{ v }}</td></tr> <tr><td><code>{{ k }}</code></td><td>{{ v }}</td></tr>
...@@ -267,41 +243,29 @@ ...@@ -267,41 +243,29 @@
</table> </table>
</div> </div>
{% endif %} {% endif %}
{% if json_meta.permission_denials %} {% if json_meta.permission_denials %}
<h3 style="color:#f44747">权限拒绝 ({{ json_meta.permission_denials|length }} 次)</h3> <h3 style="color:#f44747">权限拒绝 ({{ json_meta.permission_denials|length }} 次)</h3>
<pre>{{ json_meta.permission_denials | tojson(indent=2) }}</pre> <pre>{{ json_meta.permission_denials | tojson(indent=2) }}</pre>
{% endif %} {% endif %}
<p style="color:#666;font-size:12px;margin-top:8px">来源: {{ json_info.path }}</p> <p style="color:#666;font-size:12px;margin-top:8px">来源: {{ json_info.path }}</p>
</details>
{% endif %} {% endif %}
<h2>历史执行记录</h2> <details style="margin-top:8px">
{% if history_runs %} <summary><strong>栏目配置{% if conf %} ({{ name }}.conf){% endif %}</strong></summary>
<div class="table-wrap"> {% if conf %}
<div class="table-wrap" style="margin-top:8px">
<table> <table>
<thead>
<tr><th>运行时间</th><th>类型</th><th>状态</th><th>文章(成功 / 失败 / 目标)</th><th>Provider</th><th>耗时</th><th></th></tr>
</thead>
<tbody> <tbody>
{% for run in history_runs %} {% for k, v in conf.items() %}
<tr> <tr><td><code>{{ k }}</code></td><td>{{ v }}</td></tr>
<td><code>{{ run.run_at }}</code></td>
<td>{{ run.mode }}</td>
<td><span class="badge {{ 'ok' if run.status == 'SUCCESS' else 'fail' if run.status == 'FAILED' else 'unknown' }}">
{{ '成功' if run.status == 'SUCCESS' else '失败' if run.status == 'FAILED' else '旧版记录' if run.status == 'LEGACY' else run.status }}
</span></td>
<td>{% if run.requested_count is not none %}{{ run.succeeded_count }} / {{ run.failed_count }} / {{ run.requested_count }}{% else %}—{% endif %}</td>
<td>{{ run.provider }}</td>
<td>{{ run.duration }}</td>
<td><a class="btn btn-sm" href="{{ url_for('source_history_detail', name=name, run_file=run.filename) }}">查看</a></td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %} {% else %}
<p style="color:#666">暂无可读取的历史执行记录</p> <p style="color:#666;margin-top:8px">无独立配置,继承 <code>default.conf</code></p>
{% endif %} {% endif %}
</details>
{% endblock %} {% 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