diff --git a/frontend/src/api/moments.ts b/frontend/src/api/moments.ts
index bb1e55c0..d4c8e11b 100644
--- a/frontend/src/api/moments.ts
+++ b/frontend/src/api/moments.ts
@@ -44,6 +44,14 @@ export interface RemovedDefault {
created_at: string | null;
}
+export interface MomentRuleUsage {
+ rule_id: number;
+ /** Null for a rule deleted since it was delivered. */
+ title: string | null;
+ delivered: number;
+ opened: boolean;
+}
+
export interface MomentUsage {
/** How many times a mounted rule arrived at this moment. */
delivered: number;
@@ -53,6 +61,8 @@ export interface MomentUsage {
* the door, not the line that prompted it. */
opened: number;
last_delivered_at: string | null;
+ /** Each rule delivered here, most delivered first. */
+ by_rule: MomentRuleUsage[];
}
export interface MomentsPayload {
diff --git a/frontend/src/components/MomentRuleUsage.vue b/frontend/src/components/MomentRuleUsage.vue
new file mode 100644
index 00000000..70578498
--- /dev/null
+++ b/frontend/src/components/MomentRuleUsage.vue
@@ -0,0 +1,37 @@
+
+
+
+
+ Rules delivered here
+
+ -
+ {{ r.title ?? `Rule ${r.rule_id} (deleted)` }}
+ delivered {{ r.delivered }}
+
+ {{ r.opened ? "opened" : "not opened" }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/MomentsSettings.vue b/frontend/src/components/MomentsSettings.vue
index bff473f7..44c8a8e7 100644
--- a/frontend/src/components/MomentsSettings.vue
+++ b/frontend/src/components/MomentsSettings.vue
@@ -2,6 +2,7 @@
import { computed, onMounted, ref } from "vue";
import { useMomentsStore } from "@/stores/moments";
import MomentProposals from "@/components/MomentProposals.vue";
+import MomentRuleUsage from "@/components/MomentRuleUsage.vue";
import type { MomentAction, MomentUsage } from "@/api/moments";
import { fmtDate } from "@/utils/dateFormat";
@@ -167,6 +168,7 @@ onMounted(() => store.load(true));
>×
+
@@ -186,6 +188,7 @@ onMounted(() => store.load(true));
+
diff --git a/tests/test_integration_rule_moments.py b/tests/test_integration_rule_moments.py
index ce6e1955..3ba79b37 100644
--- a/tests/test_integration_rule_moments.py
+++ b/tests/test_integration_rule_moments.py
@@ -238,9 +238,10 @@ async def test_the_delivery_lookup_answers_by_home(world):
async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(world):
- """The per-moment readout, on real SQL: an EXISTS inside a
- COUNT(CASE …) is a shape no mock can vouch for, and the block's own guard
- would turn a database refusal into a quiet `moment_usage_failed`."""
+ """The per-moment readout, on real SQL: a correlated EXISTS selected as a
+ column beside an outer join is a shape no mock can vouch for, and the
+ block's own guard would turn a database refusal into a quiet
+ `moment_usage_failed`."""
from datetime import datetime, timedelta, timezone
from sqlalchemy import delete
@@ -289,3 +290,13 @@ async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(
report = block["by_moment"]["reply.report"]
assert (report["delivered"], report["rules"], report["opened"]) == (1, 1, 0)
assert report["last_delivered_at"]
+
+ # Two rules share work.finish: which one arrived, and which was opened.
+ assert finish["by_rule"] == [
+ {"rule_id": rule.id, "title": rule.title, "delivered": 2, "opened": True},
+ {"rule_id": other.id, "title": "Close the task with the push",
+ "delivered": 1, "opened": False},
+ ]
+ assert report["by_rule"] == [
+ {"rule_id": rule.id, "title": rule.title, "delivered": 1, "opened": False},
+ ]