diff --git a/apps/portal/src/components/ui/sonner.stories.tsx b/apps/portal/src/components/ui/sonner.stories.tsx
index 208e66ead..1770d2747 100644
--- a/apps/portal/src/components/ui/sonner.stories.tsx
+++ b/apps/portal/src/components/ui/sonner.stories.tsx
@@ -31,3 +31,22 @@ export const Default: Story = {
});
},
};
+
+export const NoMatchingReportTemplates: Story = {
+ render: () => (
+
+
+
+
+ ),
+ play: async ({ canvas }) => {
+ await userEvent.click(canvas.getByRole("button", { name: "Show no-match feedback" }));
+ await waitFor(async () => {
+ await expect(screen.getByText("No report templates matched this run")).toBeVisible();
+ });
+ },
+};
diff --git a/apps/portal/src/pages/RunDetail.tsx b/apps/portal/src/pages/RunDetail.tsx
index 77ac6b0fa..ee4427572 100644
--- a/apps/portal/src/pages/RunDetail.tsx
+++ b/apps/portal/src/pages/RunDetail.tsx
@@ -390,8 +390,14 @@ export function RunDetail() {
const generateReport = useMutation({
mutationFn: () => api.triggerReports(id!),
onSuccess: (data) => {
- toast.success(`${data.triggered} report(s) queued`);
- refetchReports();
+ if (data.triggered === 0) {
+ toast.info("No report templates matched this run", {
+ description: "No report was queued. Check the active report templates and their triggers.",
+ });
+ } else {
+ toast.success(`${data.triggered} ${data.triggered === 1 ? "report" : "reports"} queued`);
+ refetchReports();
+ }
},
onError: (err) => {
toast.error(err instanceof Error ? err.message : "Failed to generate report");
@@ -1124,7 +1130,7 @@ export function RunDetail() {
className="gap-1.5"
>
- Generate Report
+ Generate matching reports
diff --git a/docs/architecture/app-design.md b/docs/architecture/app-design.md
index f1a243262..a508db0fc 100644
--- a/docs/architecture/app-design.md
+++ b/docs/architecture/app-design.md
@@ -682,6 +682,11 @@ A sibling affordance, `components/ShortId.tsx`, applies the same hoverable-toolt
trigger stays an inline `` (not a link) so the row click still navigates to the run; the
copy button calls `e.stopPropagation()` so copying never triggers row navigation.
+Run detail's **Generate matching reports** action evaluates active report
+template triggers for that run. A zero-match response is shown as an informational
+message explaining that nothing was queued; positive counts use singular or
+plural success text.
+
## Criteria System
Criteria are reusable evaluation rules stored in the database and optionally defined in `config/criteria/*.yaml`. They support: