diff --git a/reaction_board/web/queries.py b/reaction_board/web/queries.py index 20314c8..fa1b4bd 100644 --- a/reaction_board/web/queries.py +++ b/reaction_board/web/queries.py @@ -623,10 +623,10 @@ class DashboardQueries: return None @staticmethod - def _task_finished_sort_key(value: Any) -> tuple[int, int]: - """缺 finished_at 的排末尾;有值的按完成时间倒序。""" + def _task_started_sort_key(value: Any) -> tuple[int, int]: + """缺 started_at 的排末尾;有值的按开始时间倒序(含进行中)。""" if isinstance(value, Mapping): - raw = value.get("finished_at") + raw = value.get("started_at") try: return (0, -int(raw)) except (TypeError, ValueError): @@ -645,8 +645,8 @@ class DashboardQueries: except (TypeError, json.JSONDecodeError): value = {"raw": row["v"]} items.append((name, value)) - items.sort(key=lambda pair: self._task_finished_sort_key(pair[1])) - # 插入序即完成时间倒序;JSON/Object.entries 会保留。 + items.sort(key=lambda pair: self._task_started_sort_key(pair[1])) + # 插入序即开始时间倒序;JSON/Object.entries 会保留。 return dict(items) def _task_running(self, conn: sqlite3.Connection) -> dict[str, Any]: diff --git a/reaction_board/web/static/views/system.js b/reaction_board/web/static/views/system.js index eff2b78..7bb66d4 100644 --- a/reaction_board/web/static/views/system.js +++ b/reaction_board/web/static/views/system.js @@ -7,11 +7,11 @@ import { function taskRows(tasks) { const entries = Object.entries(tasks || {}).sort((a, b) => { - const fa = Number(a[1] && a[1].finished_at); - const fb = Number(b[1] && b[1].finished_at); - const aOk = Number.isFinite(fa); - const bOk = Number.isFinite(fb); - if (aOk && bOk) return fb - fa; + const sa = Number(a[1] && a[1].started_at); + const sb = Number(b[1] && b[1].started_at); + const aOk = Number.isFinite(sa); + const bOk = Number.isFinite(sb); + if (aOk && bOk) return sb - sa; if (aOk) return -1; if (bOk) return 1; return String(a[0]).localeCompare(String(b[0])); diff --git a/tests/test_web_queries.py b/tests/test_web_queries.py index 66480a3..d4e2f2e 100644 --- a/tests/test_web_queries.py +++ b/tests/test_web_queries.py @@ -682,33 +682,34 @@ def test_overview_adjacent_previous_with_seed_and_dual_fields(): db.close() -def test_system_tasks_sorted_by_finished_at_desc(): - """最近任务按 finished_at 倒序;缺时间的排末尾。""" +def test_system_tasks_sorted_by_started_at_desc(): + """最近任务按 started_at 倒序;缺开始时间的排末尾(含进行中无 finished)。""" import json with tempfile.TemporaryDirectory() as tmp: db, q = setup(tmp) try: repo = Repository(db) - # 故意打乱写入/键名字母序,避免 ORDER BY k 碰巧正确 + # 故意打乱写入/键名字母序;finished_at 与 started_at 错开,避免误用完成时间 samples = ( - ("zzz_old", FIXED_NOW - 300), - ("aaa_mid", FIXED_NOW - 100), - ("mmm_new", FIXED_NOW - 10), - ("no_time", None), + # name, started_at, finished_at + ("zzz_old", FIXED_NOW - 300, FIXED_NOW - 10), # 开始最早,完成却很晚 + ("aaa_mid", FIXED_NOW - 100, FIXED_NOW - 90), + ("mmm_new", FIXED_NOW - 10, None), # 进行中:只有 started + ("no_time", None, FIXED_NOW), # 缺 started,应末尾 ) - for name, finished in samples: + for name, started, finished in samples: payload = { "name": name, - "ok": True, - "started_at": (finished - 5) if finished is not None else FIXED_NOW, + "ok": True if finished is not None else True, "error": None, "summary": {}, } + if started is not None: + payload["started_at"] = started if finished is not None: payload["finished_at"] = finished repo.kv_set(f"task:last:{name}", json.dumps(payload)) - # 清掉 fixture 里的旧任务,只保留本用例 for legacy in ("snapshot", "fans", "discovery"): repo.kv_delete(f"task:last:{legacy}") DASHBOARD_CACHE.clear() @@ -717,7 +718,6 @@ def test_system_tasks_sorted_by_finished_at_desc(): "mmm_new", "aaa_mid", "zzz_old", "no_time", ] overview = q.overview() - # overview 也走 _task_records,顺序应一致 assert list(overview["tasks"].keys()) == list(system["tasks"].keys()) finally: db.close() @@ -738,7 +738,7 @@ def main_test(): test_fans_candidates_system_and_export() test_system_status_uses_completed_date_and_running_flags() test_system_next_runs_follow_last_completion_not_wall_clock() - test_system_tasks_sorted_by_finished_at_desc() + test_system_tasks_sorted_by_started_at_desc() test_growth_auto_falls_back_to_one_day() test_snapshot_recent_next_run_follows_completion() test_overview_adjacent_previous_with_seed_and_dual_fields()