Skip to content

Commit 735bc7a

Browse files
committed
Add compact workflow diagnostics on tasks page
1 parent 79496a0 commit 735bc7a

File tree

4 files changed

+196
-50
lines changed

4 files changed

+196
-50
lines changed

packages/frontend/src/hooks/useApi.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ async function requestForm<T>(
139139
export type AgentReply = {
140140
messageId: string;
141141
sent: string;
142-
response: string; // Status message only
142+
response: string; // Status message only
143143
prompt?: string;
144144
sessionId?: string;
145-
processing?: boolean; // NEW: indicates polling needed
145+
processing?: boolean; // NEW: indicates polling needed
146146
};
147147

148148
type AgentStatus = {
@@ -188,7 +188,6 @@ export type AvailableAgent = {
188188
source?: string;
189189
};
190190

191-
192191
export type GitDiffEntry = {
193192
path: string;
194193
green: number;
@@ -223,7 +222,6 @@ export type HarnessDefinition = {
223222
source: string;
224223
};
225224

226-
227225
export type WorkflowStep = {
228226
type: "agent" | "bash";
229227
agent?: string;
@@ -240,6 +238,14 @@ export type WorkspaceWorkflowSummary = {
240238
schedule: string | null;
241239
shellScriptPath?: string;
242240
lastRun?: string | null;
241+
diagnostics?: {
242+
lastStartedAt?: string | null;
243+
lastFinishedAt?: string | null;
244+
lastDurationMs?: number | null;
245+
lastExitCode?: number | null;
246+
lastError?: string | null;
247+
running?: boolean;
248+
} | null;
243249
};
244250

245251
export type CronClockSummary = {
@@ -424,10 +430,13 @@ export const api = {
424430
directoryName: string,
425431
branchName: string,
426432
) =>
427-
request<{ path: string; branch: string }>(`/repositories/${name}/git/worktree`, {
428-
method: "POST",
429-
body: JSON.stringify({ directoryName, branchName }),
430-
}),
433+
request<{ path: string; branch: string }>(
434+
`/repositories/${name}/git/worktree`,
435+
{
436+
method: "POST",
437+
body: JSON.stringify({ directoryName, branchName }),
438+
},
439+
),
431440
removeRepositoryWorktree: (name: string) =>
432441
request<{ removed: string }>(`/repositories/${name}/git/worktree`, {
433442
method: "DELETE",
@@ -442,12 +451,17 @@ export const api = {
442451
),
443452

444453
getRepositoryWorkflows: (name: string) =>
445-
request<{ workflows: WorkflowDefinition[] }>(`/repositories/${name}/workflows`),
454+
request<{ workflows: WorkflowDefinition[] }>(
455+
`/repositories/${name}/workflows`,
456+
),
446457
saveRepositoryWorkflows: (name: string, workflows: WorkflowDefinition[]) =>
447-
request<{ workflows: WorkflowDefinition[] }>(`/repositories/${name}/workflows`, {
448-
method: "PUT",
449-
body: JSON.stringify({ workflows }),
450-
}),
458+
request<{ workflows: WorkflowDefinition[] }>(
459+
`/repositories/${name}/workflows`,
460+
{
461+
method: "PUT",
462+
body: JSON.stringify({ workflows }),
463+
},
464+
),
451465
getCommands: () => request<{ commands: CommandDefinition[] }>("/commands"),
452466
getHarnesses: () => request<{ harnesses: HarnessDefinition[] }>("/harnesses"),
453467
runHarness: (path: string, args?: string) =>
@@ -458,7 +472,8 @@ export const api = {
458472
getHarnessStatus: (pid: number) =>
459473
request<{ pid: number; running: boolean }>(`/harnesses/${pid}/status`),
460474

461-
getWorkflows: () => request<{ workflows: WorkflowDefinition[] }>("/workflows"),
475+
getWorkflows: () =>
476+
request<{ workflows: WorkflowDefinition[] }>("/workflows"),
462477
getWorkspaceWorkflows: () =>
463478
request<{ workflows: WorkspaceWorkflowSummary[] }>("/workspace/workflows"),
464479
saveWorkflows: (workflows: WorkflowDefinition[]) =>

packages/frontend/src/pages/TasksPage.test.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import { TasksPage } from "./TasksPage";
88
import { api } from "../hooks/useApi";
99

1010
vi.mock("../hooks/useApi", async () => {
11-
const actual = await vi.importActual<typeof import("../hooks/useApi")>(
12-
"../hooks/useApi",
13-
);
11+
const actual =
12+
await vi.importActual<typeof import("../hooks/useApi")>("../hooks/useApi");
1413
return {
1514
...actual,
1615
api: {
@@ -59,6 +58,14 @@ describe("TasksPage", () => {
5958
enabled: true,
6059
schedule: "0 8 * * 1",
6160
lastRun: "2026-01-02T03:04:05Z",
61+
diagnostics: {
62+
lastStartedAt: "2026-01-02T03:00:00Z",
63+
lastFinishedAt: "2026-01-02T03:04:05Z",
64+
lastDurationMs: 245000,
65+
lastExitCode: 0,
66+
lastError: null,
67+
running: false,
68+
},
6269
},
6370
],
6471
});
@@ -78,10 +85,14 @@ describe("TasksPage", () => {
7885
expect(screen.getByLabelText("Release enabled")).toBeChecked();
7986
expect(screen.getByText("sample-repo")).toBeInTheDocument();
8087
expect(screen.getByText("Last run")).toBeInTheDocument();
81-
expect(screen.getByText(/2026|1\/2\/2026|2\/1\/2026/)).toBeInTheDocument();
88+
expect(
89+
screen.getAllByText(/2026|1\/2\/2026|2\/1\/2026/).length,
90+
).toBeGreaterThan(0);
91+
expect(screen.getByText("Diagnostics")).toBeInTheDocument();
92+
expect(screen.getByText("Idle")).toBeInTheDocument();
93+
expect(screen.getByText("Exit 0")).toBeInTheDocument();
8294
});
8395

84-
8596
it("shows fallback last-run labels for non-cron and never-run workflows", async () => {
8697
vi.mocked(api.listTasks).mockResolvedValue({ tasks: [] });
8798
vi.mocked(api.getWorkspaceWorkflows).mockResolvedValue({
@@ -115,6 +126,31 @@ describe("TasksPage", () => {
115126
expect(screen.getAllByText("-").length).toBeGreaterThan(0);
116127
});
117128

129+
it("shows fallback diagnostics label when no diagnostics exist", async () => {
130+
vi.mocked(api.listTasks).mockResolvedValue({ tasks: [] });
131+
vi.mocked(api.getWorkspaceWorkflows).mockResolvedValue({
132+
workflows: [
133+
{
134+
repository: "repo-a",
135+
id: "wf_no_diagnostics",
136+
name: "No Diagnostics",
137+
enabled: true,
138+
schedule: "0 8 * * 1",
139+
lastRun: null,
140+
diagnostics: null,
141+
},
142+
],
143+
});
144+
145+
render(
146+
<MemoryRouter>
147+
<TasksPage />
148+
</MemoryRouter>,
149+
);
150+
151+
expect(await screen.findByText("No diagnostics yet")).toBeInTheDocument();
152+
});
153+
118154
it("creates tasks with schedule metadata", async () => {
119155
vi.mocked(api.listTasks).mockResolvedValue({ tasks: [] });
120156
vi.mocked(api.saveTask).mockResolvedValue({});
@@ -125,7 +161,9 @@ describe("TasksPage", () => {
125161
</MemoryRouter>,
126162
);
127163

128-
fireEvent.click(await screen.findByRole("button", { name: /create task/i }));
164+
fireEvent.click(
165+
await screen.findByRole("button", { name: /create task/i }),
166+
);
129167
fireEvent.change(screen.getByLabelText(/file name/i), {
130168
target: { value: "new-task" },
131169
});

packages/frontend/src/pages/TasksPage.tsx

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,102 @@ import { TabView } from "../components/TabView";
1010
import { Modal } from "../components/Modal";
1111
import "../styles/page.css";
1212

13+
type WorkflowDiagnostics = WorkspaceWorkflowSummary["diagnostics"];
14+
15+
const formatDateTime = (value?: string | null, fallback = "-") => {
16+
if (!value) {
17+
return fallback;
18+
}
19+
const date = new Date(value);
20+
if (Number.isNaN(date.getTime())) {
21+
return fallback;
22+
}
23+
return date.toLocaleString();
24+
};
25+
26+
const formatDuration = (durationMs?: number | null) => {
27+
if (typeof durationMs !== "number" || Number.isNaN(durationMs)) {
28+
return "-";
29+
}
30+
if (durationMs < 1000) {
31+
return `${durationMs}ms`;
32+
}
33+
const seconds = durationMs / 1000;
34+
if (seconds < 60) {
35+
return `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s`;
36+
}
37+
const minutes = Math.floor(seconds / 60);
38+
const remainingSeconds = Math.round(seconds % 60);
39+
return `${minutes}m ${remainingSeconds}s`;
40+
};
41+
42+
const formatExitCode = (value?: number | null) => {
43+
if (typeof value !== "number" || Number.isNaN(value)) {
44+
return "-";
45+
}
46+
return String(value);
47+
};
48+
49+
const formatErrorText = (value?: string | null) => {
50+
if (!value || !value.trim()) {
51+
return "-";
52+
}
53+
const normalized = value.trim();
54+
if (normalized.length <= 80) {
55+
return normalized;
56+
}
57+
return `${normalized.slice(0, 77)}...`;
58+
};
59+
60+
const formatWorkflowLastRun = (workflow: WorkspaceWorkflowSummary) => {
61+
if (!workflow.schedule || !workflow.schedule.trim()) {
62+
return "n.a.";
63+
}
64+
65+
if (!workflow.lastRun) {
66+
return "-";
67+
}
68+
69+
return formatDateTime(workflow.lastRun);
70+
};
71+
72+
const renderWorkflowDiagnosticsSummary = (diagnostics: WorkflowDiagnostics) => {
73+
if (!diagnostics) {
74+
return <span className="meta-secondary">No diagnostics yet</span>;
75+
}
76+
77+
return (
78+
<details className="workflow-diagnostics">
79+
<summary className="workflow-diagnostics__summary">
80+
<span
81+
className={`badge ${diagnostics.running ? "success" : ""}`.trim()}
82+
>
83+
{diagnostics.running ? "Running" : "Idle"}
84+
</span>
85+
<span className="badge">
86+
Exit {formatExitCode(diagnostics.lastExitCode)}
87+
</span>
88+
<span className="badge">
89+
{formatDuration(diagnostics.lastDurationMs)}
90+
</span>
91+
</summary>
92+
<div className="meta-secondary">
93+
<span>Started: {formatDateTime(diagnostics.lastStartedAt)}</span>
94+
<span>Finished: {formatDateTime(diagnostics.lastFinishedAt)}</span>
95+
</div>
96+
<div className="meta-secondary">
97+
Error: {formatErrorText(diagnostics.lastError)}
98+
</div>
99+
</details>
100+
);
101+
};
102+
13103
export const TasksPage: React.FC = () => {
14104
const [tasks, setTasks] = useState<ArtefactSummary[]>([]);
15105
const [activeTab, setActiveTab] = useState("tasks");
16-
const [workspaceWorkflows, setWorkspaceWorkflows] = useState<WorkspaceWorkflowSummary[]>([]);
106+
const [workspaceWorkflows, setWorkspaceWorkflows] = useState<
107+
WorkspaceWorkflowSummary[]
108+
>([]);
17109
const [createOpen, setCreateOpen] = useState(false);
18110
const [newName, setNewName] = useState("");
19111
const navigate = useNavigate();
@@ -22,29 +114,12 @@ export const TasksPage: React.FC = () => {
22114
return task.type === "template";
23115
}
24116
const frontmatterType = task.frontmatter?.type;
25-
return typeof frontmatterType === "string" && frontmatterType === "template";
117+
return (
118+
typeof frontmatterType === "string" && frontmatterType === "template"
119+
);
26120
};
27121
const templateTasks = tasks.filter(isTemplate);
28-
const documentTasks = tasks.filter(
29-
(task) => !isTemplate(task),
30-
);
31-
const formatWorkflowLastRun = (workflow: WorkspaceWorkflowSummary) => {
32-
if (!workflow.schedule || !workflow.schedule.trim()) {
33-
return "n.a.";
34-
}
35-
36-
if (!workflow.lastRun) {
37-
return "-";
38-
}
39-
40-
const lastRunDate = new Date(workflow.lastRun);
41-
if (Number.isNaN(lastRunDate.getTime())) {
42-
return "-";
43-
}
44-
45-
return lastRunDate.toLocaleString();
46-
};
47-
122+
const documentTasks = tasks.filter((task) => !isTemplate(task));
48123
const getRepositoryName = (repository: string) => {
49124
const segments = repository.split(/[\\/]/).filter(Boolean);
50125
return segments[segments.length - 1] || repository;
@@ -95,7 +170,8 @@ export const TasksPage: React.FC = () => {
95170
<Panel title="Workflows">
96171
{workspaceWorkflows.length === 0 ? (
97172
<div className="empty">
98-
No workflows found in repository .made/workflows.yml files.
173+
No workflows found in repository .made/workflows.yml
174+
files.
99175
</div>
100176
) : (
101177
<table className="git-table">
@@ -106,11 +182,14 @@ export const TasksPage: React.FC = () => {
106182
<th>Name</th>
107183
<th>Repository</th>
108184
<th>Last run</th>
185+
<th>Diagnostics</th>
109186
</tr>
110187
</thead>
111188
<tbody>
112189
{workspaceWorkflows.map((workflow) => {
113-
const repositoryName = getRepositoryName(workflow.repository);
190+
const repositoryName = getRepositoryName(
191+
workflow.repository,
192+
);
114193
return (
115194
<tr key={`${workflow.repository}:${workflow.id}`}>
116195
<td>
@@ -131,6 +210,11 @@ export const TasksPage: React.FC = () => {
131210
</td>
132211
<td>{repositoryName}</td>
133212
<td>{formatWorkflowLastRun(workflow)}</td>
213+
<td>
214+
{renderWorkflowDiagnosticsSummary(
215+
workflow.diagnostics,
216+
)}
217+
</td>
134218
</tr>
135219
);
136220
})}
@@ -164,8 +248,7 @@ export const TasksPage: React.FC = () => {
164248
{String(task.frontmatter.schedule)}
165249
</span>
166250
)}
167-
{typeof task.frontmatter?.type ===
168-
"string" && (
251+
{typeof task.frontmatter?.type === "string" && (
169252
<span className="badge">
170253
{String(task.frontmatter.type)}
171254
</span>
@@ -192,8 +275,7 @@ export const TasksPage: React.FC = () => {
192275
{String(task.frontmatter.schedule)}
193276
</span>
194277
)}
195-
{typeof task.frontmatter?.type ===
196-
"string" && (
278+
{typeof task.frontmatter?.type === "string" && (
197279
<span className="badge">
198280
{String(task.frontmatter.type)}
199281
</span>

0 commit comments

Comments
 (0)