From 67d3c68fbfc5a8004f8bd903fac0028c2900cb99 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 22 Oct 2026 19:59:02 -0400 Subject: [PATCH] feat(6254): moment_usage says which rule each moment delivered, and whether it was opened Each moment gains by_rule: {rule_id, title, delivered, opened}, read from the (moment, rule) grouping the readout already did. The moment's totals are now summed from those rows, so the two cannot disagree. The Moments settings page shows the list under each moment, closed by default. Co-Authored-By: Claude Opus 5.5 --- frontend/src/api/moments.ts | 10 ++++++ frontend/src/components/MomentRuleUsage.vue | 37 +++++++++++++++++++++ frontend/src/components/MomentsSettings.vue | 3 ++ tests/test_integration_rule_moments.py | 17 ++++++++-- 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/MomentRuleUsage.vue 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}, + ]