diff --git a/client/src/components/Dawarich/dawarichSuggestionModel.ts b/client/src/components/Dawarich/dawarichSuggestionModel.ts index 7dbfe2179..0e7927e31 100644 --- a/client/src/components/Dawarich/dawarichSuggestionModel.ts +++ b/client/src/components/Dawarich/dawarichSuggestionModel.ts @@ -34,7 +34,9 @@ export function groupByDay(suggestions: DawarichSuggestion[]): SuggestionDay[] { else byDate.set(stay.localDate, [stay]) } - const oldestFirst = [...byDate.keys()].sort() + // ISO dates, so lexicographic order IS chronological order. Spelled out rather than + // left bare, and deliberately not locale-aware, which would vary by runtime. + const oldestFirst = [...byDate.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) return oldestFirst .map((date, index) => ({ date, diff --git a/client/src/components/Map/dawarichTrail.ts b/client/src/components/Map/dawarichTrail.ts index 97adec6af..5fc536d6a 100644 --- a/client/src/components/Map/dawarichTrail.ts +++ b/client/src/components/Map/dawarichTrail.ts @@ -130,7 +130,9 @@ export function collapsedDayDates( const foldedInPlan = !!expandedDayIds && !expandedDayIds.has(day.id) if (foldedInPlan || collapsedDayIds?.has(day.id)) dates.add(day.date) } - return [...dates].sort() + // Explicit rather than bare: these are ISO dates, where lexicographic IS chronological, + // and a locale-aware comparison would reorder them differently per runtime. + return [...dates].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) } /** The overlay as a GeoJSON FeatureCollection, for the GL renderer. */ diff --git a/client/src/hooks/useDawarichJournalTrail.ts b/client/src/hooks/useDawarichJournalTrail.ts index de9e7c380..a16068c24 100644 --- a/client/src/hooks/useDawarichJournalTrail.ts +++ b/client/src/hooks/useDawarichJournalTrail.ts @@ -37,7 +37,9 @@ export function useDawarichJournalTrail( // here rather than trusting insertion order. const window = useMemo(() => { if (dates.size === 0) return null - const sorted = [...dates].sort() + // ISO dates: lexicographic order is chronological, and staying locale-independent + // keeps the window identical on every runtime. + const sorted = [...dates].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) return { from: `${sorted[0]}T00:00:00Z`, to: `${sorted[sorted.length - 1]}T23:59:59Z` } }, [dates]) diff --git a/server/src/nest/integrations/dawarich.helpers.ts b/server/src/nest/integrations/dawarich.helpers.ts index 428666290..e82e28aaa 100644 --- a/server/src/nest/integrations/dawarich.helpers.ts +++ b/server/src/nest/integrations/dawarich.helpers.ts @@ -340,7 +340,8 @@ export function bucketPointsByDay( else byDay.set(date, [{ lat, lng, ts }]); } - const dates = [...byDay.keys()].sort(); + // ISO dates, so lexicographic order is chronological. Explicit and locale-independent. + const dates = [...byDay.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); const days: DawarichTrackDay[] = []; for (let d = 0; d < dates.length; d++) { diff --git a/server/src/nest/maps/providers/amap.provider.ts b/server/src/nest/maps/providers/amap.provider.ts index 83dca9f13..e273cf81c 100644 --- a/server/src/nest/maps/providers/amap.provider.ts +++ b/server/src/nest/maps/providers/amap.provider.ts @@ -203,7 +203,10 @@ export class AmapPlacesProvider implements PlacesProvider { if (!secret) return null; const signed = { ...params, key: this.credential.key, output: 'JSON' }; const canonical = Object.keys(signed) - .sort() + // Byte order, and it has to stay byte order: Amap computes the same signature over + // the same sorted names, so a locale-aware comparison would produce a signature the + // other side rejects. + .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) .map((k) => `${k}=${signed[k]}`) .join('&'); return createHash('md5').update(`${canonical}${secret}`).digest('hex');