mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(insights): honest discovery footer, commensurable better-days ranking, settings tap-target + prop cleanup
- Mood discovery footer no longer pairs a mood-only numerator with the full behaviour-by-outcome denominator. Reword it to surface the mood relationships out of all day-to-day pairs the engine tested across every metric, so the count it reports is honest about its scope. Update all six locales. - Rank the better-days board on a comparable scale: standardize the tag mood-point delta into a Cohen's-d effect (delta over the pooled with/without mood SD, clamped to [0,1]) so it sits alongside a metric |r| instead of the old |delta|/2 heuristic. Fall back to |delta|/2 only when the pooled SD is undefined. The pooled SD is ranking-only and is stripped before the row enters the snapshot payload; the raw delta and r the cards show are unchanged. - Normalize the Withings and Whoop resume buttons to the min-h-11 scale token, dropping the arbitrary min-h-[44px] spelling. - Give the API token copy button a 44px tap target on small viewports, scaling back to the tighter icon size from sm up. - Drop the dead cached prop from InsightStatusCard and every caller; the card has carried no cached affordance since the badge was removed.
This commit is contained in:
1 parent
766b395900
commit
874e286379
23 files changed
+162
-41
No files matched your search
+1
-1
@@ -2564,7 +2564,7 @@
|
||||
"discovery": {
|
||||
"title": "Entdeckte Muster",
|
||||
"description": "Tag-zu-Tag-Zusammenhänge rund um deine Stimmung, die auch nach Korrektur für viele gleichzeitige Tests bestehen.",
|
||||
"footer": "{discovered} von {tested} getesteten Zusammenhängen haben die Korrektur für Fehlentdeckungen überstanden.",
|
||||
"footer": "Herausgefiltert aus {tested} Tag-zu-Tag-Paaren, die über all deine Werte getestet wurden, nach der Korrektur für Fehlentdeckungen.",
|
||||
"empty": "Bisher hat kein Stimmungszusammenhang die statistische Hürde genommen. Logge weiter.",
|
||||
"disclaimer": "Statistisch gefiltert, aber weiterhin beobachtend – ein Muster, das einen Blick wert ist, keine Ursache.",
|
||||
"moodUp": "Höheres {factor} geht in deinen Daten tendenziell mit besserer Stimmung am Folgetag einher.",
|
||||
|
||||
+1
-1
@@ -2564,7 +2564,7 @@
|
||||
"discovery": {
|
||||
"title": "Discovered patterns",
|
||||
"description": "Day-to-day relationships involving your mood that hold up after correcting for testing many pairs at once.",
|
||||
"footer": "{discovered} of {tested} relationships tested survived the false-discovery correction.",
|
||||
"footer": "Surfaced from {tested} day-to-day pairs tested across all your metrics, after the false-discovery correction.",
|
||||
"empty": "No mood relationships cleared the statistical bar yet. Keep logging.",
|
||||
"disclaimer": "Statistically filtered, but still observational — a pattern worth watching, not a cause.",
|
||||
"moodUp": "Higher {factor} tends to go with higher next-day mood in your data.",
|
||||
|
||||
+1
-1
@@ -2564,7 +2564,7 @@
|
||||
"discovery": {
|
||||
"title": "Patrones descubiertos",
|
||||
"description": "Relaciones de un día a otro con tu estado de ánimo que se mantienen tras corregir el haber probado muchos pares a la vez.",
|
||||
"footer": "{discovered} de {tested} relaciones probadas superaron la corrección de falsos descubrimientos.",
|
||||
"footer": "Extraídas de {tested} pares de un día a otro probados en todas tus métricas, tras la corrección de falsos descubrimientos.",
|
||||
"empty": "Todavía ninguna relación de ánimo superó el umbral estadístico. Sigue registrando.",
|
||||
"disclaimer": "Filtrado estadísticamente, pero aún observacional: un patrón que vale la pena observar, no una causa.",
|
||||
"moodUp": "Un mayor {factor} suele coincidir con mejor ánimo al día siguiente en tus datos.",
|
||||
|
||||
+1
-1
@@ -2564,7 +2564,7 @@
|
||||
"discovery": {
|
||||
"title": "Tendances détectées",
|
||||
"description": "Des relations d'un jour à l'autre autour de votre humeur qui tiennent après correction des tests multiples.",
|
||||
"footer": "{discovered} relations sur {tested} testées ont passé la correction des fausses découvertes.",
|
||||
"footer": "Issues de {tested} paires d'un jour à l'autre testées sur l'ensemble de vos mesures, après correction des fausses découvertes.",
|
||||
"empty": "Aucune relation d'humeur n'a encore franchi le seuil statistique. Continuez à enregistrer.",
|
||||
"disclaimer": "Filtré statistiquement, mais toujours observationnel : une tendance à surveiller, pas une cause.",
|
||||
"moodUp": "Un {factor} plus élevé tend à coïncider avec une meilleure humeur le lendemain dans vos données.",
|
||||
|
||||
+1
-1
@@ -2564,7 +2564,7 @@
|
||||
"discovery": {
|
||||
"title": "Schemi rilevati",
|
||||
"description": "Relazioni giorno per giorno legate al tuo umore che reggono dopo aver corretto per i test multipli.",
|
||||
"footer": "{discovered} relazioni su {tested} testate hanno superato la correzione per le false scoperte.",
|
||||
"footer": "Estratte da {tested} coppie giorno per giorno testate su tutte le tue misure, dopo la correzione per le false scoperte.",
|
||||
"empty": "Nessuna relazione sull'umore ha ancora superato la soglia statistica. Continua a registrare.",
|
||||
"disclaimer": "Filtrato statisticamente, ma sempre osservazionale: uno schema da tenere d'occhio, non una causa.",
|
||||
"moodUp": "Un {factor} più alto tende a coincidere con un umore migliore il giorno dopo nei tuoi dati.",
|
||||
|
||||
+1
-1
@@ -2564,7 +2564,7 @@
|
||||
"discovery": {
|
||||
"title": "Wykryte wzorce",
|
||||
"description": "Relacje dzień po dniu dotyczące Twojego nastroju, które utrzymują się po korekcie wielokrotnych testów.",
|
||||
"footer": "{discovered} z {tested} testowanych relacji przeszło korektę fałszywych odkryć.",
|
||||
"footer": "Wyłonione z {tested} par dzień po dniu testowanych na wszystkich Twoich wskaźnikach, po korekcie fałszywych odkryć.",
|
||||
"empty": "Żadna relacja nastroju nie przekroczyła jeszcze progu statystycznego. Notuj dalej.",
|
||||
"disclaimer": "Przefiltrowane statystycznie, ale wciąż obserwacyjne — wzorzec warty uwagi, nie przyczyna.",
|
||||
"moodUp": "Wyższy {factor} zwykle wiąże się z lepszym nastrojem następnego dnia w Twoich danych.",
|
||||
|
||||
@@ -138,7 +138,6 @@ export default function InsightsBlutdruckPage() {
|
||||
icon={<HeartPulse className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -141,7 +141,6 @@ export default function InsightsBmiPage() {
|
||||
icon={<Ruler className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -288,7 +288,6 @@ export default function InsightsMedikamentePage() {
|
||||
icon={<Pill className="h-5 w-5" />}
|
||||
text={status?.summary ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -118,7 +118,6 @@ export default function InsightsStimmungPage() {
|
||||
icon={<Smile className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -151,7 +151,6 @@ export default function InsightsPulsPage() {
|
||||
icon={<Heart className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -116,7 +116,6 @@ export default function InsightsGewichtPage() {
|
||||
icon={<Scale className="h-5 w-5" />}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isStatusLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -24,7 +24,6 @@ const baseProps = {
|
||||
icon: null,
|
||||
text: "Your pulse is stable.",
|
||||
hasProvider: true,
|
||||
cached: false,
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ const baseProps = {
|
||||
title: "Pulse",
|
||||
icon: null,
|
||||
hasProvider: true,
|
||||
cached: false,
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
@@ -90,14 +89,14 @@ describe("<InsightStatusCard>", () => {
|
||||
expect(html).toContain("No analysis yet.");
|
||||
});
|
||||
|
||||
it("never surfaces a cached badge, even when the text came from the warm cache", () => {
|
||||
// v1.11.5 — the top-right "cached" label was removed. The caching
|
||||
// behaviour is unchanged (the prop is still threaded), but the card
|
||||
// no longer announces it, so the assessment reads as authoritative.
|
||||
it("never surfaces a cached badge — the card has no cached affordance", () => {
|
||||
// v1.11.5 — the top-right "cached" label was removed and the dead
|
||||
// `cached` prop was dropped from the contract. The caching behaviour
|
||||
// upstream is unchanged; the card simply never announced it, so the
|
||||
// assessment reads as authoritative.
|
||||
const html = render(
|
||||
<InsightStatusCard
|
||||
{...baseProps}
|
||||
cached
|
||||
text="Pulse stayed inside the band."
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -15,14 +15,6 @@ interface InsightStatusCardProps {
|
||||
icon: React.ReactNode;
|
||||
text: string | null;
|
||||
hasProvider: boolean;
|
||||
/**
|
||||
* Whether the rendered text came from the warm cache rather than a
|
||||
* fresh generation. Retained on the contract so every caller can keep
|
||||
* threading it, but the card no longer surfaces a "cached" badge — the
|
||||
* label devalued the assessment and the caching is an implementation
|
||||
* detail the user does not need to see.
|
||||
*/
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
loading?: boolean;
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,6 @@ export function MetricStatusCard({
|
||||
icon={icon}
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
cached={status?.cached ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
loading={isLoading}
|
||||
preparing={status?.preparing ?? false}
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("<MoodDiscoveredRelations>", () => {
|
||||
expect(html).toContain("lower next-day");
|
||||
});
|
||||
|
||||
it("filters to mood pairs and shows the honest tested footer", () => {
|
||||
it("filters to mood pairs and shows the honest full-family footer", () => {
|
||||
const html = renderWith(
|
||||
[
|
||||
pair({ behaviour: "TIME_IN_DAYLIGHT", outcome: "MOOD" }),
|
||||
@@ -122,7 +122,8 @@ describe("<MoodDiscoveredRelations>", () => {
|
||||
expect((html.match(/data-slot="mood-discovered-pair"/g) ?? []).length).toBe(
|
||||
1,
|
||||
);
|
||||
// footer reports 1 mood pair of 12 tested
|
||||
expect(html).toContain("1 of 12");
|
||||
// Footer's denominator is the full behaviour×outcome family the engine
|
||||
// tested, not the mood-only subset — so it must report 12, not 1 of 1.
|
||||
expect(html).toContain("from 12 day-to-day pairs");
|
||||
});
|
||||
});
|
||||
@@ -124,7 +124,6 @@ export function MoodDiscoveredRelations() {
|
||||
</ul>
|
||||
<p className="text-muted-foreground text-[11px]">
|
||||
{t("insights.mood.discovery.footer", {
|
||||
discovered: moodPairs.length,
|
||||
tested: data.pairsTested,
|
||||
})}
|
||||
</p>
|
||||
|
||||
@@ -303,7 +303,7 @@ function ApiTokensCard() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
className="min-h-11 min-w-11 shrink-0 sm:h-9 sm:w-9"
|
||||
onClick={() => void handleCopyToken()}
|
||||
aria-label={t("settings.tokenCopy")}
|
||||
data-slot="settings-api-token-copy"
|
||||
|
||||
@@ -464,7 +464,7 @@ function WithingsCard({
|
||||
onClick={() => resume.mutate()}
|
||||
disabled={resume.isPending}
|
||||
data-testid="withings-resume-button"
|
||||
className="min-h-[44px] sm:min-h-0"
|
||||
className="min-h-11"
|
||||
>
|
||||
{resume.isPending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
@@ -897,7 +897,7 @@ function WhoopCard({
|
||||
onClick={() => resume.mutate()}
|
||||
disabled={resume.isPending}
|
||||
data-testid="whoop-resume-button"
|
||||
className="min-h-[44px] sm:min-h-0"
|
||||
className="min-h-11"
|
||||
>
|
||||
{resume.isPending ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
|
||||
@@ -638,7 +638,8 @@ describe("computeBetterDays", () => {
|
||||
withoutDays: 10,
|
||||
withAvg: 4.4,
|
||||
withoutAvg: 2.4,
|
||||
delta: 2, // effect 1.0 (capped)
|
||||
delta: 2,
|
||||
pooledSd: 1, // |d| = 2 → standardized effect 1.0 (capped)
|
||||
pValue: 0.001,
|
||||
confidence: "high",
|
||||
},
|
||||
@@ -680,6 +681,7 @@ describe("computeBetterDays", () => {
|
||||
withAvg: 2.2,
|
||||
withoutAvg: 3.8,
|
||||
delta: -1.6,
|
||||
pooledSd: 0.9,
|
||||
pValue: 0.01,
|
||||
confidence: "medium",
|
||||
},
|
||||
@@ -698,6 +700,88 @@ describe("computeBetterDays", () => {
|
||||
expect(board[0].delta).toBe(-1.6);
|
||||
});
|
||||
|
||||
it("ranks tags by the standardized (Cohen's-d) effect, not the raw delta", () => {
|
||||
// "tight" has the smaller raw delta but a far smaller pooled SD, so its
|
||||
// standardized effect (0.8 / 0.4 = 2.0 → capped 1.0) beats "wide"
|
||||
// (1.2 / 2.0 = 0.6). The legacy |delta|/2 heuristic would have ordered
|
||||
// them the other way (0.6 vs 0.4), so this pins the new behaviour.
|
||||
const tagInfluence: TagInfluence = {
|
||||
flat: [
|
||||
{
|
||||
tag: "wide",
|
||||
labelKey: null,
|
||||
categoryKey: null,
|
||||
icon: null,
|
||||
withDays: 10,
|
||||
withoutDays: 10,
|
||||
withAvg: 4.2,
|
||||
withoutAvg: 3.0,
|
||||
delta: 1.2,
|
||||
pooledSd: 2.0,
|
||||
pValue: 0.02,
|
||||
confidence: "medium",
|
||||
},
|
||||
{
|
||||
tag: "tight",
|
||||
labelKey: null,
|
||||
categoryKey: null,
|
||||
icon: null,
|
||||
withDays: 10,
|
||||
withoutDays: 10,
|
||||
withAvg: 4.0,
|
||||
withoutAvg: 3.2,
|
||||
delta: 0.8,
|
||||
pooledSd: 0.4,
|
||||
pValue: 0.001,
|
||||
confidence: "high",
|
||||
},
|
||||
],
|
||||
structured: [],
|
||||
};
|
||||
const board = computeBetterDays(tagInfluence, {
|
||||
sleep: emptyCorr,
|
||||
steps: emptyCorr,
|
||||
pulse: emptyCorr,
|
||||
weight: emptyCorr,
|
||||
bloodPressureSystolic: emptyCorr,
|
||||
});
|
||||
expect(board.map((f) => f.key)).toEqual(["tight", "wide"]);
|
||||
// Raw deltas shown in the UI are untouched by the ranking change.
|
||||
expect(board[0].delta).toBe(0.8);
|
||||
expect(board[1].delta).toBe(1.2);
|
||||
});
|
||||
|
||||
it("falls back to the |delta|/2 heuristic when a tag has no pooled SD", () => {
|
||||
const tagInfluence: TagInfluence = {
|
||||
flat: [
|
||||
{
|
||||
tag: "constant",
|
||||
labelKey: null,
|
||||
categoryKey: null,
|
||||
icon: null,
|
||||
withDays: 10,
|
||||
withoutDays: 10,
|
||||
withAvg: 4.0,
|
||||
withoutAvg: 2.0,
|
||||
delta: 2,
|
||||
pooledSd: null, // both groups perfectly constant
|
||||
pValue: 1,
|
||||
confidence: "low",
|
||||
},
|
||||
],
|
||||
structured: [],
|
||||
};
|
||||
const board = computeBetterDays(tagInfluence, {
|
||||
sleep: emptyCorr,
|
||||
steps: emptyCorr,
|
||||
pulse: emptyCorr,
|
||||
weight: emptyCorr,
|
||||
bloodPressureSystolic: emptyCorr,
|
||||
});
|
||||
expect(board).toHaveLength(1);
|
||||
expect(board[0].effectSize).toBe(1); // min(1, |2|/2)
|
||||
});
|
||||
|
||||
it("returns an empty board when nothing clears the gates", () => {
|
||||
const board = computeBetterDays(
|
||||
{ flat: [], structured: [] },
|
||||
@@ -723,6 +807,7 @@ describe("computeBetterDays", () => {
|
||||
withAvg: 4,
|
||||
withoutAvg: 3,
|
||||
delta: 1 - i * 0.01,
|
||||
pooledSd: 1,
|
||||
pValue: 0.02,
|
||||
confidence: "low" as const,
|
||||
}));
|
||||
|
||||
@@ -313,6 +313,13 @@ export interface TagInfluenceRow {
|
||||
withoutAvg: number;
|
||||
/** withAvg − withoutAvg, rounded. Positive = higher mood with the tag. */
|
||||
delta: number;
|
||||
/**
|
||||
* Pooled mood SD across the with/without groups (Cohen's-d denominator).
|
||||
* `null` when neither group has testable spread (both constant). Lets the
|
||||
* board standardize the raw delta into a unitless effect comparable to a
|
||||
* Pearson |r|. Not surfaced in the UI — ranking only.
|
||||
*/
|
||||
pooledSd: number | null;
|
||||
/** Welch two-sided p-value for the difference of means. */
|
||||
pValue: number;
|
||||
/** Discrete confidence band derived from p-value + per-group sample size. */
|
||||
@@ -431,6 +438,22 @@ function influenceForTag(
|
||||
const delta = withAvg - withoutAvg;
|
||||
const minGroupDays = Math.min(withVals.length, withoutVals.length);
|
||||
|
||||
// Pooled SD (Cohen's-d denominator) so the board can standardize this raw
|
||||
// mood-point delta into a unitless effect comparable to a Pearson |r|.
|
||||
// Unbiased (n−1) per-group variances pooled on (nWith + nWithout − 2) df;
|
||||
// null when there is no testable spread (both groups perfectly constant).
|
||||
const nWith = withVals.length;
|
||||
const nWithout = withoutVals.length;
|
||||
const varWith =
|
||||
withVals.reduce((s, v) => s + (v - withAvg) ** 2, 0) / (nWith - 1);
|
||||
const varWithout =
|
||||
withoutVals.reduce((s, v) => s + (v - withoutAvg) ** 2, 0) /
|
||||
(nWithout - 1);
|
||||
const pooledVar =
|
||||
((nWith - 1) * varWith + (nWithout - 1) * varWithout) /
|
||||
(nWith + nWithout - 2);
|
||||
const pooledSd = pooledVar > 0 ? Math.sqrt(pooledVar) : null;
|
||||
|
||||
const pValue = welch.status === "ok" ? welch.pValue : 1;
|
||||
// A zero delta with no spread is not an "influence" — drop it so the
|
||||
// board never shows a 0.0 row.
|
||||
@@ -446,6 +469,7 @@ function influenceForTag(
|
||||
withAvg: round(withAvg, 2),
|
||||
withoutAvg: round(withoutAvg, 2),
|
||||
delta: round(delta, 2),
|
||||
pooledSd: pooledSd === null ? null : round(pooledSd, 3),
|
||||
pValue,
|
||||
confidence: influenceConfidence(pValue, minGroupDays),
|
||||
};
|
||||
@@ -586,8 +610,16 @@ function metricConfidence(
|
||||
* non-"keine" strength (|r| ≥ 0.2) are folded in — a near-zero r is not
|
||||
* an association.
|
||||
*
|
||||
* Ranking is by `effectSize` desc (|r| for metrics, |delta|/2 capped for
|
||||
* tags), then by confidence, then key for stability. Observational only.
|
||||
* Ranking is by `effectSize` desc, then confidence, then key for stability.
|
||||
* The two sources are put on one comparable scale: metric rows use |r|
|
||||
* (already unitless in [0,1]); tag rows use a standardized effect — the raw
|
||||
* mood-point `delta` divided by the pooled with/without mood SD (Cohen's d),
|
||||
* clamped to [0,1]. A |d| ≥ 1 (a mean shift of one full SD) saturates the
|
||||
* scale, mirroring an |r| near its ceiling, so neither source dominates the
|
||||
* board for scale reasons alone. When a tag has no pooled SD (both groups
|
||||
* perfectly constant), it falls back to the legacy |delta|/2 heuristic — a
|
||||
* rare degenerate case that can't be standardized. The raw delta / r the UI
|
||||
* shows is unchanged; this only governs the sort order. Observational only.
|
||||
*/
|
||||
export function computeBetterDays(
|
||||
tagInfluence: TagInfluence,
|
||||
@@ -606,7 +638,13 @@ export function computeBetterDays(
|
||||
direction: row.delta >= 0 ? "up" : "down",
|
||||
n: Math.min(row.withDays, row.withoutDays),
|
||||
confidence: row.confidence,
|
||||
effectSize: Math.min(1, Math.abs(row.delta) / 2),
|
||||
// Cohen's-d standardization so the tag effect is commensurable with a
|
||||
// metric |r|; fall back to the legacy |delta|/2 only when the pooled
|
||||
// SD is unavailable (both groups perfectly constant).
|
||||
effectSize:
|
||||
row.pooledSd && row.pooledSd > 0
|
||||
? Math.min(1, Math.abs(row.delta) / row.pooledSd)
|
||||
: Math.min(1, Math.abs(row.delta) / 2),
|
||||
delta: row.delta,
|
||||
r: null,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { pearsonCorrelation } from "@/lib/analytics/correlations";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import {
|
||||
type TagInfluenceRow,
|
||||
MOOD_GREEN_MAX,
|
||||
MOOD_GREEN_MIN,
|
||||
MOOD_ORANGE_MAX,
|
||||
@@ -47,6 +48,17 @@ import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { toBerlinDayKey } from "@/lib/tz/resolver";
|
||||
|
||||
/**
|
||||
* Drop the ranking-only `pooledSd` before a tag-influence row enters the
|
||||
* snapshot payload — it exists solely to standardize the better-days sort
|
||||
* and has no place in the model prompt or the UI.
|
||||
*/
|
||||
function stripRankingOnly(row: TagInfluenceRow): Omit<TagInfluenceRow, "pooledSd"> {
|
||||
const { pooledSd: _pooledSd, ...rest } = row;
|
||||
void _pooledSd;
|
||||
return rest;
|
||||
}
|
||||
|
||||
export async function generateMoodStatusForUser(
|
||||
userId: string,
|
||||
options?: {
|
||||
@@ -352,10 +364,13 @@ export async function generateMoodStatusForUser(
|
||||
tagInfluence:
|
||||
tagInfluence.flat.length > 0 || tagInfluence.structured.length > 0
|
||||
? {
|
||||
flat: tagInfluence.flat.length > 0 ? tagInfluence.flat : null,
|
||||
flat:
|
||||
tagInfluence.flat.length > 0
|
||||
? tagInfluence.flat.map(stripRankingOnly)
|
||||
: null,
|
||||
structured:
|
||||
tagInfluence.structured.length > 0
|
||||
? tagInfluence.structured
|
||||
? tagInfluence.structured.map(stripRankingOnly)
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
|
||||
Reference in new issue
Block a user