feat(workouts): show fitness, fatigue and form from the stored training impulse

The first page of the workout list now carries the fitness-fatigue model
over the day-total heart-rate impulse the nightly strain run already
stores: fitness as the 42-day and fatigue as the 7-day exponentially
weighted average, form as their difference. A day without a stored impulse
counts as rest. It needs 28 days and three training days, flags the first
six weeks as warming up, and reads in the person's own units only. The
workouts page shows the three figures above the list.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 22:13:28 +02:00
1 parent 5c64192afc
commit 9be720146c
19 files changed
+651 -1

No files matched your search

@@ -26,6 +26,8 @@ vi.mock("@/lib/db", () => ({
user: {
findUnique: vi.fn(),
},
// v1.44 — the first page carries the training load.
strainTrimpCache: { findMany: vi.fn(async () => []) },
},
}));
@@ -423,3 +425,45 @@ describe("GET /api/workouts — canonical dedup", () => {
);
});
});
describe("GET /api/workouts — training load (v1.44)", () => {
beforeEach(() => {
vi.clearAllMocks();
__resetAllCachesForTests();
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
sourcePriorityJson: null,
} as never);
vi.mocked(requireModuleEnabled).mockResolvedValue({
enabled: true,
} as never);
vi.mocked(prisma.workout.findMany).mockResolvedValue([] as never);
});
it("carries fitness, fatigue and form on the first page only", async () => {
const yesterday = new Date(Date.now() - 86_400_000)
.toISOString()
.slice(0, 10);
const rows = Array.from({ length: 50 }, (_, i) => ({
day: new Date(
Date.parse(`${yesterday}T00:00:00Z`) - (49 - i) * 86_400_000,
)
.toISOString()
.slice(0, 10),
dayTrimp: 80,
}));
vi.mocked(prisma.strainTrimpCache.findMany).mockResolvedValue(
rows as never,
);
const first = await (await GET(makeRequest())).json();
expect(first.data.trainingLoad).toMatchObject({
status: "ok",
asOf: yesterday,
daysCovered: 50,
trainingDays: 50,
warmingUp: false,
});
const later = await (await GET(makeRequest("?offset=100"))).json();
expect(later.data.trainingLoad).toBeUndefined();
});
});