diff --git a/reaction_board/web/queries.py b/reaction_board/web/queries.py index 5db1331..20314c8 100644 --- a/reaction_board/web/queries.py +++ b/reaction_board/web/queries.py @@ -622,19 +622,32 @@ class DashboardQueries: continue return None + @staticmethod + def _task_finished_sort_key(value: Any) -> tuple[int, int]: + """缺 finished_at 的排末尾;有值的按完成时间倒序。""" + if isinstance(value, Mapping): + raw = value.get("finished_at") + try: + return (0, -int(raw)) + except (TypeError, ValueError): + pass + return (1, 0) + def _task_records(self, conn: sqlite3.Connection) -> dict[str, Any]: rows = conn.execute( - "SELECT k, v FROM kv WHERE k LIKE 'task:last:%' ORDER BY k" + "SELECT k, v FROM kv WHERE k LIKE 'task:last:%'" ).fetchall() - result: dict[str, Any] = {} + items: list[tuple[str, Any]] = [] for row in rows: name = str(row["k"]).removeprefix("task:last:") try: value = json.loads(str(row["v"])) except (TypeError, json.JSONDecodeError): value = {"raw": row["v"]} - result[name] = value - return result + items.append((name, value)) + items.sort(key=lambda pair: self._task_finished_sort_key(pair[1])) + # 插入序即完成时间倒序;JSON/Object.entries 会保留。 + return dict(items) def _task_running(self, conn: sqlite3.Connection) -> dict[str, Any]: rows = conn.execute( diff --git a/reaction_board/web/static/views/system.js b/reaction_board/web/static/views/system.js index f4920d7..eff2b78 100644 --- a/reaction_board/web/static/views/system.js +++ b/reaction_board/web/static/views/system.js @@ -6,7 +6,16 @@ import { } from "../format.js"; function taskRows(tasks) { - const entries = Object.entries(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; + if (aOk) return -1; + if (bOk) return 1; + return String(a[0]).localeCompare(String(b[0])); + }); if (!entries.length) return emptyState("暂无任务记录", "运行一次同步、快照或发现后会写入最近结果。"); return `
diff --git a/tests/test_web_queries.py b/tests/test_web_queries.py index 5ab401b..66480a3 100644 --- a/tests/test_web_queries.py +++ b/tests/test_web_queries.py @@ -681,6 +681,48 @@ def test_overview_adjacent_previous_with_seed_and_dual_fields(): finally: db.close() + +def test_system_tasks_sorted_by_finished_at_desc(): + """最近任务按 finished_at 倒序;缺时间的排末尾。""" + import json + + with tempfile.TemporaryDirectory() as tmp: + db, q = setup(tmp) + try: + repo = Repository(db) + # 故意打乱写入/键名字母序,避免 ORDER BY k 碰巧正确 + samples = ( + ("zzz_old", FIXED_NOW - 300), + ("aaa_mid", FIXED_NOW - 100), + ("mmm_new", FIXED_NOW - 10), + ("no_time", None), + ) + for name, finished in samples: + payload = { + "name": name, + "ok": True, + "started_at": (finished - 5) if finished is not None else FIXED_NOW, + "error": None, + "summary": {}, + } + 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() + system = q.system() + assert list(system["tasks"].keys()) == [ + "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() + + def main_test(): test_overview() test_video_listing_filters_and_sorting() @@ -696,6 +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_growth_auto_falls_back_to_one_day() test_snapshot_recent_next_run_follows_completion() test_overview_adjacent_previous_with_seed_and_dual_fields()