From 0c3c1564c38f4cc62365c7ad845fe79f67c9a7c4 Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 16 Oct 2026 11:36:58 -0500 Subject: [PATCH] fix(health): email.readiness says what is missing and where to set it The readiness message was only "Email configuration incomplete" plus a code. It now names each gap in plain words with the Email app path to fix it, and notes that transactional email is unaffected by marketing-only gaps. The failedChecks detail is unchanged; howToFix carries the same text. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../qbits/email/health/EmailHealthChecks.java | 35 +++++++++++++++++-- .../email/health/EmailHealthChecksTest.java | 2 ++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aec729d..fb539d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and - `EmailTablesGuardrailTest`: every table is used by runtime code, seeded or listed as starting empty with a reason, and has help. ### Fixed +- The `email.readiness` health message now names each missing item in plain words and where to set it (for example "Brand postal address is not set (Email > Brand > open the default brand > Postal address)") and says transactional email is unaffected by marketing-only gaps. The `failedChecks` detail is unchanged; a new `howToFix` detail lists the same text. - Derive the qbit metadata version from the Maven artifact version so released artifacts report their release version. - `email_template.ownerQbit` was stored but never read, and the built-in template did not set it. The built-in now names `qbit-email`, the field is read-only with help, and messages sent from a template name its owner qbit as `senderQbit` unless the request names one. diff --git a/src/main/java/com/kingsrook/qbits/email/health/EmailHealthChecks.java b/src/main/java/com/kingsrook/qbits/email/health/EmailHealthChecks.java index b72be21..e1c64e9 100644 --- a/src/main/java/com/kingsrook/qbits/email/health/EmailHealthChecks.java +++ b/src/main/java/com/kingsrook/qbits/email/health/EmailHealthChecks.java @@ -27,6 +27,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import com.kingsrook.qbits.email.EmailRuntime; import com.kingsrook.qbits.email.api.EmailApi; import com.kingsrook.qbits.email.api.EmailApiCategory; import com.kingsrook.qbits.email.api.EmailApiReadinessCheck; @@ -134,8 +135,38 @@ public class EmailHealthChecks implements HealthCheckSupplier ready &= readiness.ready(); failed.addAll(readiness.failedChecks()); } - return ((ready ? HealthCheckOutcome.up("Email configuration ready") : HealthCheckOutcome.degraded("Email configuration incomplete")) - .withDetails(new HealthDetails().withValue("failedChecks", failed.stream().map(Enum::name).collect(Collectors.joining(","))))); + if(ready) + { + return (HealthCheckOutcome.up("Email configuration ready").withDetails(new HealthDetails().withValue("failedChecks", ""))); + } + var config = EmailRuntime.config(input.getQInstance()); + String app = config == null || config.getAppLabel() == null ? "Email" : config.getAppLabel(); + List fixes = failed.stream().map(check -> fix(check, app)).toList(); + String message = "Email configuration incomplete: " + String.join(" ", fixes); + String details = String.join("\n", fixes); + return (HealthCheckOutcome.degraded(message.length() > 500 ? message.substring(0, 500) : message) + .withDetails(new HealthDetails().withValue("failedChecks", failed.stream().map(Enum::name).collect(Collectors.joining(","))) + .withValue("howToFix", details.length() > 4000 ? details.substring(0, 4000) : details))); + } + + + + /******************************************************************************* + ** Plain-words statement of one missing prerequisite and where to set it. + *******************************************************************************/ + private static String fix(EmailApiReadinessCheck check, String app) + { + return (switch(check) + { + case NOT_INSTALLED -> "The Email qbit is not installed."; + case PROVIDER_MISSING -> "No enabled default email provider (" + app + " > Provider: add one and tick Enabled and Is Default)."; + case PROVIDER_INVALID -> "The default email provider settings are invalid (" + app + " > Provider: open the default provider and correct it)."; + case PHI_PROVIDER -> "PHI email needs a PHI-enabled provider with open and click tracking off (" + app + " > Provider)."; + case PUBLIC_BASE_URL -> "Public base URL is not set (the publicBaseUrl setting in the Email qbit config). Needed for marketing unsubscribe links; transactional email is unaffected."; + case UNSUBSCRIBE_KEY -> "The QQQ_EMAIL_UNSUBSCRIBE_KEY secret is not set (deployment environment variable). Needed for marketing unsubscribe links; transactional email is unaffected."; + case BRAND_POSTAL_ADDRESS -> "Brand postal address is not set (" + app + " > Brand > open the default brand > Postal address). Needed for marketing email footers; transactional email is unaffected."; + case BODY_KEY -> "The QQQ_EMAIL_BODY_KEY secret is not set (deployment environment variable). Needed for PHI email."; + }); } diff --git a/src/test/java/com/kingsrook/qbits/email/health/EmailHealthChecksTest.java b/src/test/java/com/kingsrook/qbits/email/health/EmailHealthChecksTest.java index 728e072..fd47c63 100644 --- a/src/test/java/com/kingsrook/qbits/email/health/EmailHealthChecksTest.java +++ b/src/test/java/com/kingsrook/qbits/email/health/EmailHealthChecksTest.java @@ -165,6 +165,8 @@ class EmailHealthChecksTest assertThat(outcome.getStatus()).isEqualTo(HealthStatus.DEGRADED); assertThat(outcome.getDetails().getValues().get("failedChecks").toString().split(",")) .containsExactly("PUBLIC_BASE_URL", "BRAND_POSTAL_ADDRESS"); + assertThat(outcome.getMessage()).contains("Brand postal address is not set").contains("Brand > open the default brand > Postal address").contains("transactional email is unaffected").hasSizeLessThanOrEqualTo(500); + assertThat(outcome.getDetails().getValues().get("howToFix").toString()).contains("Public base URL is not set").contains("Brand postal address is not set"); configureMarketing(); assertThat(run(readinessCheck()).getStatus()).isEqualTo(HealthStatus.UP); update("enabled", false); -- 2.54.0