fix(workouts): order the list newest-first; show the year on past-year dates

The canonical picker re-sorts its output to startedAt ASC for its rollup callers, so the list route sliced the oldest page and recent workouts never surfaced on a long history — re-establish newest-first after the pick. And a workout from a previous year now shows its year so it is not read as this year's.
This commit is contained in:
Marc-André Bombeck committed 2026-07-17 16:53:06 +02:00
1 parent 546c5c7d8f
commit 05f80ad378
7 files changed
+48 -3

No files matched your search

+8
View File
@@ -2,6 +2,14 @@
## [Unreleased]
## [1.29.4] — 2026-07-17
- **The workouts list shows the most recent first.** The list was ordered
oldest-first, so on a longer history the newest sessions never made the first
page. It now leads with the latest workout.
- **Past-year dates show the year.** A workout from a previous year now carries
its year in the list, so a December date is never mistaken for this year's.
## [1.29.3] — 2026-07-17
- **Hydration and micronutrients now have a home.** Your fluid intake and the
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.29.3
version: 1.29.4
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.29.3",
"version": "1.29.4",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
+1 -1
View File
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
/* @sw-version-fallback */ "v1.29.3";
/* @sw-version-fallback */ "v1.29.4";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
@@ -147,6 +147,28 @@ describe("GET /api/workouts — canonical dedup", () => {
expect(body.data.meta.droppedDuplicates).toBe(1);
});
it("returns the most recent workouts first", async () => {
// The canonical picker re-sorts its output to startedAt ASC (its rollup
// contract), so the list route must re-establish newest-first AFTER the
// pick — otherwise offset/limit slice the OLDEST page and recent workouts
// never surface. These two never cluster (different day + slot).
const older = makeRow("w-old", "APPLE_HEALTH", "2026-01-01T07:00:00Z");
const newer = makeRow("w-new", "APPLE_HEALTH", "2026-07-01T07:00:00Z");
vi.mocked(prisma.workout.findMany).mockResolvedValueOnce([
newer,
older,
] as never);
const res = await GET(makeRequest());
const body = await res.json();
expect(res.status).toBe(200);
expect(body.data.workouts.map((w: { id: string }) => w.id)).toEqual([
"w-new",
"w-old",
]);
});
it("returns 401 without a session", async () => {
vi.mocked(getSession).mockResolvedValueOnce(null);
const res = await GET(makeRequest());
+11
View File
@@ -143,6 +143,17 @@ async function buildWorkoutsResponse(
userRow?.sourcePriorityJson ?? null,
);
// `pickCanonicalWorkoutRows` re-sorts its output to `startedAt ASC` (its
// documented contract for the rollup callers). This surface is a
// most-recent-first list, so the DB `orderBy startedAt desc` must be
// re-established AFTER the pick — otherwise `offset`/`limit` slice the
// OLDEST page and a user with a long history never sees recent workouts.
canonical.sort((a, b) => {
const delta = (b.startedAt?.getTime() ?? 0) - (a.startedAt?.getTime() ?? 0);
if (delta !== 0) return delta;
return a.id.localeCompare(b.id);
});
const page = canonical.slice(offset, offset + limit).map((row) => ({
id: row.id,
sportType: row.sportType,
+4
View File
@@ -87,10 +87,14 @@ function formatDate(
timeFormat: TimeFormatPreference,
): string {
const d = new Date(iso);
// Show the year whenever the workout is not from the current year, so a
// "Do., 16. Dez." from a previous year is never mistaken for this year's.
const showYear = d.getFullYear() !== new Date().getFullYear();
return getDateTimeFormat(locale, {
weekday: "short",
day: "2-digit",
month: "short",
...(showYear ? { year: "numeric" as const } : {}),
hour: "2-digit",
minute: "2-digit",
...hourCycleOptions(timeFormat),