Show collect start/end times on biz-state task and batch lists.

Helps tune schedule interval from last-run window and batch duration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-22 15:58:32 +08:00
parent 12b60c2a52
commit c15ea00a23
4 changed files with 48 additions and 4 deletions

View file

@ -428,6 +428,9 @@ def list_tasks(db: Session, *, purpose: str | None = None) -> list[dict[str, Any
"interval_sec": t.interval_sec, "interval_sec": t.interval_sec,
"collect_running": bool(t.collect_running), "collect_running": bool(t.collect_running),
"last_error": t.last_error, "last_error": t.last_error,
"last_collect_started_at": t.last_collect_started_at.isoformat() + "Z"
if t.last_collect_started_at
else None,
"last_collect_ended_at": t.last_collect_ended_at.isoformat() + "Z" "last_collect_ended_at": t.last_collect_ended_at.isoformat() + "Z"
if t.last_collect_ended_at if t.last_collect_ended_at
else None, else None,

View file

@ -295,6 +295,8 @@ const en = {
colNe: "NE", colNe: "NE",
colStatus: "Status", colStatus: "Status",
colLast: "Last collect", colLast: "Last collect",
colStarted: "Started",
colEnded: "Ended",
colTime: "Time", colTime: "Time",
colRows: "Rows", colRows: "Rows",
colActions: "Actions", colActions: "Actions",

View file

@ -295,6 +295,8 @@ const zh = {
colNe: "网元", colNe: "网元",
colStatus: "状态", colStatus: "状态",
colLast: "最近采集", colLast: "最近采集",
colStarted: "开始时间",
colEnded: "结束时间",
colTime: "时间", colTime: "时间",
colRows: "行数", colRows: "行数",
colActions: "操作", colActions: "操作",

View file

@ -50,6 +50,7 @@ type TaskRow = {
status: string; status: string;
collect_running: boolean; collect_running: boolean;
last_error: string; last_error: string;
last_collect_started_at?: string | null;
last_collect_ended_at?: string | null; last_collect_ended_at?: string | null;
interval_sec?: number; interval_sec?: number;
}; };
@ -82,6 +83,7 @@ type BatchRow = {
row_count: number; row_count: number;
command_count: number; command_count: number;
started_at?: string | null; started_at?: string | null;
ended_at?: string | null;
is_baseline?: boolean; is_baseline?: boolean;
protected?: boolean; protected?: boolean;
protect_reasons?: string[]; protect_reasons?: string[];
@ -169,6 +171,21 @@ function fmtTime(v?: string | null) {
return formatSystemTime(v) || v; return formatSystemTime(v) || v;
} }
function fmtDuration(started?: string | null, ended?: string | null) {
if (!started || !ended) return "";
const a = Date.parse(started);
const b = Date.parse(ended);
if (!Number.isFinite(a) || !Number.isFinite(b) || b < a) return "";
const sec = Math.round((b - a) / 1000);
if (sec < 60) return `${sec}s`;
const m = Math.floor(sec / 60);
const s = sec % 60;
if (m < 60) return s ? `${m}m${s}s` : `${m}m`;
const h = Math.floor(m / 60);
const rm = m % 60;
return rm ? `${h}h${rm}m` : `${h}h`;
}
function cellText(v: unknown): string { function cellText(v: unknown): string {
if (v === null || v === undefined) return ""; if (v === null || v === undefined) return "";
if (typeof v === "object") return JSON.stringify(v); if (typeof v === "object") return JSON.stringify(v);
@ -1056,7 +1073,8 @@ export function BizStatePage() {
<th>{t("bizState.colInterval")}</th> <th>{t("bizState.colInterval")}</th>
<th>{t("bizState.scheduleEnabled")}</th> <th>{t("bizState.scheduleEnabled")}</th>
<th>{t("bizState.colStatus")}</th> <th>{t("bizState.colStatus")}</th>
<th>{t("bizState.colLast")}</th> <th>{t("bizState.colStarted")}</th>
<th>{t("bizState.colEnded")}</th>
<th>{t("bizState.colActions")}</th> <th>{t("bizState.colActions")}</th>
</tr> </tr>
</thead> </thead>
@ -1119,7 +1137,15 @@ export function BizStatePage() {
)} )}
</div> </div>
</td> </td>
<td className="pt-list-time">{fmtTime(row.last_collect_ended_at)}</td> <td className="pt-list-time">{fmtTime(row.last_collect_started_at)}</td>
<td className="pt-list-time">
{fmtTime(row.last_collect_ended_at)}
{fmtDuration(row.last_collect_started_at, row.last_collect_ended_at) ? (
<div className="muted">
{fmtDuration(row.last_collect_started_at, row.last_collect_ended_at)}
</div>
) : null}
</td>
<td> <td>
<div className="pt-list-actions"> <div className="pt-list-actions">
<Button size="sm" variant="primary" onPress={() => void openTask(row.id, "profiles")}> <Button size="sm" variant="primary" onPress={() => void openTask(row.id, "profiles")}>
@ -1593,7 +1619,8 @@ export function BizStatePage() {
<thead> <thead>
<tr> <tr>
<th style={{ width: 36 }} /> <th style={{ width: 36 }} />
<th>{t("bizState.colTime")}</th> <th>{t("bizState.colStarted")}</th>
<th>{t("bizState.colEnded")}</th>
<th>{t("bizState.colAlias")}</th> <th>{t("bizState.colAlias")}</th>
<th>{t("bizState.colStatus")}</th> <th>{t("bizState.colStatus")}</th>
<th>{t("bizState.colProtect")}</th> <th>{t("bizState.colProtect")}</th>
@ -1615,6 +1642,12 @@ export function BizStatePage() {
/> />
</td> </td>
<td className="pt-list-time">{fmtTime(b.started_at)}</td> <td className="pt-list-time">{fmtTime(b.started_at)}</td>
<td className="pt-list-time">
{fmtTime(b.ended_at)}
{fmtDuration(b.started_at, b.ended_at) ? (
<div className="muted">{fmtDuration(b.started_at, b.ended_at)}</div>
) : null}
</td>
<td> <td>
<button <button
type="button" type="button"
@ -1896,7 +1929,11 @@ export function BizStatePage() {
})} })}
{" · "} {" · "}
{batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "} {batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "}
{fmtTime(batchDetail.started_at)} {t("bizState.colStarted")} {fmtTime(batchDetail.started_at)} ·{" "}
{t("bizState.colEnded")} {fmtTime(batchDetail.ended_at)}
{fmtDuration(batchDetail.started_at, batchDetail.ended_at)
? ` · ${fmtDuration(batchDetail.started_at, batchDetail.ended_at)}`
: ""}
</span> </span>
<div className="bs-id-row" title={String(batchDetail.id || "")}> <div className="bs-id-row" title={String(batchDetail.id || "")}>
<span className="bs-id-row__label">{t("bizState.batchId")}</span> <span className="bs-id-row__label">{t("bizState.batchId")}</span>