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 @@ + + + + + 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}, + ]