mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(cycle): drop card log button, fill predicted-period days, assessment on batch
- Phase education card no longer carries its own log-today button (the top-level log entry covers it). - Predicted-period calendar days render as a soft dashed fill instead of an underline, so they read as period rather than an unfinished dash. - The derived batch route now attaches the deterministic per-score assessment for the five scores so the dashboard grid (and iOS) paints the explanation without a second per-id round-trip; guarded so it never fails the grid.
This commit is contained in:
1 parent
546835a937
commit
2b9d2370e9
6 files changed
+52
-38
No files matched your search
@@ -34,6 +34,8 @@ import {
|
||||
isDerivedMetricId,
|
||||
type DerivedMetricId,
|
||||
} from "@/lib/insights/derived";
|
||||
import { resolveDeterministicAssessment } from "@/lib/insights/derived/derived-assessment";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -146,6 +148,35 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
// four computes at a time against the shared pool, not seventeen.
|
||||
const limit = pLimit(BATCH_CONCURRENCY);
|
||||
const now = new Date();
|
||||
|
||||
// Attach the cheap DETERMINISTIC per-score assessment to the grid read so
|
||||
// iOS paints the "Einschätzung" straight off the overview without a second
|
||||
// per-id round-trip (the AI-warm prose stays lazy, served by the single
|
||||
// route). `resolveDeterministicAssessment` is pure — no DB, no LLM — and
|
||||
// null for non-assessable ids / non-`ok` status, so a non-score item still
|
||||
// gets `assessment: null`. A best-effort grid extra must NEVER fail the
|
||||
// whole batch, so each call is guarded → null on any unexpected shape.
|
||||
const locale = await resolveServerLocale({
|
||||
request,
|
||||
userLocale: user.locale ?? null,
|
||||
override: request.nextUrl.searchParams.get("locale"),
|
||||
});
|
||||
const assessmentLocale = locale === "de" ? "de" : "en";
|
||||
const safeAssessment = (
|
||||
metric: DerivedMetricId,
|
||||
derived: Parameters<typeof resolveDeterministicAssessment>[1],
|
||||
) => {
|
||||
try {
|
||||
return resolveDeterministicAssessment(
|
||||
metric,
|
||||
derived,
|
||||
assessmentLocale,
|
||||
now,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const results = await Promise.all(
|
||||
items.map((item) =>
|
||||
limit(async () => {
|
||||
@@ -167,11 +198,10 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
provenance: derived.provenance,
|
||||
reason:
|
||||
derived.status === "insufficient" ? derived.reason : null,
|
||||
// The batch backs the dashboard GRID; per-score assessment text is
|
||||
// a detail-sheet concern served by the single-metric route. Always
|
||||
// null here so the wire shape matches `DerivedMetricResponse`
|
||||
// without paying the assessment cost on a 17-item grid read.
|
||||
assessment: null,
|
||||
// Deterministic assessment only (cheap, pure). The AI-warm prose
|
||||
// stays a single-route concern; here every assessable `ok` score
|
||||
// carries its template "why" text, everything else stays null.
|
||||
assessment: safeAssessment(item.metric, derived),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -49,14 +49,16 @@ describe("<CycleCalendar>", () => {
|
||||
expect(html).toContain("Logged period");
|
||||
});
|
||||
|
||||
it("renders the predicted period as a band, not a logged pip", () => {
|
||||
it("renders the predicted period as a dashed soft fill, distinct from a logged pip", () => {
|
||||
const days = [{ ...dayBase("2026-06-20"), isPredictedPeriod: true }];
|
||||
const html = render(
|
||||
<CycleCalendar days={days} today={today} onSelectDay={() => {}} />,
|
||||
);
|
||||
// The predicted band uses a dashed gradient underline + the aria marker.
|
||||
// Predicted days are a soft FILL with a dashed outline (not the old
|
||||
// underline), tagged data-predicted; the solid logged pip has no dashes.
|
||||
expect(html).toContain("Predicted period");
|
||||
expect(html).toContain("repeating-linear-gradient");
|
||||
expect(html).toContain('data-predicted="true"');
|
||||
expect(html).toContain("border-dashed");
|
||||
});
|
||||
|
||||
it("labels a fertile-window day (only present when goal-gated server allows it)", () => {
|
||||
|
||||
@@ -30,11 +30,10 @@ const okGate = {
|
||||
predictionEnabled: true,
|
||||
rawChartMode: false,
|
||||
cyclesObserved: 4,
|
||||
onLogToday: () => {},
|
||||
};
|
||||
|
||||
describe("<PhaseEducationCard>", () => {
|
||||
it("renders phase name + descriptive line + the log nudge when the gate passes", () => {
|
||||
it("renders phase name + descriptive line when the gate passes", () => {
|
||||
const html = render(
|
||||
<PhaseEducationCard
|
||||
phase="LUTEAL"
|
||||
@@ -44,7 +43,6 @@ describe("<PhaseEducationCard>", () => {
|
||||
);
|
||||
expect(html).toContain("Luteal");
|
||||
expect(html).toContain("progesterone"); // curated whatsHappening line
|
||||
expect(html).toContain("Log today");
|
||||
expect(html).toContain('data-phase="LUTEAL"');
|
||||
});
|
||||
|
||||
@@ -69,7 +67,6 @@ describe("<PhaseEducationCard>", () => {
|
||||
predictionEnabled
|
||||
rawChartMode={false}
|
||||
cyclesObserved={2}
|
||||
onLogToday={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("Still learning your cycle");
|
||||
@@ -85,7 +82,6 @@ describe("<PhaseEducationCard>", () => {
|
||||
predictionEnabled
|
||||
rawChartMode
|
||||
cyclesObserved={9}
|
||||
onLogToday={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(raw).toContain("Still learning your cycle");
|
||||
@@ -97,7 +93,6 @@ describe("<PhaseEducationCard>", () => {
|
||||
predictionEnabled={false}
|
||||
rawChartMode={false}
|
||||
cyclesObserved={9}
|
||||
onLogToday={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(off).toContain("Still learning your cycle");
|
||||
@@ -113,6 +108,6 @@ describe("<PhaseEducationCard>", () => {
|
||||
);
|
||||
expect(html).toContain("progesterone");
|
||||
expect(html).toContain("cycle-phase-education-chips");
|
||||
expect(html).toContain("Log today");
|
||||
expect(html).toContain("Cramps");
|
||||
});
|
||||
});
|
||||
@@ -269,14 +269,18 @@ export function CycleCalendar({
|
||||
{cell.getDate()}
|
||||
</span>
|
||||
|
||||
{/* Predicted-period band: a soft hatched underline, never a dot. */}
|
||||
{/* Predicted-period day: a soft FILL behind the number (like a
|
||||
logged period day, but lighter) with a dashed outline so it
|
||||
still reads as predicted, not confirmed — replaces the old
|
||||
hatched underline, which looked unfinished. */}
|
||||
{info?.isPredictedPeriod && !info?.isPeriodLogged ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-1.5 bottom-1 h-0.5 rounded-full opacity-70"
|
||||
className="absolute inset-1 rounded-md border border-dashed"
|
||||
style={{
|
||||
backgroundColor: FLOW_HUE,
|
||||
backgroundImage: `repeating-linear-gradient(90deg, ${FLOW_HUE} 0 3px, transparent 3px 5px)`,
|
||||
opacity: 0.3,
|
||||
borderColor: FLOW_HUE,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -219,7 +219,6 @@ export function CycleView() {
|
||||
predictionEnabled={calProfile?.predictionEnabled ?? false}
|
||||
rawChartMode={calProfile?.rawChartMode ?? false}
|
||||
cyclesObserved={calProfile?.cyclesObserved ?? 0}
|
||||
onLogToday={() => openSheet(today)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Sparkles, Plus } from "lucide-react";
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PHASE_WHATS_HAPPENING_KEY } from "@/lib/cycle/phase-copy";
|
||||
@@ -23,8 +22,8 @@ import type { SymptomPhaseRow } from "./use-cycle";
|
||||
* never clinical advice / a medical claim),
|
||||
* 3. a "you often notice this here" chip row sourced from the user's OWN
|
||||
* symptom-by-phase history (`symptomPatterns`), filtered to symptoms that
|
||||
* cluster in THIS phase — never a generic population claim — plus a
|
||||
* "log today" nudge into the existing day-log sheet.
|
||||
* cluster in THIS phase — never a generic population claim. Logging lives
|
||||
* on the top-level log entry, so the card carries no own log button.
|
||||
*
|
||||
* Honesty gate (Clue precedent): when prediction is disabled / raw-chart mode
|
||||
* or fewer than three observed cycles, the predictive framing is suppressed and
|
||||
@@ -47,8 +46,6 @@ export interface PhaseEducationCardProps {
|
||||
predictionEnabled: boolean;
|
||||
rawChartMode: boolean;
|
||||
cyclesObserved: number;
|
||||
/** Open the day-log sheet for today (the "log today" nudge). */
|
||||
onLogToday: () => void;
|
||||
/** Ride the wheel's once-per-session reveal — no independent animation. */
|
||||
animate?: boolean;
|
||||
className?: string;
|
||||
@@ -60,7 +57,6 @@ export function PhaseEducationCard({
|
||||
predictionEnabled,
|
||||
rawChartMode,
|
||||
cyclesObserved,
|
||||
onLogToday,
|
||||
animate = false,
|
||||
className,
|
||||
}: PhaseEducationCardProps) {
|
||||
@@ -189,18 +185,6 @@ export function PhaseEducationCard({
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* The "log today" nudge — opens the existing day-log sheet. */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onLogToday}
|
||||
className="bg-background/55 mt-4 gap-1.5"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("cycle.phaseEducation.trackNudge")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user