v1.0.1: 弹窗防误关确认/布局修复/定时任务首次执行时间/日间夜间双主题/总体统计区
This commit is contained in:
@@ -38,6 +38,33 @@ def now_str():
|
||||
return store.now_str()
|
||||
|
||||
|
||||
def _parse_first_run(s):
|
||||
"""解析表单提交的首次执行时间 (datetime-local 格式), 非法返回 None"""
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(str(s), "%Y-%m-%dT%H:%M")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _schedule_next_run(sch):
|
||||
"""根据调度配置 + 首次执行时间计算 next_run (str)"""
|
||||
first_run = _parse_first_run(sch.get("first_run"))
|
||||
base = first_run if (first_run and first_run > datetime.now()) else None
|
||||
if sch.get("type") == "cron":
|
||||
expr = sch.get("cron") or "0 * * * *"
|
||||
nn = cron_next(expr, base or datetime.now())
|
||||
if not nn:
|
||||
raise ValueError("cron 表达式在未来一年内无匹配时间")
|
||||
return nn.strftime("%Y-%m-%d %H:%M:%S")
|
||||
sch.setdefault("interval_unit", "hours")
|
||||
sch.setdefault("interval_value", 24)
|
||||
if base:
|
||||
return base.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return (datetime.now() + interval_delta(sch)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def resolve_out_dir(task):
|
||||
cfg = task.get("config", {}) or {}
|
||||
if cfg.get("out_dir", "").strip():
|
||||
@@ -161,16 +188,7 @@ def api_create_task():
|
||||
sch.setdefault("enabled", True)
|
||||
sch.setdefault("type", "interval")
|
||||
try:
|
||||
if sch.get("type") == "cron":
|
||||
expr = sch.get("cron") or "0 * * * *"
|
||||
nn = cron_next(expr)
|
||||
if not nn:
|
||||
raise ValueError("cron 表达式在未来一年内无匹配时间")
|
||||
sch["next_run"] = nn.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
sch.setdefault("interval_unit", "hours")
|
||||
sch.setdefault("interval_value", 24)
|
||||
sch["next_run"] = (datetime.now() + interval_delta(sch)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
sch["next_run"] = _schedule_next_run(sch)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": f"调度配置错误: {e}"}), 400
|
||||
sch.setdefault("last_run", "")
|
||||
@@ -221,13 +239,7 @@ def api_update_task(tid):
|
||||
if "schedule" in body and task.get("mode") == "scheduled":
|
||||
sch = {**task.get("schedule", {}), **body["schedule"]}
|
||||
try:
|
||||
if sch.get("type") == "cron":
|
||||
nn = cron_next(sch.get("cron") or "0 * * * *")
|
||||
if not nn:
|
||||
raise ValueError("cron 表达式在未来一年内无匹配时间")
|
||||
sch["next_run"] = nn.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
sch["next_run"] = (datetime.now() + interval_delta(sch)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
sch["next_run"] = _schedule_next_run(sch)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": f"调度配置错误: {e}"}), 400
|
||||
task["schedule"] = sch
|
||||
@@ -285,6 +297,46 @@ def api_resume(tid):
|
||||
return jsonify({"error": "任务未在运行"}), 409
|
||||
|
||||
|
||||
# ---------------- API: 统计 ----------------
|
||||
|
||||
@app.route("/api/stats")
|
||||
def api_stats():
|
||||
tasks = store.load_tasks()
|
||||
total_runs = ok = fail = imgs = 0
|
||||
for t in tasks:
|
||||
for r in store.get_runs(t["id"]):
|
||||
total_runs += 1
|
||||
st = r.get("stats") or {}
|
||||
ok += st.get("ok", 0)
|
||||
fail += st.get("fail", 0)
|
||||
imgs += st.get("images", 0)
|
||||
with JOBS_LOCK:
|
||||
running = sum(1 for j in JOBS.values() if j.is_running())
|
||||
# 统计各任务输出目录的磁盘占用
|
||||
size = 0
|
||||
seen = set()
|
||||
for t in tasks:
|
||||
d = os.path.realpath(resolve_out_dir(t))
|
||||
if d in seen or not os.path.isdir(d):
|
||||
continue
|
||||
seen.add(d)
|
||||
for root, _dirs, files in os.walk(d):
|
||||
for f in files:
|
||||
try:
|
||||
size += os.path.getsize(os.path.join(root, f))
|
||||
except OSError:
|
||||
pass
|
||||
return jsonify({
|
||||
"tasks": len(tasks),
|
||||
"running": running,
|
||||
"runs": total_runs,
|
||||
"ok": ok,
|
||||
"fail": fail,
|
||||
"images": imgs,
|
||||
"disk_mb": round(size / 1048576, 1),
|
||||
})
|
||||
|
||||
|
||||
# ---------------- API: 运行记录与文件 ----------------
|
||||
|
||||
@app.route("/api/runs/<rid>")
|
||||
|
||||
Reference in New Issue
Block a user