mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
fix(health-score): drop the mood pillar when the mood module is disabled
The Personal Health Score weighted the 0.20 mood-stability pillar unconditionally — the fast-path read mood entries regardless of the account's module state, so a user who turned the mood module off was still scored (and silently penalised or rewarded) by a pillar hidden everywhere else in the product. The dashboard already blanks the mood tile and the coach snapshot unions mood out; the score ring and the insights card pillar list did not. Thread a default-on `moodEnabled` flag through the fast-path. When the caller resolves the mood module as disabled the helper skips the mood read entirely and leaves the series empty, so `moodStability` returns null and the existing proportional null-redistribution re-scales the surviving pillars to sum to 100. The analytics route resolves the module server-side and threads the flag; the insights card hides the Mood row via a new SSR-safe `useModuleEnabled` accessor so it never advertises a pillar the account turned off. Golden fixtures pin mood-on vs mood-off to different, correctly re-weighted scores and assert the disabled read is never issued. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7c8d82f565
commit
3de53f549c
6 files changed
+224
-13
No files matched your search
@@ -78,6 +78,15 @@ export interface HealthScoreCardProps {
|
||||
* starts collapsed.
|
||||
*/
|
||||
initiallyExpanded?: boolean;
|
||||
/**
|
||||
* v1.18.0 R4 — when the `mood` module is disabled for the account the
|
||||
* server already drops the mood-stability pillar from the score (the
|
||||
* pillar arrives null-weighted). This hides the Mood row from the
|
||||
* component + provenance lists too so the card never advertises a
|
||||
* pillar the user turned off. Default-on so omitting it keeps the
|
||||
* four-row layout for the common (mood-enabled) case.
|
||||
*/
|
||||
moodEnabled?: boolean;
|
||||
}
|
||||
|
||||
type ComponentKey = keyof HealthScoreCardProps["components"];
|
||||
@@ -164,6 +173,7 @@ export function HealthScoreCard({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onAskCoach: _onAskCoach,
|
||||
initiallyExpanded = false,
|
||||
moodEnabled = true,
|
||||
}: HealthScoreCardProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
// The asOf timestamps render under the source pill as a tooltip
|
||||
@@ -198,9 +208,14 @@ export function HealthScoreCard({
|
||||
// without relying on visual proximity alone.
|
||||
const deltaExplainerId = useId();
|
||||
|
||||
const componentEntries = (
|
||||
Object.keys(components) as Array<ComponentKey>
|
||||
).map((key) => ({
|
||||
// v1.18.0 R4 — drop the Mood pillar from the card's row lists when the
|
||||
// module is disabled. The server already null-weights it in the score;
|
||||
// hiding the row keeps the card from naming a pillar the account turned
|
||||
// off (it would otherwise render as a permanently empty "Mood" line).
|
||||
const visibleKeys = (Object.keys(components) as Array<ComponentKey>).filter(
|
||||
(key) => moodEnabled || key !== "mood",
|
||||
);
|
||||
const componentEntries = visibleKeys.map((key) => ({
|
||||
key,
|
||||
label: t(COMPONENT_LABEL_KEY[key]),
|
||||
value: components[key].value,
|
||||
@@ -211,6 +226,7 @@ export function HealthScoreCard({
|
||||
// null values sink to the bottom (`weight * 0 === 0`); the tie-break
|
||||
// is the alphabetical key order so determinism holds across renders.
|
||||
const provenanceRows = COMPONENT_ORDER
|
||||
.filter((key) => moodEnabled || key !== "mood")
|
||||
.map((key) => {
|
||||
const c = components[key];
|
||||
const inferredSource: HealthScoreComponentSource =
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { formatRelativeTime } from "@/lib/i18n/relative-time";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useDisableCoach } from "@/hooks/use-disable-coach";
|
||||
import { useModuleEnabled } from "@/hooks/use-module-enabled";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SuggestedPrompts } from "./suggested-prompts";
|
||||
import {
|
||||
@@ -149,6 +150,11 @@ export function HeroStrip({
|
||||
// tests render the strip without a QueryClient.
|
||||
const disableCoach = useDisableCoach();
|
||||
const coachEnabled = flags.coach && !disableCoach;
|
||||
// v1.18.0 R4 — mood-module state so the score card hides its Mood row
|
||||
// when the account turned the module off (the server already drops the
|
||||
// pillar from the number itself). SSR-safe + default-on, matching the
|
||||
// disabled-allowlist gate.
|
||||
const moodEnabled = useModuleEnabled("mood");
|
||||
const greetingKey = resolveGreetingKey(now ?? new Date());
|
||||
const greetingBase = t(greetingKey);
|
||||
const greeting = userName ? `${greetingBase}, ${userName}` : greetingBase;
|
||||
@@ -339,6 +345,7 @@ export function HeroStrip({
|
||||
band={healthScore.band}
|
||||
components={healthScore.components}
|
||||
delta={healthScore.delta}
|
||||
moodEnabled={moodEnabled}
|
||||
onAskCoach={
|
||||
// v1.4.37 W5 — short-circuit the prop drilling even
|
||||
// though `HealthScoreCard` retired its inline button in
|
||||
|
||||
Reference in new issue
Block a user