mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
feat(journey): keep the route recorded on a journey day from Dawarich, previewed first, with its gaps
A day header in the journey timeline now carries the Dawarich mark for an editor whose Dawarich answers (dimmed with the reason while offline). It opens a dialog that names the journey and the day and reads that one local day from the caller's own connection: a sketch of the route, distance, first and last time, pieces, gaps without recording, modes and whether tracks or thinned points answered. The day is cut in the zone Dawarich records in where its answer shows one, else in this device's, and a picker cuts it in any other. Nothing is kept before "Keep route". On a phone the same sheet opens from the journey's More options for the day of the card shown. What is kept is the preview that was seen: the save reads the day again and refuses with recording_changed when its hash moved, and the dialog shows the new version. The same day taken over twice keeps one version and announces nothing; a different version is replaced only on "Replace", never touching an entry or text. A failed read throws before any write, so the kept version stays. Gaps stay gaps: points are split where silence ends elsewhere, and no day is joined to the one before. The dialog says how many other people will see the route; editors may keep their own recording. Kept days show in the day header (distance, whose recording, since when, still running) and on the desktop and phone journey maps in the colour of their day's pins, winning over the live trail of their date, and stay drawn without Dawarich. A trip's GPX track is now drawn dashed as the plan, recorded routes solid. Shared links do not show recorded days. Remove route asks first and works without a connection. Server: migration 046160 adds journey_recorded_days (one row per journey, local day and source, segments as JSON, zone, window, fetch time, hash, importer kept past their account). The journey domain owns the rows through JourneyRecordedDaysService; the new journey-recordings domain takes a day over through DawarichTracksService.fetchDay, which reads past the cache, asks a window wide enough for every zone and refuses with DomainErrors. REST under /api/journeys/:id/recordings (list, preview, PUT and DELETE /:date) with @trek/shared contracts, and MCP list_journey_recorded_days, preview_journey_recorded_day, save_journey_recorded_day and delete_journey_recorded_day on the same service and access. A new user event journey:recording:updated keeps every open map current. JourneyTrack gains an optional kind. All 27 locales carry the strings; the Journey and Dawarich wiki pages and the MCP tool list say what it does.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// FE-API-JOURNEY-RECORDINGS-001
|
||||
//
|
||||
// JRN-01: every method pins its verb, its path and its body, and hands back what
|
||||
// the server answered.
|
||||
import { http, HttpResponse, type JsonBodyType } from 'msw';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { server } from '../../tests/helpers/msw/server';
|
||||
import { journeyRecordingsApi } from './journeyRecordings';
|
||||
|
||||
const BASE = '/api/journeys/7/recordings';
|
||||
const HASH = 'a'.repeat(64);
|
||||
|
||||
let calls: Array<{ method: string; path: string; body: unknown }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
calls = [];
|
||||
});
|
||||
|
||||
function record(data: JsonBodyType) {
|
||||
return async ({ request }: { request: Request }) => {
|
||||
const text = await request.text();
|
||||
calls.push({ method: request.method, path: new URL(request.url).pathname, body: text ? JSON.parse(text) : null });
|
||||
return HttpResponse.json(data);
|
||||
};
|
||||
}
|
||||
|
||||
describe('journeyRecordingsApi', () => {
|
||||
it('FE-API-JOURNEY-RECORDINGS-001: lists, previews, keeps and removes the recorded days of a journey', async () => {
|
||||
server.use(
|
||||
http.get(BASE, record({ days: [] })),
|
||||
http.post(`${BASE}/preview`, record({ date: '2026-05-14' })),
|
||||
http.put(`${BASE}/2026-05-14`, record({ outcome: 'created' })),
|
||||
http.delete(`${BASE}/2026-05-14`, record({ success: true }))
|
||||
);
|
||||
expect(await journeyRecordingsApi.list(7)).toEqual({ days: [] });
|
||||
expect(
|
||||
await journeyRecordingsApi.preview(7, { date: '2026-05-14', utcOffsetMinutes: 120, useSourceOffset: true })
|
||||
).toEqual({ date: '2026-05-14' });
|
||||
expect(await journeyRecordingsApi.save(7, '2026-05-14', { utcOffsetMinutes: 120, expectedHash: HASH })).toEqual({
|
||||
outcome: 'created',
|
||||
});
|
||||
expect(await journeyRecordingsApi.remove(7, '2026-05-14')).toBeUndefined();
|
||||
expect(calls).toEqual([
|
||||
{ method: 'GET', path: BASE, body: null },
|
||||
{
|
||||
method: 'POST',
|
||||
path: `${BASE}/preview`,
|
||||
body: { date: '2026-05-14', utcOffsetMinutes: 120, useSourceOffset: true },
|
||||
},
|
||||
{ method: 'PUT', path: `${BASE}/2026-05-14`, body: { utcOffsetMinutes: 120, expectedHash: HASH } },
|
||||
{ method: 'DELETE', path: `${BASE}/2026-05-14`, body: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type {
|
||||
JourneyRecordedDaysResponse,
|
||||
JourneyRecordingPreview,
|
||||
JourneyRecordingPreviewRequest,
|
||||
JourneyRecordingSaveRequest,
|
||||
JourneyRecordingSaveResult,
|
||||
} from '@trek/shared';
|
||||
import apiClient from './client';
|
||||
import { DAWARICH_UPSTREAM_TIMEOUT_MS } from './dawarich';
|
||||
|
||||
const base = (journeyId: number) => `/journeys/${journeyId}/recordings`;
|
||||
|
||||
/** The preview and the save ask Dawarich first, so they get its longer wait. */
|
||||
const upstream = { timeout: DAWARICH_UPSTREAM_TIMEOUT_MS };
|
||||
|
||||
/** JRN-01: the recorded days a journey keeps, previewing one, keeping it and dropping it. */
|
||||
export const journeyRecordingsApi = {
|
||||
list: (journeyId: number, signal?: AbortSignal): Promise<JourneyRecordedDaysResponse> =>
|
||||
apiClient.get(base(journeyId), { signal }).then((r) => r.data),
|
||||
preview: (
|
||||
journeyId: number,
|
||||
body: JourneyRecordingPreviewRequest,
|
||||
signal?: AbortSignal
|
||||
): Promise<JourneyRecordingPreview> =>
|
||||
apiClient.post(`${base(journeyId)}/preview`, body, { ...upstream, signal }).then((r) => r.data),
|
||||
save: (journeyId: number, date: string, body: JourneyRecordingSaveRequest): Promise<JourneyRecordingSaveResult> =>
|
||||
apiClient.put(`${base(journeyId)}/${date}`, body, upstream).then((r) => r.data),
|
||||
remove: (journeyId: number, date: string): Promise<void> =>
|
||||
apiClient.delete(`${base(journeyId)}/${date}`).then(() => undefined),
|
||||
};
|
||||
@@ -81,6 +81,9 @@ export const HANDLED_OUTSIDE_TRIP_STORE = [
|
||||
// Capture times landed after a photo import (#1587); the same listener reloads
|
||||
// the journey so the gallery re-sorts.
|
||||
'journey:photos:updated',
|
||||
// A day's recorded route was taken over or removed (JRN-01):
|
||||
// components/Journey/useJourneyRecordings reads the kept days again.
|
||||
'journey:recording:updated',
|
||||
// Studio book — components/Studio/useBookStore (its own listener: a client
|
||||
// with nothing outstanding takes the new version, one with unsaved edits
|
||||
// deliberately does not and conflicts on its next save instead)
|
||||
|
||||
Reference in New Issue
Block a user