Health probe: dump all relationships for a given contact, flag direction issues

This commit is contained in:
Joel Brock
2026-05-09 21:08:30 -07:00
parent 2e9d3855ba
commit da830af533

View File

@@ -286,6 +286,72 @@ async function probeChecksumWithSampleContact(): Promise<CheckResult> {
};
}
async function probeContactRelationships(cid: string): Promise<CheckResult> {
// 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());
}