mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(mood): entry edits spare archived-tag history and insights speak the custom labels
Editing any field of a mood entry re-sent its tag set, and archived tag keys can no longer resolve — the diff deleted the entry's links to them invisibly, against the archive contract the tag suite just shipped. Submitted tag keys now govern active links only; links to archived tags survive every edit, and the explicit purge stays the one removal path. The insights surfaces — tag breakdown, influence, better days, crosstab, the model snapshot and the narratives — render the decrypted custom-tag label instead of the raw key, decrypted once per tag server-side.
This commit is contained in:
1 parent
f3dda21917
commit
97822e9e6d
14 files changed
+372
-22
No files matched your search
@@ -55,6 +55,25 @@ describe("<MoodBetterDays>", () => {
|
||||
expect(html).toContain('data-direction="up"');
|
||||
});
|
||||
|
||||
it("prefers a custom tag's decrypted label over its raw labelKey", () => {
|
||||
const html = render(
|
||||
<MoodBetterDays
|
||||
factors={[
|
||||
{
|
||||
...tagFactor,
|
||||
key: "custom:abc-123",
|
||||
labelKey: "custom:abc-123",
|
||||
label: "Migraine",
|
||||
categoryKey: "custom",
|
||||
icon: "Tag",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("Migraine");
|
||||
expect(html).not.toContain("custom:abc-123");
|
||||
});
|
||||
|
||||
it("shows a metric factor with its labelled name and r value", () => {
|
||||
const html = render(<MoodBetterDays factors={[metricFactor]} />);
|
||||
// sleep maps to the localized correlation title, not the raw key
|
||||
|
||||
@@ -79,6 +79,26 @@ describe("<MoodTagInfluence>", () => {
|
||||
expect(html).not.toContain("charts.weekdaysFull.mon");
|
||||
});
|
||||
|
||||
it("prefers a custom tag's decrypted label over its raw labelKey", () => {
|
||||
// A custom tag's labelKey mirrors its raw `custom:<uuid>` key — the
|
||||
// decrypted label must win or the row prints the uuid.
|
||||
const html = render(
|
||||
<MoodTagInfluence
|
||||
rows={[
|
||||
flatRow({
|
||||
tag: "custom:abc-123",
|
||||
labelKey: "custom:abc-123",
|
||||
label: "Migraine",
|
||||
categoryKey: "custom",
|
||||
icon: "Tag",
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("Migraine");
|
||||
expect(html).not.toContain("custom:abc-123");
|
||||
});
|
||||
|
||||
it("does not render a local generic disclaimer", () => {
|
||||
const html = render(
|
||||
<MoodTagInfluence rows={[flatRow(), flatRow({ tag: "social" })]} />,
|
||||
|
||||
@@ -23,6 +23,13 @@ export interface MoodBetterDayFactor {
|
||||
source: "tag" | "metric";
|
||||
key: string;
|
||||
labelKey: string | null;
|
||||
/**
|
||||
* v1.16.11 — decrypted custom-tag label, resolved server-side. A custom
|
||||
* tag's `labelKey` mirrors its raw `custom:<uuid>` key, so the label
|
||||
* takes precedence over `t(labelKey)`. Null for catalogue / flat /
|
||||
* metric rows.
|
||||
*/
|
||||
label?: string | null;
|
||||
categoryKey: string | null;
|
||||
icon: string | null;
|
||||
direction: "up" | "down";
|
||||
@@ -78,6 +85,11 @@ export function MoodBetterDays({
|
||||
let Icon: ReturnType<typeof moodTagIcon> | null = null;
|
||||
if (factor.source === "metric") {
|
||||
label = t(METRIC_LABEL_KEY[factor.key] ?? factor.key);
|
||||
} else if (factor.label) {
|
||||
// Custom tag — render the decrypted label verbatim (its
|
||||
// labelKey is the raw `custom:<uuid>` key).
|
||||
label = factor.label;
|
||||
Icon = moodTagIcon(factor.icon);
|
||||
} else if (factor.labelKey) {
|
||||
label = t(factor.labelKey);
|
||||
Icon = moodTagIcon(factor.icon);
|
||||
|
||||
@@ -16,6 +16,12 @@ export interface MoodStructuredTagRow {
|
||||
key: string;
|
||||
categoryKey: string;
|
||||
labelKey: string;
|
||||
/**
|
||||
* v1.16.11 — decrypted custom-tag label, resolved server-side. A custom
|
||||
* tag's `labelKey` mirrors its raw `custom:<uuid>` key, so the label
|
||||
* takes precedence over `t(labelKey)`. Null for catalogue tags.
|
||||
*/
|
||||
label?: string | null;
|
||||
icon: string | null;
|
||||
count: number;
|
||||
avgScore: number;
|
||||
@@ -47,7 +53,7 @@ export function MoodStructuredTagBreakdown({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-foreground w-28 shrink-0 truncate">
|
||||
{t(row.labelKey)}
|
||||
{row.label ?? t(row.labelKey)}
|
||||
</span>
|
||||
<div className="bg-secondary relative h-3 flex-1 overflow-hidden rounded-full">
|
||||
{/* v1.16.8 — `opacity-55` matches the `fillOpacity={0.55}`
|
||||
|
||||
@@ -21,6 +21,12 @@ export type MoodInfluenceConfidence = "low" | "medium" | "high";
|
||||
export interface MoodTagInfluenceRow {
|
||||
tag: string;
|
||||
labelKey: string | null;
|
||||
/**
|
||||
* v1.16.11 — decrypted custom-tag label, resolved server-side. A custom
|
||||
* tag's `labelKey` mirrors its raw `custom:<uuid>` key, so the label
|
||||
* takes precedence over `t(labelKey)`. Null for catalogue / flat tags.
|
||||
*/
|
||||
label?: string | null;
|
||||
categoryKey: string | null;
|
||||
icon: string | null;
|
||||
withDays: number;
|
||||
@@ -67,7 +73,8 @@ export function MoodTagInfluence({
|
||||
<ul className="divide-border divide-y">
|
||||
{rows.map((row) => {
|
||||
const Icon = row.labelKey ? moodTagIcon(row.icon) : null;
|
||||
const label = row.labelKey ? t(row.labelKey) : row.tag;
|
||||
const label =
|
||||
row.label ?? (row.labelKey ? t(row.labelKey) : row.tag);
|
||||
const up = row.delta >= 0;
|
||||
const deltaText = `${up ? "+" : ""}${row.delta.toFixed(1)}`;
|
||||
return (
|
||||
|
||||
@@ -24,6 +24,12 @@ export type MoodCrosstabMode = "sameDay" | "nextDay";
|
||||
export interface MoodTagMetricCrosstabRow {
|
||||
tag: string;
|
||||
labelKey: string;
|
||||
/**
|
||||
* v1.16.11 — decrypted custom-tag label, resolved server-side. A custom
|
||||
* tag's `labelKey` mirrors its raw `custom:<uuid>` key, so the label
|
||||
* takes precedence over `t(labelKey)`. Null for catalogue tags.
|
||||
*/
|
||||
label?: string | null;
|
||||
categoryKey: string;
|
||||
icon: string | null;
|
||||
metricKey: string;
|
||||
@@ -88,7 +94,7 @@ export function MoodTagMetricCrosstab({
|
||||
<ul className="divide-border divide-y">
|
||||
{rows.map((row) => {
|
||||
const Icon = moodTagIcon(row.icon);
|
||||
const tagLabel = t(row.labelKey);
|
||||
const tagLabel = row.label ?? t(row.labelKey);
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user