fix(system): sort recent tasks by finished_at desc
「最近任务」原先按 kv 键名排序;改为按 finished_at 倒序, 缺完成时间的排末尾。前端渲染侧同步按 finished_at 排序兜底。
This commit is contained in:
1 parent
8e7ead1669
commit
e7b48ba5d9
3 files changed
+70
-5
No files matched your search
@@ -622,19 +622,32 @@ class DashboardQueries:
|
|||||||
continue
|
continue
|
||||||
return None
|
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]:
|
def _task_records(self, conn: sqlite3.Connection) -> dict[str, Any]:
|
||||||
rows = conn.execute(
|
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()
|
).fetchall()
|
||||||
result: dict[str, Any] = {}
|
items: list[tuple[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
name = str(row["k"]).removeprefix("task:last:")
|
name = str(row["k"]).removeprefix("task:last:")
|
||||||
try:
|
try:
|
||||||
value = json.loads(str(row["v"]))
|
value = json.loads(str(row["v"]))
|
||||||
except (TypeError, json.JSONDecodeError):
|
except (TypeError, json.JSONDecodeError):
|
||||||
value = {"raw": row["v"]}
|
value = {"raw": row["v"]}
|
||||||
result[name] = value
|
items.append((name, value))
|
||||||
return result
|
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]:
|
def _task_running(self, conn: sqlite3.Connection) -> dict[str, Any]:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
|
|||||||
@@ -6,7 +6,16 @@ import {
|
|||||||
} from "../format.js";
|
} from "../format.js";
|
||||||
|
|
||||||
function taskRows(tasks) {
|
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("暂无任务记录", "运行一次同步、快照或发现后会写入最近结果。");
|
if (!entries.length) return emptyState("暂无任务记录", "运行一次同步、快照或发现后会写入最近结果。");
|
||||||
return `<div class="table-wrap">
|
return `<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
|
|||||||
@@ -681,6 +681,48 @@ def test_overview_adjacent_previous_with_seed_and_dual_fields():
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
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():
|
def main_test():
|
||||||
test_overview()
|
test_overview()
|
||||||
test_video_listing_filters_and_sorting()
|
test_video_listing_filters_and_sorting()
|
||||||
@@ -696,6 +738,7 @@ def main_test():
|
|||||||
test_fans_candidates_system_and_export()
|
test_fans_candidates_system_and_export()
|
||||||
test_system_status_uses_completed_date_and_running_flags()
|
test_system_status_uses_completed_date_and_running_flags()
|
||||||
test_system_next_runs_follow_last_completion_not_wall_clock()
|
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_growth_auto_falls_back_to_one_day()
|
||||||
test_snapshot_recent_next_run_follows_completion()
|
test_snapshot_recent_next_run_follows_completion()
|
||||||
test_overview_adjacent_previous_with_seed_and_dual_fields()
|
test_overview_adjacent_previous_with_seed_and_dual_fields()
|
||||||
|
|||||||
Reference in new issue
Block a user