mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(journey): select all takes every photo in the range and imports keep their order
The photo picker's Select all took only the pages already loaded, 50 at a time, and replaced whatever had been picked on another tab. It now loads the rest of a day, trip or date-range search before selecting (cancelled by closing the picker or switching tab), adds to earlier picks, and Deselect all only clears what is in view. Add stays disabled until that loading is done. The whole-library tab keeps selecting what is loaded and shows the count as "N+". A date-filtered Immich search stopped at 1,000 photos without a word, because its page budget was absolute; it is now relative to the page asked for, with an absolute ceiling of 270 raw pages sized so the picker's deepest page still gets a full answer. Adds go out in batches of 500 ids, under the JSON body limit, and each batch is sent oldest first, so photos land in an entry in the order they were taken. When a later batch fails, the photos already saved are counted and the journey reloads while the error still shows, through one hook for desktop and phone. After a provider add, the capture-time lookup runs at most four at a time per user, shared by all of that user's batches. When it fills a row, a journey:photos:updated event reloads open journeys once per batch, from the REST routes and the MCP add tool alike. Device uploads, which arrive one file per request, get the same lookup without the event. Addresses discussion #1587.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
PROVIDER_SELECT_ALL_MAX_PAGES,
|
||||
addTripPhotosSchema,
|
||||
createAlbumLinkSchema,
|
||||
immichSearchSchema,
|
||||
@@ -71,6 +72,12 @@ describe('immich contracts', () => {
|
||||
expect(immichSearchSchema.safeParse({ utc_offset_minutes: '-480' }).success).toBe(true);
|
||||
expect(immichSearchSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
|
||||
it('pins how deep the picker pages a search, which the server scan ceiling is sized from (#1587)', () => {
|
||||
// Page 251 of 200 ends at photo 50,200. Raising it deepens the server's scan
|
||||
// ceiling with it, since that ceiling is derived from this number.
|
||||
expect(PROVIDER_SELECT_ALL_MAX_PAGES).toBe(250);
|
||||
});
|
||||
});
|
||||
|
||||
describe('synology contracts', () => {
|
||||
|
||||
@@ -75,6 +75,20 @@ export const immichSearchSchema = z.looseObject({
|
||||
utc_offset_minutes: utcOffsetMinutes,
|
||||
});
|
||||
|
||||
/**
|
||||
* How far the journey photo picker pages a date-bounded search (#1587): its
|
||||
* first page and at most this many after it, so it never asks for a page past
|
||||
* PROVIDER_SELECT_ALL_MAX_PAGES + 1. At the picker's 200 a page that is 50,200
|
||||
* photos.
|
||||
*
|
||||
* An absolute page rather than a count per run: whether "Select all" loads the
|
||||
* pages or they are scrolled into view, and however often it is pressed, page
|
||||
* 251 is the last. Shared because both ends size against it: the picker stops
|
||||
* there, and the Immich search keeps its absolute scan ceiling deep enough that
|
||||
* this last page is still answered in full.
|
||||
*/
|
||||
export const PROVIDER_SELECT_ALL_MAX_PAGES = 250;
|
||||
|
||||
// ── Synology ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const synologySettingsSchema = z.looseObject({
|
||||
|
||||
@@ -123,6 +123,7 @@ const FIXTURES: Record<TrekWsEventName, Record<string, unknown>> = {
|
||||
},
|
||||
'journey:book:cursor': { journeyId: 7, socketId: 3, userId: 2, spreadIndex: 0, x: 105.5, y: 60 },
|
||||
'journey:contributor:changed': { journeyId: 3, targetUserId: 2, role: 'editor' },
|
||||
'journey:photos:updated': { journeyId: 3 },
|
||||
'import:progress': { jobId: 'j1', tripId: 1, status: 'running', done: 1, total: 3, fileName: 'a.pdf' },
|
||||
'import:done': { jobId: 'j1', tripId: 1, result: { items: [] } },
|
||||
'import:error': { jobId: 'j1', tripId: 1, message: 'boom' },
|
||||
@@ -145,14 +146,16 @@ const DRIFT_VARIANTS: Partial<Record<TrekWsEventName, Record<string, unknown>[]>
|
||||
};
|
||||
|
||||
describe('@trek/shared realtime event registry', () => {
|
||||
it('WSEVT-REG-001: pins the authoritative inventory counts (74 trip + 32 user = 106)', () => {
|
||||
it('WSEVT-REG-001: pins the authoritative inventory counts (74 trip + 33 user = 107)', () => {
|
||||
// 67th to 69th trip event: the three collab:link:* a shared link emits.
|
||||
// 70th and 71st: the road trip's vias and tracks, which used to be written silently.
|
||||
// 74th: docsync:changed, so a sync run that moved documents refreshes the
|
||||
// panel without every member polling for it.
|
||||
// 33rd user event: journey:photos:updated, so the gallery re-sorts once the
|
||||
// capture times of an import have landed (#1587).
|
||||
expect(TREK_WS_TRIP_EVENT_NAMES).toHaveLength(74);
|
||||
expect(TREK_WS_USER_EVENT_NAMES).toHaveLength(32);
|
||||
expect(TREK_WS_EVENT_NAMES).toHaveLength(106);
|
||||
expect(TREK_WS_USER_EVENT_NAMES).toHaveLength(33);
|
||||
expect(TREK_WS_EVENT_NAMES).toHaveLength(107);
|
||||
});
|
||||
|
||||
it('WSEVT-REG-002: every name is domain:action shaped and outside the reserved plugin: namespace', () => {
|
||||
|
||||
@@ -319,6 +319,19 @@ export const TREK_WS_EVENTS = {
|
||||
scope: 'user',
|
||||
payload: z.object({ journeyId: id, targetUserId: id, role: z.unknown() }),
|
||||
},
|
||||
/*
|
||||
* Photos on the journey learned when (or where) they were taken (#1587).
|
||||
*
|
||||
* Sent once per batch of provider photos (Immich, Synology) added, after the
|
||||
* detached capture-time backfill wrote something, and to everyone on the
|
||||
* journey including whoever added them: the add answered before the backfill
|
||||
* ran, so the importer's own reload still had the new photos at the end of the
|
||||
* gallery. Content-free, the gallery order is the server's to work out.
|
||||
*
|
||||
* Never for a device upload: those go one file per request, so an event per
|
||||
* request would reload every open client once per uploaded photo.
|
||||
*/
|
||||
'journey:photos:updated': { scope: 'user', payload: z.object({ journeyId: id }) },
|
||||
|
||||
/*
|
||||
* The Studio book was saved (#1973).
|
||||
|
||||
Reference in New Issue
Block a user