mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
1 parent
546c5c7d8f
commit
05f80ad378
7 files changed
+48
-3
No files matched your search
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.29.3] — 2026-07-17
|
||||||
|
|
||||||
- **Hydration and micronutrients now have a home.** Your fluid intake and the
|
- **Hydration and micronutrients now have a home.** Your fluid intake and the
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: HealthLog API
|
title: HealthLog API
|
||||||
version: 1.29.3
|
version: 1.29.4
|
||||||
description: >-
|
description: >-
|
||||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "healthlog",
|
"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.",
|
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "PolyForm-Noncommercial-1.0.0",
|
||||||
"homepage": "https://healthlog.dev",
|
"homepage": "https://healthlog.dev",
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ try {
|
|||||||
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
|
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
|
||||||
const CACHE_VERSION =
|
const CACHE_VERSION =
|
||||||
(typeof self !== "undefined" && self.__APP_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 STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
|
||||||
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
||||||
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
|
// 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);
|
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 () => {
|
it("returns 401 without a session", async () => {
|
||||||
vi.mocked(getSession).mockResolvedValueOnce(null);
|
vi.mocked(getSession).mockResolvedValueOnce(null);
|
||||||
const res = await GET(makeRequest());
|
const res = await GET(makeRequest());
|
||||||
|
|||||||
@@ -143,6 +143,17 @@ async function buildWorkoutsResponse(
|
|||||||
userRow?.sourcePriorityJson ?? null,
|
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) => ({
|
const page = canonical.slice(offset, offset + limit).map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
sportType: row.sportType,
|
sportType: row.sportType,
|
||||||
|
|||||||
@@ -87,10 +87,14 @@ function formatDate(
|
|||||||
timeFormat: TimeFormatPreference,
|
timeFormat: TimeFormatPreference,
|
||||||
): string {
|
): string {
|
||||||
const d = new Date(iso);
|
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, {
|
return getDateTimeFormat(locale, {
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
|
...(showYear ? { year: "numeric" as const } : {}),
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
...hourCycleOptions(timeFormat),
|
...hourCycleOptions(timeFormat),
|
||||||
|
|||||||
Reference in new issue
Block a user