Fix biz_state status while collecting and improve raw log export UX.

Show collecting over paused schedule, enqueue sync so collect_running flips immediately, and expose export on metric sheets with a card layout for Commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-23 11:19:01 +08:00
parent 05357e971e
commit a1acd4c2af
7 changed files with 224 additions and 38 deletions

View file

@ -189,7 +189,7 @@ const en = {
colConnect: "Connect",
start: "Start schedule",
pause: "Pause",
statusPaused: "Paused",
statusPaused: "Manual only",
scheduleEnabled: "Enable schedule",
scheduleOn: "Scheduled",
scheduleOff: "Manual",
@ -204,6 +204,7 @@ const en = {
started: "Schedule enabled",
paused: "Schedule paused (manual only)",
collecting: "Collecting",
statusCollecting: "Collecting",
detail: "Details",
interval: "Interval",
intervalUnitDays: "days",

View file

@ -189,7 +189,7 @@ const zh = {
colConnect: "连通",
start: "启动周期",
pause: "暂停",
statusPaused: "已暂停",
statusPaused: "仅手动",
scheduleEnabled: "启用周期调度",
scheduleOn: "周期",
scheduleOff: "手动",
@ -204,6 +204,7 @@ const zh = {
started: "已启用周期采集",
paused: "已暂停周期(仅手动采集)",
collecting: "采集中",
statusCollecting: "采集中",
detail: "详情",
interval: "采集周期",
intervalUnitDays: "天",

View file

@ -10072,7 +10072,40 @@ html.login-page--paused .login-page__flare {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
flex: 0 1 auto;
flex-wrap: wrap;
max-width: 100%;
}
.bs-commands-card-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: min(48vh, 520px);
overflow: auto;
padding: 2px;
}
.bs-commands-card {
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(15, 23, 42, 0.45);
}
.bs-commands-card__msg {
flex: 1 0 100%;
margin-top: 2px;
font-size: 12px;
color: #94a3b8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bs-cmd-metric {
font-size: 11px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.bs-inline-cmd {

View file

@ -467,13 +467,13 @@ export function BizStatePage() {
const commandsSheetColumns = useMemo<SheetCol[]>(
() => [
{ key: "_actions", header: t("bizState.colActions") },
{ key: "raw_command", header: t("bizState.colCommand") },
{ key: "metric_id", header: "metric" },
{ key: "parse_status", header: t("bizState.colStatus") },
{ key: "raw_line_count", header: t("bizState.colRawLines") },
{ key: "row_count", header: t("bizState.colRows") },
{ key: "message", header: t("bizState.colMessage") },
{ key: "_actions", header: t("bizState.colActions") },
],
[t],
);
@ -823,8 +823,20 @@ export function BizStatePage() {
const collectNowForTask = async (id: string, fromModal = false) => {
setTaskCollecting(id, true);
try {
await bizStateCollectNow(id);
showOk(t("bizState.collecting"));
const out = await bizStateCollectNow(id);
if (out && out.started === false) {
setTaskCollecting(id, false);
const reason = String(out.reason || "");
if (reason === "already_collecting") {
setTaskCollecting(id, true);
showOk(t("bizState.collecting"));
} else {
showError(reason || t("common.opFailed"));
return;
}
} else {
showOk(t("bizState.collecting"));
}
if (fromModal && taskId === id) {
setTaskTab("batches");
try {
@ -1295,8 +1307,8 @@ export function BizStatePage() {
</td>
<td>
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
{row.collect_running ? (
<NmStatusChip color="accent">{t("bizState.collecting")}</NmStatusChip>
{row.collect_running || collectingIds[row.id] ? (
<NmStatusChip color="accent">{t("bizState.statusCollecting")}</NmStatusChip>
) : row.status === "running" ? (
<NmStatusChip color="success">{t("bizState.scheduleOn")}</NmStatusChip>
) : row.status === "paused" ? (
@ -1500,13 +1512,21 @@ export function BizStatePage() {
<Modal.Header>
<Modal.Heading>
{detail?.ne_name || detail?.ne_ip || t("bizState.detail")}
{detail ? ` · ${detail.status}` : ""}
</Modal.Heading>
<Modal.CloseTrigger />
</Modal.Header>
<Modal.Body className="flex flex-col gap-3">
{detail ? (
<div className="config-sync-policy-row bs-schedule-row">
{detail.collect_running || (taskId && collectingIds[taskId]) ? (
<NmStatusChip color="accent">{t("bizState.statusCollecting")}</NmStatusChip>
) : detail.status === "running" ? (
<NmStatusChip color="success">{t("bizState.scheduleOn")}</NmStatusChip>
) : detail.status === "paused" ? (
<NmStatusChip color="warning">{t("bizState.statusPaused")}</NmStatusChip>
) : (
<NmStatusChip color="default">{t("bizState.scheduleOff")}</NmStatusChip>
)}
<label className="config-sync-policy-check">
<input
type="checkbox"
@ -2212,6 +2232,26 @@ export function BizStatePage() {
{c.raw_command || "—"}
</code>
<div className="bs-sheet-cmd-actions">
<Button
size="sm"
variant="secondary"
isDisabled={!c.has_raw}
onPress={() => void openRawLog(c.id)}
>
{t("bizState.viewRawLog")}
</Button>
<Button
size="sm"
variant="ghost"
isDisabled={!c.has_raw || !batchDetail?.id}
onPress={() =>
void bizStateDownloadCommandRaw(String(batchDetail.id), c.id).catch((e) =>
showError(formatErr(e)),
)
}
>
{t("bizState.exportRawLog")}
</Button>
<span className="muted bs-cmd-stat">
{t("bizState.rawLogStats", {
lines: Number(c.raw_line_count ?? 0),
@ -2223,14 +2263,6 @@ export function BizStatePage() {
{c.parse_status}
</NmStatusChip>
) : null}
<Button
size="sm"
variant="secondary"
isDisabled={!c.has_raw}
onPress={() => void openRawLog(c.id)}
>
{t("bizState.viewRawLog")}
</Button>
</div>
</div>
))}
@ -2238,6 +2270,64 @@ export function BizStatePage() {
</div>
) : null}
{activeSheet.id === "commands" ? (
<div className="bs-sheet-cmd-list bs-commands-card-list">
{displayRows.map((row, i) => (
<div key={String(row.id || i)} className="bs-sheet-cmd-row bs-commands-card">
<code className="bs-sheet-cmd-code" title={cellText(row.raw_command)}>
{cellText(row.raw_command) || "—"}
</code>
<div className="bs-sheet-cmd-actions">
<Button
size="sm"
variant="secondary"
isDisabled={!row.has_raw}
onPress={() => void openRawLog(String(row.id || ""))}
>
{t("bizState.viewRawLog")}
</Button>
<Button
size="sm"
variant="ghost"
isDisabled={!row.has_raw || !batchDetail?.id}
onPress={() =>
void bizStateDownloadCommandRaw(
String(batchDetail.id),
String(row.id || ""),
).catch((e) => showError(formatErr(e)))
}
>
{t("bizState.exportRawLog")}
</Button>
<span className="muted bs-cmd-stat">
{t("bizState.rawLogStats", {
lines: Number(row.raw_line_count ?? 0),
rows: Number(row.row_count ?? 0),
})}
</span>
{cellText(row.parse_status) ? (
<NmStatusChip color={jobChipColor(cellText(row.parse_status))}>
{cellText(row.parse_status)}
</NmStatusChip>
) : null}
{cellText(row.metric_id) ? (
<span className="muted bs-cmd-metric">{cellText(row.metric_id)}</span>
) : null}
</div>
{cellText(row.message) ? (
<div className="bs-commands-card__msg" title={cellText(row.message)}>
{cellText(row.message)}
</div>
) : null}
</div>
))}
{!displayRows.length ? (
<div className="pt-list-empty">{t("bizState.sheetEmpty")}</div>
) : null}
</div>
) : null}
{activeSheet.id !== "commands" ? (
<div className="filter-inline bs-sheet-filter">
<FieldSelect
value={sheetColumn}
@ -2268,7 +2358,23 @@ export function BizStatePage() {
{sheetLoading ? t("bizState.sheetLoading") : `${displayTotal} ${t("bizState.colRows")}`}
</span>
</div>
) : (
<div className="filter-inline bs-sheet-filter">
<Input
value={sheetKeyword}
placeholder={t("bizState.sheetFilterPh")}
onChange={(e) => {
setSheetKeyword(e.target.value);
setSheetPage(1);
}}
/>
<span className="muted bs-sheet-count">
{`${displayTotal} ${t("bizState.colRows")}`}
</span>
</div>
)}
{activeSheet.id !== "commands" ? (
<div className="pt-list-table-wrap bs-sheet-table">
<table className="data-table pt-list-table">
<thead>
@ -2327,7 +2433,7 @@ export function BizStatePage() {
</td>
</tr>
) : null}
{sheetLoading && activeSheet.id !== "commands" && !displayRows.length ? (
{sheetLoading && !displayRows.length ? (
<tr>
<td colSpan={Math.max(1, displayColumns.length)}>
<div className="pt-list-empty muted">{t("bizState.sheetLoading")}</div>
@ -2337,6 +2443,7 @@ export function BizStatePage() {
</tbody>
</table>
</div>
) : null}
<ListPager
page={sheetPage}

View file

@ -1824,7 +1824,14 @@ export const bizStateDeleteTask = (taskId: string) =>
apiDelete<{ ok: boolean }>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}`);
export const bizStateCollectNow = (taskId: string) =>
apiPost<{ ok: boolean }>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/collect`, {});
apiPost<{
ok: boolean;
started?: boolean;
queued?: boolean;
reason?: string;
batch_id?: string;
collect_running?: boolean;
}>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/collect`, {});
export const bizStateCollectStop = (taskId: string) =>
apiPost<{