From da830af5339258f573d343c60a5cd3fa7fd32be9 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Sat, 9 May 2026 21:08:30 -0700 Subject: [PATCH] Health probe: dump all relationships for a given contact, flag direction issues --- app/api/health/route.ts | 74 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 2b6a9dd..82c07df 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -286,6 +286,72 @@ async function probeChecksumWithSampleContact(): Promise { }; } +async function probeContactRelationships(cid: string): Promise { + // Dumps every active relationship involving this contact, regardless of + // direction. Useful when /api/data returns 404 — tells you whether the + // relationship exists at all and which side this contact is on. + try { + const res = await civi<{ + id: number; + contact_id_a: number; + contact_id_b: number; + "relationship_type_id:name_a_b": string; + "relationship_type_id:name_b_a": string; + is_active: boolean; + }>("Relationship", "get", { + select: [ + "id", + "contact_id_a", + "contact_id_b", + "relationship_type_id:name_a_b", + "relationship_type_id:name_b_a", + "is_active", + ], + where: [ + ["OR", [["contact_id_a", "=", Number(cid)], ["contact_id_b", "=", Number(cid)]]], + ["is_active", "=", true], + ], + limit: 50, + }); + const rows = res.values ?? []; + if (rows.length === 0) { + return { + check: "contact_relationships_dump", + ok: false, + detail: `Contact ${cid} has no active relationships at all. Add a relationship of type "${FORM_CONTACT_RELATIONSHIP}" with this contact on side A (Individual) and the target Organization on side B.`, + }; + } + const lines = rows.map((r) => { + const sideA = String(r.contact_id_a) === cid ? "★A" : "·A"; + const sideB = String(r.contact_id_b) === cid ? "★B" : "·B"; + return ` ${sideA}=${r.contact_id_a} ${sideB}=${r.contact_id_b} type="${r["relationship_type_id:name_a_b"]}" / inverse="${r["relationship_type_id:name_b_a"]}"`; + }); + const usable = rows.find( + (r) => + String(r.contact_id_a) === cid && + r["relationship_type_id:name_a_b"] === FORM_CONTACT_RELATIONSHIP, + ); + return { + check: "contact_relationships_dump", + ok: Boolean(usable), + detail: + `Active relationships involving contact ${cid} (★ marks the side this contact is on):\n` + + lines.join("\n") + + "\n\n" + + (usable + ? `✓ Found a usable "${FORM_CONTACT_RELATIONSHIP}" with contact ${cid} on side A (id=${usable.id}, org=${usable.contact_id_b}).` + : `✗ No relationship matches: type="${FORM_CONTACT_RELATIONSHIP}" with contact ${cid} on side A. ` + + `If you see the right relationship above with contact ${cid} on side B, the relationship is stored backwards — delete and re-add with the Individual chosen first, OR change FORM_CONTACT_RELATIONSHIP in config/form.ts to the inverse name (name_b_a).`), + }; + } catch (e) { + return { + check: "contact_relationships_dump", + ok: false, + detail: e instanceof Error ? e.message : String(e), + }; + } +} + export async function GET(req: Request) { const env = envSummary(); const summary = { mode: env.ok ? "live" : "stub" }; @@ -323,7 +389,11 @@ export async function GET(req: Request) { }); } - // If a sample cid+cs was provided, verify that specific checksum. + // If a sample cid+cs was provided, verify that specific checksum + dump + // the contact's relationships. + if (cid) { + checks.push(await probeContactRelationships(cid)); + } if (cid && cs) { try { const ok = await verifyChecksum(cid, cs); @@ -341,7 +411,7 @@ export async function GET(req: Request) { detail: e instanceof Error ? e.message : String(e), }); } - } else { + } else if (!cid) { checks.push(await probeChecksumWithSampleContact()); }