Merge branch 'cursor/trigger-failure-workflow-3038' into cursor/ftp-sftp-script-3038

This commit is contained in:
2026-08-19 15:26:20 +07:00
17 changed files with 374 additions and 76 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ export function EventsPage() {
return (
<div className="space-y-4">
<h1 className="text-xl font-semibold">Events</h1>
<h1 className="text-xl font-semibold">{status === "failed" ? "Failed events" : "Events"}</h1>
<div className="flex flex-col sm:flex-row gap-2">
<input
className="input input-sm w-full sm:max-w-sm"
+63
View File
@@ -0,0 +1,63 @@
import { Link } from "react-router-dom";
import { useConsecutiveFailures } from "../api/hooks.js";
import { formatTime } from "../lib/format.jsx";
export function FailuresPage() {
const { data, isLoading, error } = useConsecutiveFailures();
const items = data?.items ?? [];
return (
<div className="space-y-4">
<h1 className="text-xl font-semibold">Failures</h1>
<p className="opacity-70 text-sm">
Workflows that have failed 4 or more times in a row for the same trigger.
</p>
{isLoading ? (
<span className="loading loading-spinner" />
) : error ? (
<p className="text-error">Failed to load consecutive failures</p>
) : items.length === 0 ? (
<p className="opacity-60 text-sm">None</p>
) : (
<div className="overflow-x-auto">
<table className="table table-sm">
<thead>
<tr>
<th>Streak</th>
<th>Workflow</th>
<th>Trigger</th>
<th>Last error</th>
<th>Last failed</th>
</tr>
</thead>
<tbody>
{items.map((s) => (
<tr
key={`${s.workflow}\0${s.trigger_type}\0${s.trigger_detail ?? ""}`}
className="hover"
>
<td>
<span className="badge badge-error badge-sm">{s.consecutiveFailures}</span>
</td>
<td>
<Link className="link" to={`/events/${s.lastRun.id}`}>
{s.workflow_name || s.workflow}
</Link>
</td>
<td className="text-xs">
{s.trigger_type}
{s.trigger_detail ? ` · ${s.trigger_detail}` : ""}
</td>
<td className="max-w-md truncate text-xs" title={s.lastRun.error ?? ""}>
{s.lastRun.error || "—"}
</td>
<td className="whitespace-nowrap">{formatTime(s.lastRun.started_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
+77 -43
View File
@@ -1,10 +1,5 @@
import { Link } from "react-router-dom";
import {
LuActivity,
LuCode,
LuGitBranch,
LuTriangleAlert,
} from "react-icons/lu";
import { LuCode, LuGitBranch, LuTriangleAlert } from "react-icons/lu";
import { useDashboard } from "../api/hooks.js";
import { formatTime, StatusBadge } from "../lib/format.jsx";
@@ -18,8 +13,10 @@ export function HomePage() {
return <p className="text-error">Failed to load dashboard</p>;
}
const failed = data.needsAttention?.failed ?? [];
const streaks = data.needsAttention?.consecutiveFailures ?? [];
const streakCount = data.needsAttention?.consecutiveFailureCount ?? streaks.length;
const broken = data.needsAttention?.brokenWorkflows ?? [];
const failedEvents = data.failedEvents ?? [];
return (
<div className="space-y-6">
@@ -39,52 +36,89 @@ export function HomePage() {
<div className="stat-title">Scripts</div>
<div className="stat-value text-2xl">{data.scriptCount}</div>
</div>
<div className="stat">
<div className="stat-figure text-warning">
<LuActivity className="size-7" />
</div>
<div className="stat-title">Running</div>
<div className="stat-value text-2xl">{data.running?.length ?? 0}</div>
</div>
<div className="stat">
<div className="stat-figure text-error">
<LuTriangleAlert className="size-7" />
</div>
<div className="stat-title">Attention</div>
<div className="stat-value text-2xl">
{failed.length + broken.length}
</div>
<div className="stat-value text-2xl">{streakCount + broken.length}</div>
</div>
</div>
<section>
<h2 className="text-lg font-semibold mb-2">Running</h2>
<RunList runs={data.running} empty="None" />
</section>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<section className="min-w-0">
<div className="mb-2 flex items-baseline justify-between gap-2">
<h2 className="text-lg font-semibold">Needs attention</h2>
<Link className="link link-hover text-sm" to="/failures">
Show all
</Link>
</div>
{broken.length > 0 ? (
<ul className="mb-3 space-y-1">
{broken.map((w) => (
<li key={w.key}>
<Link
className="link link-error"
to={`/workflows/${encodeURIComponent(w.owner)}/${encodeURIComponent(w.file)}/edit`}
>
{w.key}: {w.loadError}
</Link>
</li>
))}
</ul>
) : null}
<StreakList streaks={streaks} empty={broken.length ? "" : "None"} />
</section>
<section>
<h2 className="text-lg font-semibold mb-2">Needs attention</h2>
{broken.length > 0 ? (
<ul className="mb-3 space-y-1">
{broken.map((w) => (
<li key={w.key}>
<Link
className="link link-error"
to={`/workflows/${encodeURIComponent(w.owner)}/${encodeURIComponent(w.file)}/edit`}
>
{w.key}: {w.loadError}
<section className="min-w-0">
<div className="mb-2 flex items-baseline justify-between gap-2">
<h2 className="text-lg font-semibold">Failed events</h2>
<Link className="link link-hover text-sm" to="/events?status=failed">
View all
</Link>
</div>
<RunList runs={failedEvents} empty="None" />
</section>
<section className="min-w-0">
<h2 className="text-lg font-semibold mb-2">Recent</h2>
<RunList runs={data.recent} empty="None" />
</section>
</div>
</div>
);
}
function StreakList({ streaks, empty }) {
if (!streaks?.length) {
return empty ? <p className="opacity-60 text-sm">{empty}</p> : null;
}
return (
<div className="overflow-x-auto">
<table className="table table-sm">
<thead>
<tr>
<th>Streak</th>
<th>Workflow</th>
<th>Last failed</th>
</tr>
</thead>
<tbody>
{streaks.map((s) => (
<tr key={`${s.workflow}\0${s.trigger_type}\0${s.trigger_detail ?? ""}`} className="hover">
<td>
<span className="badge badge-error badge-sm">{s.consecutiveFailures}</span>
</td>
<td>
<Link className="link" to={`/events/${s.lastRun.id}`}>
{s.workflow_name || s.workflow}
</Link>
</li>
))}
</ul>
) : null}
<RunList runs={failed} empty={broken.length ? "" : "None"} />
</section>
<section>
<h2 className="text-lg font-semibold mb-2">Recent</h2>
<RunList runs={data.recent} empty="None" />
</section>
</td>
<td className="whitespace-nowrap">{formatTime(s.lastRun.started_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}