From a29fd3c7ded966b6d0b4b188528bbb4632101f6e Mon Sep 17 00:00:00 2001 From: Jonathan Jackson Date: Fri, 22 May 2026 03:31:05 -0600 Subject: [PATCH] feat(commcare): conditional_alert list atom code (deferred registration) Adds listConditionalAlerts method in commcare.ts based on the documented AJAX endpoint (corehq/messaging/scheduling/views.py:653 get_conditional_alerts_ajax_response). Source says GET /a//messaging/conditional/?action=list_conditional_alerts should return JSON with rules + total. Verified hypothesis-vs-reality: the live HQ (www.commcarehq.org as of 2026-05-22) returns the HTML page regardless of action/Accept/ X-Requested-With/Authorization headers. Could be deployed-vs-source version skew, middleware stripping the param, or JS using a different endpoint entirely. Atom code preserved in commcare.ts for future debugging; MCP tool registration commented out so we don't ship a broken atom. The code is correct per source; just not exercised by the live deployment. V1 manual steps remain at 2 (subscription + conditional alert). Co-Authored-By: Claude Opus 4.7 --- mcp/connect-server.ts | 10 ++++ mcp/connect/backends/commcare.ts | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/mcp/connect-server.ts b/mcp/connect-server.ts index 3697e1b..c5f61be 100644 --- a/mcp/connect-server.ts +++ b/mcp/connect-server.ts @@ -506,6 +506,16 @@ server.tool('commcare_create_lookup_table', async (args) => runAtom(async () => (await commcareClient()).createLookupTable(args)) ); +// commcare_list_conditional_alerts — DEFERRED (atom code in place but +// AJAX endpoint doesn't behave as source suggests on live HQ as of +// 2026-05-22. corehq/messaging/scheduling/views.py:653 documents +// `?action=list_conditional_alerts` returning JSON, but live calls +// return the HTML page regardless of action/Accept/X-Requested-With. +// Possible causes: deployed HQ predates the handler; middleware +// strips the param; or JS uses a different endpoint that the source +// doesn't expose. Atom code is preserved in commcare.ts for future +// debugging; not registered to avoid shipping a broken atom. + server.tool('commcare_list_user_fields', 'Read the current custom-user-data field definition for a CommCare HQ domain. GET /a//users/user_data/ and parse the
initial_page_data div (HQ\'s standard Django→JS bootstrap). Returns the list of fields (slug, label, is_required, choices, regex) + the list of profiles. Requires can_edit_commcare_users permission; 302s to settings/users/ surface as a typed error.', { domain: z.string() }, diff --git a/mcp/connect/backends/commcare.ts b/mcp/connect/backends/commcare.ts index b429a95..0b3693b 100644 --- a/mcp/connect/backends/commcare.ts +++ b/mcp/connect/backends/commcare.ts @@ -230,6 +230,25 @@ export interface ListUserFieldsArgs { domain: string; } +export interface ConditionalAlert { + id: number; + name: string; + case_type: string; + /** Whether the rule's *schedule* is active (not the rule itself — see ConditionalAlertListView docstring). */ + active: boolean; + /** Whether the alert can be edited from the UI (false for SMS-survey-using alerts on subscriptions without inbound SMS). */ + editable: boolean; + locked_for_editing: boolean; + progress_pct: number; +} + +export interface ListConditionalAlertsArgs { + domain: string; + /** Server-side substring filter on rule name. */ + query?: string; + limit?: number; +} + export interface SetUserFieldsArgs { domain: string; /** @@ -1334,6 +1353,67 @@ export class CommCareBackend { }); } + /** + * List Conditional Alerts on a domain. GET + * /a//messaging/conditional/?action=list_conditional_alerts&page=N&limit=Y. + * + * The ConditionalAlertListView has a dedicated AJAX list endpoint + * (verified against /tmp/ace-refs/hq/corehq/messaging/scheduling/views.py + * :653-665 `get_conditional_alerts_ajax_response`) that returns JSON + * `{rules: [{id, name, case_type, active, editable, ...}], total: N}`. + * + * Gated by REMINDERS_FRAMEWORK (Standard+ subscription). + * + * NB: `active` here is the rule's schedule's active flag (not the + * rule.active flag). The list view's docstring explains: + * "Therefore rule processing occurs unconditionally every time a + * rule is saved." For verifier purposes treat any rule whose + * schedule is active as "live." + * + * The CREATE counterpart is deferred — see notes in + * docs/connect-interviews/v1-acceptance.md. + */ + async listConditionalAlerts(args: ListConditionalAlertsArgs): Promise<{ alerts: ConditionalAlert[]; total: number }> { + return this.runWithSessionRetry(async (request) => { + const params = new URLSearchParams({ + action: 'list_conditional_alerts', + page: '1', + limit: String(args.limit ?? 100), + }); + if (args.query) params.set('query', args.query); + const path = `/a/${encodeURIComponent(args.domain)}/messaging/conditional/?${params.toString()}`; + const res = await request.get(`${this.opts.baseUrl}${path}`, { + maxRedirects: 0, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + Accept: 'application/json', + }, + }); + if (res.status() === 302) { + CommCareBackend.assertNotLoginRedirect(res, `commcare_list_conditional_alerts GET ${path}`); + } + if (res.status() === 404) { + throw new Error( + `commcare_list_conditional_alerts: 404 — domain ${args.domain} lacks REMINDERS_FRAMEWORK privilege (Standard+).`, + ); + } + if (res.status() !== 200) { + throw new Error(`commcare_list_conditional_alerts GET ${path} returned ${res.status()}: ${(await res.text()).slice(0, 300)}`); + } + const body = JSON.parse(await res.text()) as { rules?: any[]; total?: number }; + const alerts = (body.rules ?? []).map((r) => ({ + id: Number(r.id), + name: String(r.name ?? ''), + case_type: String(r.case_type ?? ''), + active: !!r.active, + editable: !!r.editable, + locked_for_editing: !!r.locked_for_editing, + progress_pct: Number(r.progress_pct ?? 0), + })); + return { alerts, total: body.total ?? alerts.length }; + }); + } + /** * Read the current custom-user-data field definition for a domain. * GETs /a//users/user_data/ and parses the