From 72a82b3c447f06bb91ed066740c32af386c23071 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 29 Jul 2026 11:57:24 +0200 Subject: [PATCH] fix(client): work through the findings from the coverage pass 214 items came out of writing the tests. Real defects are corrected, dead code is removed rather than covered, and the tests that pinned the old behaviour move with them. Also drops two agent coverage scratch files that slipped into the coverage PR and adds the ignore rule that keeps them out. --- .gitignore | 1 + client/coverage-agentF/coverage-final.json | 3 - client/coverage-jsheet/coverage-final.json | 2 - client/src/api/client.behavior.test.ts | 34 +- client/src/api/client.ts | 42 ++- .../components/Admin/AddonManager.test.tsx | 4 +- .../Admin/AdminPluginsPanel.test.tsx | 15 + .../components/Admin/AdminPluginsPanel.tsx | 40 +- .../src/components/Admin/CategoryManager.tsx | 2 +- .../Admin/DefaultUserSettingsTab.tsx | 4 +- .../Admin/DevNotificationsPanel.test.tsx | 17 +- .../Admin/DevNotificationsPanel.tsx | 5 +- .../Admin/PackingTemplateManager.test.tsx | 4 +- .../Admin/PackingTemplateManager.tsx | 7 +- .../Budget/BudgetPanel.helpers.test.ts | 16 +- .../components/Budget/BudgetPanel.helpers.ts | 12 + .../Budget/BudgetPanelAddItemRow.tsx | 3 +- .../Budget/BudgetPanelInlineEditCell.tsx | 17 +- .../src/components/Budget/CostsPanel.test.tsx | 29 ++ client/src/components/Budget/CostsPanel.tsx | 10 +- .../components/Collab/CollabChatMessages.tsx | 9 +- .../Collab/CollabChatSmall.test.tsx | 31 +- .../components/Collab/CollabNotes.test.tsx | 19 +- client/src/components/Collab/CollabNotes.tsx | 5 +- .../Collab/CollabNotesWebsiteThumbnail.tsx | Bin 2211 -> 2651 bytes client/src/components/Collab/CollabPanel.tsx | 10 +- client/src/components/Collab/CollabPolls.tsx | 14 +- .../AddPlaceToCollectionModal.test.tsx | 8 + .../Collections/AddPlaceToCollectionModal.tsx | 2 +- .../Collections/CollectionPicker.tsx | 29 +- .../CollectionPlaceDetail.test.tsx | 3 +- .../Collections/CollectionPlaceDetail.tsx | 2 +- .../Collections/ListEditorModal.test.tsx | 3 +- .../Collections/ListEditorModal.tsx | 2 +- .../SaveToCollectionModal.test.tsx | 6 +- .../Collections/SaveToCollectionModal.tsx | 2 +- .../SaveTripPlacesToListModal.test.tsx | 4 +- .../Collections/SaveTripPlacesToListModal.tsx | 2 +- .../Collections/ShareCollectionModal.tsx | 1 + .../Files/FileManagerImageLightbox.test.tsx | 39 +- .../Files/FileManagerImageLightbox.tsx | 27 +- client/src/components/Files/useFileManager.ts | 1 + .../components/FourZero/NoFearShow.test.tsx | 23 +- client/src/components/FourZero/NoFearShow.tsx | 22 +- .../FourZero/noFearAssembly.test.ts | 17 + .../src/components/FourZero/noFearAssembly.ts | 5 +- client/src/components/FourZero/noFearScene.ts | 2 +- .../components/Journey/JournalBody.test.tsx | 14 +- client/src/components/Journey/JournalBody.tsx | 21 +- .../JourneyDetailPageEntryEditor.test.tsx | 21 ++ .../Journey/JourneyDetailPageEntryEditor.tsx | 13 +- .../Journey/JourneyDetailPageMapView.tsx | 6 - .../JourneyDetailPageSettingsDialog.test.tsx | 13 + .../JourneyDetailPageSettingsDialog.tsx | 17 +- client/src/components/Journey/JourneyMap.tsx | 4 - .../components/Journey/JourneyMapGL.test.tsx | 14 + .../src/components/Journey/JourneyMapGL.tsx | 12 +- .../Journey/MobileEntryCard.test.tsx | 5 + .../components/Journey/MobileEntryCard.tsx | 23 +- .../components/Journey/MobileEntryView.tsx | 3 +- .../Journey/MobileMapTimeline.test.tsx | 19 + .../components/Journey/MobileMapTimeline.tsx | 14 +- client/src/components/Map/MapView.test.tsx | 9 + client/src/components/Map/MapView.tsx | 29 +- client/src/components/Map/MapViewGL.test.tsx | 34 ++ client/src/components/Map/MapViewGL.tsx | 42 ++- .../components/Map/PoiCategoryPill.test.tsx | 60 +++ client/src/components/Map/PoiCategoryPill.tsx | 4 +- .../src/components/Map/ReservationOverlay.tsx | 192 +--------- client/src/components/Map/placePhoto.ts | 18 + .../components/Map/reservationsMapbox.test.ts | 8 +- .../src/components/Map/reservationsMapbox.ts | 69 +--- .../src/components/Map/usePoiExplore.test.tsx | 32 +- client/src/components/Map/usePoiExplore.ts | 5 + client/src/components/PDF/TripPDF.test.ts | 12 + client/src/components/PDF/TripPDF.tsx | 10 +- .../PackingListPanelCategoryGroup.test.tsx | 16 +- .../Packing/PackingListPanelCategoryGroup.tsx | 21 +- .../Packing/PackingShareControl.tsx | 2 +- .../src/components/Planner/AddressInput.tsx | 2 +- .../src/components/Planner/AirportSelect.tsx | 5 +- .../src/components/Planner/DayDetailPanel.tsx | 60 --- .../Planner/DayPlanSidebar.test.tsx | 15 +- .../src/components/Planner/DayPlanSidebar.tsx | 341 +++++------------- .../Planner/LocationSelect.test.tsx | 6 +- .../src/components/Planner/LocationSelect.tsx | 2 +- .../Planner/PlaceFormModal.test.tsx | 31 +- .../src/components/Planner/PlaceFormModal.tsx | 11 +- .../src/components/Planner/PlaceInspector.tsx | 14 - .../components/Planner/ReservationModal.tsx | 14 - .../components/Planner/ReservationsPanel.tsx | 5 - .../Planner/TransitSearchPanel.test.tsx | 10 +- .../components/Planner/TransitSearchPanel.tsx | 14 +- .../components/Planner/usePlacesSidebar.ts | 13 +- client/src/components/Settings/AccountTab.tsx | 3 +- .../components/Settings/MapSettingsTab.tsx | 3 +- .../components/Settings/NotificationsTab.tsx | 8 +- .../components/Settings/PasskeysSection.tsx | 4 +- .../Settings/PhotoProvidersSection.tsx | 7 +- .../SystemNotices/SystemNoticeBanner.tsx | 36 +- .../SystemNotices/SystemNoticeModal.test.tsx | 10 +- .../SystemNotices/SystemNoticeModal.tsx | 39 +- client/src/components/Todo/TodoListPanel.tsx | 91 +++-- client/src/components/Todo/useTodoList.ts | 3 +- client/src/components/Trips/TripFormModal.tsx | 16 +- .../Trips/TripMembersModal.test.tsx | 17 + .../src/components/Trips/TripMembersModal.tsx | 14 +- .../components/shared/ConfirmDialog.test.tsx | 15 + .../src/components/shared/ConfirmDialog.tsx | 12 +- client/src/db/offlineDb.ts | 9 +- .../screens/admin/MAdminAuditLogPanel.tsx | 16 +- .../screens/admin/MAdminBackupPanel.tsx | 5 +- .../screens/admin/MAdminCategoryManager.tsx | 1 - .../admin/MAdminDefaultUserSettings.tsx | 8 +- .../admin/MAdminDevNotificationsPanel.tsx | 125 +++---- .../screens/admin/MAdminGitHubPanel.tsx | 50 ++- .../screens/admin/MAdminMcpTokensPanel.tsx | 11 +- .../admin/MAdminNotificationsSection.tsx | 1 - .../screens/admin/MAdminNotifyMatrix.tsx | 6 +- .../admin/MAdminPackingTemplateManager.tsx | 4 +- .../screens/admin/MAdminPluginsPanel.tsx | 24 +- .../src/mobile/screens/admin/MAdminSheets.tsx | 2 +- .../screens/atlas/MAtlasCountryPopup.tsx | 13 +- .../screens/collections/MCollAddSheet.tsx | 12 +- .../screens/collections/MCollEditSheet.tsx | 15 +- .../screens/collections/MCollLabelsSheet.tsx | 15 +- .../collections/MCollListPickerSheet.tsx | 2 + .../screens/collections/MCollListsDrawer.tsx | 125 ------- .../screens/collections/MCollPlaceSheet.tsx | 2 +- .../mobile/screens/dashboard/MDashboard.tsx | 4 +- .../screens/dashboard/MNewTripSheet.tsx | 7 +- .../mobile/screens/journey/MJourneyDetail.tsx | 9 +- .../settings/MPhotoProvidersSection.tsx | 7 +- .../screens/settings/MSettingsAccount.tsx | 13 +- .../mobile/screens/settings/MSettingsMcp.tsx | 8 +- .../screens/settings/MSettingsPlugins.tsx | 4 +- .../screens/trip/MTripLoadingSplash.tsx | 13 +- client/src/mobile/screens/trip/MTripShell.tsx | 4 +- .../screens/trip/places/MPlacesBrowser.tsx | 16 +- .../screens/trip/plan/MPlanTimeline.tsx | 17 +- .../screens/trip/plan/MPlanTimelineRows.tsx | 33 +- .../screens/trip/plan/useMPlanTimeline.ts | 5 + .../screens/trip/sheets/ImpFileStep.tsx | 36 +- .../trip/sheets/MAccommodationSheet.tsx | 39 +- .../trip/sheets/MBrowseActionsSheet.tsx | 3 +- .../mobile/screens/trip/sheets/MCostSheet.tsx | 5 +- .../mobile/screens/trip/sheets/MDaySheet.tsx | 3 +- .../screens/trip/sheets/MExportSheet.tsx | 12 +- .../mobile/screens/trip/sheets/MNoteSheet.tsx | 8 +- .../screens/trip/sheets/MPlaceEditSheet.tsx | 4 +- .../screens/trip/sheets/MPlaceSheet.tsx | 3 +- .../screens/trip/sheets/MReservationSheet.tsx | 10 +- .../trip/sheets/MTransportFormSheet.tsx | 24 +- .../screens/trip/sheets/PlPlaceSearch.tsx | 3 +- .../mobile/screens/trip/tabs/MBookingsTab.tsx | 120 +++--- .../mobile/screens/trip/tabs/MCollabChat.tsx | 19 +- .../mobile/screens/trip/tabs/MCollabNotes.tsx | 22 +- .../mobile/screens/trip/tabs/MCollabPolls.tsx | 3 +- .../mobile/screens/trip/tabs/MCostsTab.tsx | 56 ++- .../screens/trip/tabs/MFileLightbox.tsx | 10 +- .../screens/trip/tabs/MFileTrashSheet.tsx | 11 +- .../mobile/screens/trip/tabs/MFilesTab.tsx | 26 +- .../screens/trip/tabs/MPackItemSheet.tsx | 7 +- .../screens/trip/tabs/MPackingListTab.tsx | 4 +- .../mobile/screens/trip/tabs/MTaskSheet.tsx | 6 +- .../mobile/screens/trip/tabs/MTodoListTab.tsx | 49 ++- .../screens/trip/tabs/MTransportsTab.tsx | 114 +++--- .../mobile/screens/trip/tabs/listsModel.ts | 15 +- .../mobile/screens/trip/tabs/tabChrome.tsx | 27 ++ .../screens/vacay/MVacaySettingsSheet.tsx | 12 +- client/src/pages/AtlasPage.tsx | 49 --- client/src/pages/DashboardPage.tsx | 63 ++-- client/src/pages/SharedTripPage.test.tsx | 4 +- client/src/pages/SharedTripPage.tsx | 5 +- client/src/pages/TripPlannerPage.tsx | 4 +- .../src/pages/TripPlannerPage.wiring.test.tsx | 4 + .../admin/AdminNotificationsPanel.test.tsx | 29 +- .../pages/admin/AdminNotificationsPanel.tsx | 26 +- .../src/pages/admin/AdminNotificationsTab.tsx | 3 +- client/src/pages/admin/useAdmin.test.tsx | 18 + client/src/pages/admin/useAdmin.ts | 1 + client/src/pages/atlas/useAtlas.ts | 4 +- .../src/pages/dashboard/useDashboard.test.tsx | 13 + client/src/pages/dashboard/useDashboard.ts | 9 +- .../journeyDetail/useJourneyDetail.test.tsx | 8 +- .../pages/journeyDetail/useJourneyDetail.ts | 5 +- .../pages/tripPlanner/useTripPlanner.test.tsx | 14 + .../src/pages/tripPlanner/useTripPlanner.ts | 4 +- client/src/repo/placeRepo.offline.test.ts | 7 +- client/src/repo/placeRepo.ts | 10 +- client/src/store/collectionStore.test.ts | 95 ++++- client/src/store/collectionStore.ts | 83 ++++- client/src/store/journeyStore.ts | 2 +- client/src/store/slices/budgetSlice.test.ts | 19 + client/src/store/slices/budgetSlice.ts | 28 +- client/src/store/slices/daysSlice.test.ts | 3 +- client/src/store/slices/daysSlice.ts | 2 - client/src/store/slices/packingSlice.test.ts | 6 +- client/src/store/slices/packingSlice.ts | 7 +- client/src/store/slices/placesSlice.ts | 2 +- client/src/store/slices/remoteEventHandler.ts | 16 +- client/src/store/slices/todoSlice.test.ts | 6 +- client/src/store/slices/todoSlice.ts | 7 +- client/src/store/vacayStore.ts | 6 - client/src/sync/offlinePrefs.ts | 2 +- .../src/sync/tripSyncManager.prepare.test.ts | 16 +- client/src/sync/tripSyncManager.ts | 26 +- client/src/types.ts | 4 +- client/src/utils/fileDownload.ts | 11 + client/src/utils/uploadQueue.test.ts | 18 +- client/src/utils/uploadQueue.ts | 9 +- .../mobile/admin/MAdminAddonManager.test.tsx | 22 +- .../mobile/admin/MAdminBackupPanel.test.tsx | 8 + .../admin/MAdminDefaultUserSettings.test.tsx | 6 +- .../mobile/admin/MAdminGitHubPanel.test.tsx | 32 +- .../MAdminPackingTemplateManager.test.tsx | 4 +- .../mobile/admin/MAdminPluginsPanel.test.tsx | 15 + .../unit/mobile/admin/MAdminSheets.test.tsx | 2 +- .../mobile/atlas/MAtlasCountryPopup.test.tsx | 27 +- .../mobile/collections/MCollAddSheet.test.tsx | 18 +- .../collections/MCollEditSheet.test.tsx | 20 +- .../collections/MCollLabelsSheet.test.tsx | 20 +- .../collections/MCollListPickerSheet.test.tsx | 10 + .../collections/MCollListsDrawer.test.tsx | 122 ------- .../unit/mobile/dashboard/MDashboard.test.tsx | 10 +- .../mobile/journey/MJourneyDetail.test.tsx | 6 +- .../settings/MPhotoProvidersSection.test.tsx | 9 +- .../mobile/settings/MSettingsAccount.test.tsx | 2 +- .../mobile/settings/MSettingsMcp.test.tsx | 9 +- .../mobile/settings/MSettingsPlugins.test.tsx | 3 +- .../unit/mobile/trip/ImpFileStep.test.tsx | 18 +- .../mobile/trip/MAccommodationSheet.test.tsx | 6 + .../unit/mobile/trip/MBookingsTab.test.tsx | 6 +- .../mobile/trip/MBrowseActionsSheet.test.tsx | 4 +- .../unit/mobile/trip/MCollabNotes.test.tsx | 5 +- .../unit/mobile/trip/MCostSheet.test.tsx | 8 +- .../tests/unit/mobile/trip/MCostsTab.test.tsx | 21 +- .../tests/unit/mobile/trip/MDaySheet.test.tsx | 11 +- .../unit/mobile/trip/MExportSheet.test.tsx | 21 +- .../unit/mobile/trip/MFileLightbox.test.tsx | 2 +- .../unit/mobile/trip/MFileTrashSheet.test.tsx | 16 + .../tests/unit/mobile/trip/MFilesTab.test.tsx | 15 + .../unit/mobile/trip/MNoteSheet.test.tsx | 16 + .../unit/mobile/trip/MPackItemSheet.test.tsx | 13 +- .../unit/mobile/trip/MPackingListTab.test.tsx | 4 +- .../unit/mobile/trip/MPlaceSheet.test.tsx | 19 +- .../unit/mobile/trip/MPlacesBrowser.test.tsx | 51 ++- .../unit/mobile/trip/MPlanTimeline.test.tsx | 27 +- .../mobile/trip/MPlanTimelineRows.test.tsx | 38 +- .../mobile/trip/MReservationSheet.test.tsx | 15 + .../unit/mobile/trip/MTaskSheet.test.tsx | 12 +- .../unit/mobile/trip/MTodoListTab.test.tsx | 25 +- .../mobile/trip/MTransportFormSheet.test.tsx | 29 +- .../unit/mobile/trip/MTransportSheet.test.tsx | 2 - .../unit/mobile/trip/MTransportsTab.test.tsx | 6 +- .../mobile/trip/MTripLoadingSplash.test.tsx | 41 ++- .../unit/mobile/trip/MTripShell.test.tsx | 4 +- .../tests/unit/mobile/trip/listsModel.test.ts | 7 +- .../unit/mobile/trip/useMPlanTimeline.test.ts | 5 +- .../mobile/vacay/MVacaySettingsSheet.test.tsx | 24 ++ .../remoteEventHandler/assignments.test.ts | 7 +- .../unit/remoteEventHandler/budget.test.ts | 5 +- .../nest/plugins/registry/registry.service.ts | 20 + .../integration/plugins/registry.test.ts | 22 ++ shared/src/i18n/ar/admin.ts | 2 + shared/src/i18n/ar/collab.ts | 2 + shared/src/i18n/ar/collection.ts | 1 + shared/src/i18n/ar/common.ts | 1 + shared/src/i18n/ar/places.ts | 2 + shared/src/i18n/ar/settings.ts | 1 + shared/src/i18n/ar/undo.ts | 1 + shared/src/i18n/br/admin.ts | 2 + shared/src/i18n/br/collab.ts | 2 + shared/src/i18n/br/collection.ts | 1 + shared/src/i18n/br/common.ts | 1 + shared/src/i18n/br/places.ts | 2 + shared/src/i18n/br/settings.ts | 1 + shared/src/i18n/br/undo.ts | 1 + shared/src/i18n/ca/admin.ts | 2 + shared/src/i18n/ca/collab.ts | 2 + shared/src/i18n/ca/collection.ts | 1 + shared/src/i18n/ca/common.ts | 1 + shared/src/i18n/ca/places.ts | 2 + shared/src/i18n/ca/settings.ts | 1 + shared/src/i18n/ca/undo.ts | 1 + shared/src/i18n/cs/admin.ts | 2 + shared/src/i18n/cs/collab.ts | 2 + shared/src/i18n/cs/collection.ts | 1 + shared/src/i18n/cs/common.ts | 1 + shared/src/i18n/cs/places.ts | 2 + shared/src/i18n/cs/settings.ts | 1 + shared/src/i18n/cs/undo.ts | 1 + shared/src/i18n/de/admin.ts | 2 + shared/src/i18n/de/collab.ts | 2 + shared/src/i18n/de/collection.ts | 1 + shared/src/i18n/de/common.ts | 1 + shared/src/i18n/de/places.ts | 2 + shared/src/i18n/de/settings.ts | 1 + shared/src/i18n/de/undo.ts | 1 + shared/src/i18n/en/admin.ts | 2 + shared/src/i18n/en/collab.ts | 2 + shared/src/i18n/en/collection.ts | 1 + shared/src/i18n/en/common.ts | 1 + shared/src/i18n/en/places.ts | 2 + shared/src/i18n/en/settings.ts | 1 + shared/src/i18n/en/undo.ts | 1 + shared/src/i18n/es/admin.ts | 2 + shared/src/i18n/es/collab.ts | 2 + shared/src/i18n/es/collection.ts | 1 + shared/src/i18n/es/common.ts | 1 + shared/src/i18n/es/places.ts | 2 + shared/src/i18n/es/settings.ts | 1 + shared/src/i18n/es/undo.ts | 1 + shared/src/i18n/fr/admin.ts | 2 + shared/src/i18n/fr/collab.ts | 2 + shared/src/i18n/fr/collection.ts | 1 + shared/src/i18n/fr/common.ts | 1 + shared/src/i18n/fr/places.ts | 2 + shared/src/i18n/fr/settings.ts | 1 + shared/src/i18n/fr/undo.ts | 1 + shared/src/i18n/gr/admin.ts | 2 + shared/src/i18n/gr/collab.ts | 2 + shared/src/i18n/gr/collection.ts | 1 + shared/src/i18n/gr/common.ts | 1 + shared/src/i18n/gr/places.ts | 2 + shared/src/i18n/gr/settings.ts | 1 + shared/src/i18n/gr/undo.ts | 1 + shared/src/i18n/hu/admin.ts | 2 + shared/src/i18n/hu/collab.ts | 2 + shared/src/i18n/hu/collection.ts | 1 + shared/src/i18n/hu/common.ts | 1 + shared/src/i18n/hu/places.ts | 2 + shared/src/i18n/hu/settings.ts | 1 + shared/src/i18n/hu/undo.ts | 1 + shared/src/i18n/id/admin.ts | 2 + shared/src/i18n/id/collab.ts | 2 + shared/src/i18n/id/collection.ts | 1 + shared/src/i18n/id/common.ts | 1 + shared/src/i18n/id/places.ts | 2 + shared/src/i18n/id/settings.ts | 1 + shared/src/i18n/id/undo.ts | 1 + shared/src/i18n/it/admin.ts | 2 + shared/src/i18n/it/collab.ts | 2 + shared/src/i18n/it/collection.ts | 1 + shared/src/i18n/it/common.ts | 1 + shared/src/i18n/it/places.ts | 2 + shared/src/i18n/it/settings.ts | 1 + shared/src/i18n/it/undo.ts | 1 + shared/src/i18n/ja/admin.ts | 2 + shared/src/i18n/ja/collab.ts | 2 + shared/src/i18n/ja/collection.ts | 1 + shared/src/i18n/ja/common.ts | 1 + shared/src/i18n/ja/places.ts | 2 + shared/src/i18n/ja/settings.ts | 1 + shared/src/i18n/ja/undo.ts | 1 + shared/src/i18n/ko/admin.ts | 2 + shared/src/i18n/ko/collab.ts | 2 + shared/src/i18n/ko/collection.ts | 1 + shared/src/i18n/ko/common.ts | 1 + shared/src/i18n/ko/places.ts | 2 + shared/src/i18n/ko/settings.ts | 1 + shared/src/i18n/ko/undo.ts | 1 + shared/src/i18n/nl/admin.ts | 2 + shared/src/i18n/nl/collab.ts | 2 + shared/src/i18n/nl/collection.ts | 1 + shared/src/i18n/nl/common.ts | 1 + shared/src/i18n/nl/places.ts | 2 + shared/src/i18n/nl/settings.ts | 1 + shared/src/i18n/nl/undo.ts | 1 + shared/src/i18n/pl/admin.ts | 2 + shared/src/i18n/pl/collab.ts | 2 + shared/src/i18n/pl/collection.ts | 1 + shared/src/i18n/pl/common.ts | 1 + shared/src/i18n/pl/places.ts | 2 + shared/src/i18n/pl/settings.ts | 1 + shared/src/i18n/pl/undo.ts | 1 + shared/src/i18n/ru/admin.ts | 2 + shared/src/i18n/ru/collab.ts | 2 + shared/src/i18n/ru/collection.ts | 1 + shared/src/i18n/ru/common.ts | 1 + shared/src/i18n/ru/places.ts | 2 + shared/src/i18n/ru/settings.ts | 1 + shared/src/i18n/ru/undo.ts | 1 + shared/src/i18n/sv/admin.ts | 2 + shared/src/i18n/sv/collab.ts | 2 + shared/src/i18n/sv/collection.ts | 1 + shared/src/i18n/sv/common.ts | 1 + shared/src/i18n/sv/places.ts | 2 + shared/src/i18n/sv/settings.ts | 1 + shared/src/i18n/sv/undo.ts | 1 + shared/src/i18n/tr/admin.ts | 2 + shared/src/i18n/tr/collab.ts | 2 + shared/src/i18n/tr/collection.ts | 1 + shared/src/i18n/tr/common.ts | 1 + shared/src/i18n/tr/places.ts | 2 + shared/src/i18n/tr/settings.ts | 1 + shared/src/i18n/tr/undo.ts | 1 + shared/src/i18n/uk/admin.ts | 2 + shared/src/i18n/uk/collab.ts | 2 + shared/src/i18n/uk/collection.ts | 1 + shared/src/i18n/uk/common.ts | 1 + shared/src/i18n/uk/places.ts | 2 + shared/src/i18n/uk/settings.ts | 1 + shared/src/i18n/uk/undo.ts | 1 + shared/src/i18n/vi/admin.ts | 2 + shared/src/i18n/vi/collab.ts | 2 + shared/src/i18n/vi/collection.ts | 1 + shared/src/i18n/vi/common.ts | 1 + shared/src/i18n/vi/places.ts | 2 + shared/src/i18n/vi/settings.ts | 1 + shared/src/i18n/vi/undo.ts | 1 + shared/src/i18n/zh-TW/admin.ts | 2 + shared/src/i18n/zh-TW/collab.ts | 2 + shared/src/i18n/zh-TW/collection.ts | 1 + shared/src/i18n/zh-TW/common.ts | 1 + shared/src/i18n/zh-TW/places.ts | 2 + shared/src/i18n/zh-TW/settings.ts | 1 + shared/src/i18n/zh-TW/undo.ts | 1 + shared/src/i18n/zh/admin.ts | 2 + shared/src/i18n/zh/collab.ts | 2 + shared/src/i18n/zh/collection.ts | 1 + shared/src/i18n/zh/common.ts | 1 + shared/src/i18n/zh/places.ts | 2 + shared/src/i18n/zh/settings.ts | 1 + shared/src/i18n/zh/undo.ts | 1 + 425 files changed, 3155 insertions(+), 1968 deletions(-) delete mode 100644 client/coverage-agentF/coverage-final.json delete mode 100644 client/coverage-jsheet/coverage-final.json create mode 100644 client/src/components/Map/PoiCategoryPill.test.tsx delete mode 100644 client/src/mobile/screens/collections/MCollListsDrawer.tsx delete mode 100644 client/tests/unit/mobile/collections/MCollListsDrawer.test.tsx diff --git a/.gitignore b/.gitignore index a83917a19..fa3baa3aa 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ yarn-error.log* # Coverage coverage +coverage-*/ *.lcov .nyc_output diff --git a/client/coverage-agentF/coverage-final.json b/client/coverage-agentF/coverage-final.json deleted file mode 100644 index 62f7efc82..000000000 --- a/client/coverage-agentF/coverage-final.json +++ /dev/null @@ -1,3 +0,0 @@ -{"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\services\\photoService.ts": {"path":"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\services\\photoService.ts","statementMap":{"0":{"start":{"line":9,"column":14},"end":{"line":9,"column":null}},"1":{"start":{"line":10,"column":17},"end":{"line":10,"column":null}},"2":{"start":{"line":11,"column":18},"end":{"line":11,"column":null}},"3":{"start":{"line":13,"column":23},"end":{"line":13,"column":null}},"4":{"start":{"line":17,"column":23},"end":{"line":17,"column":null}},"5":{"start":{"line":18,"column":21},"end":{"line":18,"column":null}},"6":{"start":{"line":19,"column":40},"end":{"line":19,"column":null}},"7":{"start":{"line":22,"column":2},"end":{"line":25,"column":null}},"8":{"start":{"line":23,"column":4},"end":{"line":23,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":26,"column":2},"end":{"line":26,"column":null}},"11":{"start":{"line":26,"column":32},"end":{"line":26,"column":58}},"12":{"start":{"line":30,"column":15},"end":{"line":30,"column":null}},"13":{"start":{"line":31,"column":2},"end":{"line":35,"column":null}},"14":{"start":{"line":32,"column":4},"end":{"line":32,"column":null}},"15":{"start":{"line":34,"column":4},"end":{"line":34,"column":null}},"16":{"start":{"line":39,"column":2},"end":{"line":39,"column":null}},"17":{"start":{"line":39,"column":36},"end":{"line":39,"column":45}},"18":{"start":{"line":40,"column":2},"end":{"line":40,"column":null}},"19":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"20":{"start":{"line":44,"column":41},"end":{"line":44,"column":50}},"21":{"start":{"line":45,"column":2},"end":{"line":45,"column":null}},"22":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"23":{"start":{"line":49,"column":27},"end":{"line":49,"column":null}},"24":{"start":{"line":50,"column":2},"end":{"line":50,"column":null}},"25":{"start":{"line":51,"column":2},"end":{"line":51,"column":null}},"26":{"start":{"line":51,"column":17},"end":{"line":51,"column":48}},"27":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"28":{"start":{"line":56,"column":32},"end":{"line":56,"column":null}},"29":{"start":{"line":57,"column":2},"end":{"line":57,"column":null}},"30":{"start":{"line":58,"column":2},"end":{"line":58,"column":null}},"31":{"start":{"line":58,"column":17},"end":{"line":58,"column":53}},"32":{"start":{"line":62,"column":2},"end":{"line":62,"column":null}},"33":{"start":{"line":66,"column":2},"end":{"line":66,"column":null}},"34":{"start":{"line":71,"column":2},"end":{"line":92,"column":null}},"35":{"start":{"line":72,"column":16},"end":{"line":72,"column":null}},"36":{"start":{"line":73,"column":4},"end":{"line":73,"column":null}},"37":{"start":{"line":74,"column":4},"end":{"line":89,"column":null}},"38":{"start":{"line":75,"column":6},"end":{"line":88,"column":null}},"39":{"start":{"line":76,"column":23},"end":{"line":76,"column":null}},"40":{"start":{"line":77,"column":8},"end":{"line":77,"column":null}},"41":{"start":{"line":78,"column":8},"end":{"line":78,"column":null}},"42":{"start":{"line":79,"column":20},"end":{"line":79,"column":null}},"43":{"start":{"line":80,"column":18},"end":{"line":80,"column":null}},"44":{"start":{"line":81,"column":14},"end":{"line":81,"column":null}},"45":{"start":{"line":82,"column":14},"end":{"line":82,"column":null}},"46":{"start":{"line":83,"column":8},"end":{"line":83,"column":null}},"47":{"start":{"line":84,"column":8},"end":{"line":84,"column":null}},"48":{"start":{"line":85,"column":8},"end":{"line":85,"column":null}},"49":{"start":{"line":86,"column":8},"end":{"line":86,"column":null}},"50":{"start":{"line":87,"column":8},"end":{"line":87,"column":null}},"51":{"start":{"line":88,"column":16},"end":{"line":88,"column":30}},"52":{"start":{"line":90,"column":4},"end":{"line":90,"column":null}},"53":{"start":{"line":90,"column":24},"end":{"line":90,"column":null}},"54":{"start":{"line":91,"column":4},"end":{"line":91,"column":null}},"55":{"start":{"line":103,"column":17},"end":{"line":103,"column":null}},"56":{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},"57":{"start":{"line":104,"column":16},"end":{"line":104,"column":36}},"58":{"start":{"line":104,"column":36},"end":{"line":104,"column":43}},"59":{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},"60":{"start":{"line":107,"column":4},"end":{"line":107,"column":null}},"61":{"start":{"line":107,"column":18},"end":{"line":107,"column":null}},"62":{"start":{"line":108,"column":4},"end":{"line":108,"column":null}},"63":{"start":{"line":112,"column":2},"end":{"line":122,"column":null}},"64":{"start":{"line":113,"column":30},"end":{"line":113,"column":null}},"65":{"start":{"line":114,"column":4},"end":{"line":114,"column":null}},"66":{"start":{"line":115,"column":4},"end":{"line":115,"column":null}},"67":{"start":{"line":116,"column":4},"end":{"line":116,"column":null}},"68":{"start":{"line":118,"column":4},"end":{"line":120,"column":null}},"69":{"start":{"line":119,"column":6},"end":{"line":119,"column":null}},"70":{"start":{"line":119,"column":19},"end":{"line":119,"column":47}},"71":{"start":{"line":119,"column":47},"end":{"line":119,"column":76}},"72":{"start":{"line":121,"column":4},"end":{"line":121,"column":null}},"73":{"start":{"line":124,"column":2},"end":{"line":124,"column":null}},"74":{"start":{"line":125,"column":2},"end":{"line":157,"column":null}},"75":{"start":{"line":126,"column":4},"end":{"line":156,"column":null}},"76":{"start":{"line":128,"column":25},"end":{"line":128,"column":null}},"77":{"start":{"line":129,"column":8},"end":{"line":135,"column":null}},"78":{"start":{"line":130,"column":36},"end":{"line":130,"column":null}},"79":{"start":{"line":131,"column":10},"end":{"line":131,"column":null}},"80":{"start":{"line":132,"column":10},"end":{"line":132,"column":null}},"81":{"start":{"line":133,"column":10},"end":{"line":133,"column":null}},"82":{"start":{"line":134,"column":10},"end":{"line":134,"column":null}},"83":{"start":{"line":138,"column":34},"end":{"line":138,"column":null}},"84":{"start":{"line":139,"column":8},"end":{"line":139,"column":null}},"85":{"start":{"line":140,"column":8},"end":{"line":140,"column":null}},"86":{"start":{"line":141,"column":8},"end":{"line":141,"column":null}},"87":{"start":{"line":144,"column":22},"end":{"line":144,"column":null}},"88":{"start":{"line":145,"column":8},"end":{"line":148,"column":null}},"89":{"start":{"line":146,"column":10},"end":{"line":146,"column":null}},"90":{"start":{"line":147,"column":10},"end":{"line":147,"column":null}},"91":{"start":{"line":151,"column":34},"end":{"line":151,"column":null}},"92":{"start":{"line":152,"column":8},"end":{"line":152,"column":null}},"93":{"start":{"line":153,"column":8},"end":{"line":153,"column":null}},"94":{"start":{"line":154,"column":8},"end":{"line":154,"column":null}},"95":{"start":{"line":156,"column":23},"end":{"line":156,"column":50}},"96":{"start":{"line":156,"column":50},"end":{"line":156,"column":71}},"97":{"start":{"line":161,"column":36},"end":{"line":161,"column":null}},"98":{"start":{"line":162,"column":2},"end":{"line":164,"column":null}},"99":{"start":{"line":163,"column":4},"end":{"line":163,"column":null}},"100":{"start":{"line":163,"column":24},"end":{"line":163,"column":null}},"101":{"start":{"line":165,"column":2},"end":{"line":165,"column":null}}},"fnMap":{"0":{"name":"acquireRequestSlot","decl":{"start":{"line":21,"column":9},"end":{"line":21,"column":45}},"loc":{"start":{"line":21,"column":45},"end":{"line":27,"column":null}},"line":21},"1":{"name":"(anonymous_1)","decl":{"start":{"line":26,"column":13},"end":{"line":26,"column":21}},"loc":{"start":{"line":26,"column":32},"end":{"line":26,"column":58}},"line":26},"2":{"name":"releaseRequestSlot","decl":{"start":{"line":29,"column":9},"end":{"line":29,"column":36}},"loc":{"start":{"line":29,"column":36},"end":{"line":36,"column":null}},"line":29},"3":{"name":"notify","decl":{"start":{"line":38,"column":9},"end":{"line":38,"column":16}},"loc":{"start":{"line":38,"column":48},"end":{"line":41,"column":null}},"line":38},"4":{"name":"(anonymous_4)","decl":{"start":{"line":39,"column":22},"end":{"line":39,"column":30}},"loc":{"start":{"line":39,"column":36},"end":{"line":39,"column":45}},"line":39},"5":{"name":"notifyThumb","decl":{"start":{"line":43,"column":9},"end":{"line":43,"column":21}},"loc":{"start":{"line":43,"column":49},"end":{"line":46,"column":null}},"line":43},"6":{"name":"(anonymous_6)","decl":{"start":{"line":44,"column":27},"end":{"line":44,"column":35}},"loc":{"start":{"line":44,"column":41},"end":{"line":44,"column":50}},"line":44},"7":{"name":"onPhotoLoaded","decl":{"start":{"line":48,"column":16},"end":{"line":48,"column":30}},"loc":{"start":{"line":48,"column":88},"end":{"line":52,"column":null}},"line":48},"8":{"name":"(anonymous_8)","decl":{"start":{"line":51,"column":2},"end":{"line":51,"column":15}},"loc":{"start":{"line":51,"column":15},"end":{"line":51,"column":null}},"line":51},"9":{"name":"onThumbReady","decl":{"start":{"line":55,"column":16},"end":{"line":55,"column":29}},"loc":{"start":{"line":55,"column":83},"end":{"line":59,"column":null}},"line":55},"10":{"name":"(anonymous_10)","decl":{"start":{"line":58,"column":2},"end":{"line":58,"column":15}},"loc":{"start":{"line":58,"column":15},"end":{"line":58,"column":null}},"line":58},"11":{"name":"getCached","decl":{"start":{"line":61,"column":16},"end":{"line":61,"column":26}},"loc":{"start":{"line":61,"column":63},"end":{"line":63,"column":null}},"line":61},"12":{"name":"isLoading","decl":{"start":{"line":65,"column":16},"end":{"line":65,"column":26}},"loc":{"start":{"line":65,"column":48},"end":{"line":67,"column":null}},"line":65},"13":{"name":"urlToBase64","decl":{"start":{"line":70,"column":16},"end":{"line":70,"column":28}},"loc":{"start":{"line":70,"column":84},"end":{"line":93,"column":null}},"line":70},"14":{"name":"(anonymous_14)","decl":{"start":{"line":71,"column":13},"end":{"line":71,"column":21}},"loc":{"start":{"line":71,"column":32},"end":{"line":92,"column":3}},"line":71},"15":{"name":"(anonymous_15)","decl":{"start":{"line":74,"column":8},"end":{"line":74,"column":23}},"loc":{"start":{"line":74,"column":23},"end":{"line":89,"column":null}},"line":74},"16":{"name":"(anonymous_16)","decl":{"start":{"line":90,"column":8},"end":{"line":90,"column":24}},"loc":{"start":{"line":90,"column":24},"end":{"line":90,"column":null}},"line":90},"17":{"name":"fetchPhoto","decl":{"start":{"line":95,"column":16},"end":{"line":95,"column":null}},"loc":{"start":{"line":102,"column":2},"end":{"line":158,"column":null}},"line":102},"18":{"name":"(anonymous_18)","decl":{"start":{"line":118,"column":25},"end":{"line":118,"column":30}},"loc":{"start":{"line":118,"column":39},"end":{"line":120,"column":5}},"line":118},"19":{"name":"(anonymous_19)","decl":{"start":{"line":125,"column":23},"end":{"line":125,"column":null}},"loc":{"start":{"line":126,"column":4},"end":{"line":156,"column":null}},"line":126},"20":{"name":"(anonymous_20)","decl":{"start":{"line":127,"column":12},"end":{"line":127,"column":19}},"loc":{"start":{"line":127,"column":51},"end":{"line":149,"column":7}},"line":127},"21":{"name":"(anonymous_21)","decl":{"start":{"line":150,"column":7},"end":{"line":150,"column":19}},"loc":{"start":{"line":150,"column":19},"end":{"line":155,"column":7}},"line":150},"22":{"name":"(anonymous_22)","decl":{"start":{"line":156,"column":7},"end":{"line":156,"column":21}},"loc":{"start":{"line":156,"column":21},"end":{"line":156,"column":72}},"line":156},"23":{"name":"getAllThumbs","decl":{"start":{"line":160,"column":16},"end":{"line":160,"column":55}},"loc":{"start":{"line":160,"column":55},"end":{"line":166,"column":null}},"line":160}},"branchMap":{"0":{"loc":{"start":{"line":22,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":22,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":22},"1":{"loc":{"start":{"line":31,"column":2},"end":{"line":35,"column":null}},"type":"if","locations":[{"start":{"line":31,"column":2},"end":{"line":35,"column":null}},{"start":{"line":33,"column":9},"end":{"line":35,"column":null}}],"line":31},"2":{"loc":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"type":"if","locations":[{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},{"start":{},"end":{}}],"line":49},"3":{"loc":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"type":"if","locations":[{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},{"start":{},"end":{}}],"line":56},"4":{"loc":{"start":{"line":70,"column":41},"end":{"line":70,"column":84}},"type":"default-arg","locations":[{"start":{"line":70,"column":56},"end":{"line":70,"column":84}}],"line":70},"5":{"loc":{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},"type":"if","locations":[{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},{"start":{},"end":{}}],"line":104},"6":{"loc":{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},"type":"if","locations":[{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},{"start":{},"end":{}}],"line":106},"7":{"loc":{"start":{"line":107,"column":4},"end":{"line":107,"column":null}},"type":"if","locations":[{"start":{"line":107,"column":4},"end":{"line":107,"column":null}},{"start":{},"end":{}}],"line":107},"8":{"loc":{"start":{"line":112,"column":2},"end":{"line":122,"column":null}},"type":"if","locations":[{"start":{"line":112,"column":2},"end":{"line":122,"column":null}},{"start":{},"end":{}}],"line":112},"9":{"loc":{"start":{"line":112,"column":6},"end":{"line":112,"column":63}},"type":"binary-expr","locations":[{"start":{"line":112,"column":6},"end":{"line":112,"column":17}},{"start":{"line":112,"column":17},"end":{"line":112,"column":63}}],"line":112},"10":{"loc":{"start":{"line":119,"column":6},"end":{"line":119,"column":null}},"type":"if","locations":[{"start":{"line":119,"column":6},"end":{"line":119,"column":null}},{"start":{},"end":{}}],"line":119},"11":{"loc":{"start":{"line":128,"column":25},"end":{"line":128,"column":null}},"type":"binary-expr","locations":[{"start":{"line":128,"column":25},"end":{"line":128,"column":42}},{"start":{"line":128,"column":42},"end":{"line":128,"column":null}}],"line":128},"12":{"loc":{"start":{"line":129,"column":8},"end":{"line":135,"column":null}},"type":"if","locations":[{"start":{"line":129,"column":8},"end":{"line":135,"column":null}},{"start":{},"end":{}}],"line":129},"13":{"loc":{"start":{"line":145,"column":8},"end":{"line":148,"column":null}},"type":"if","locations":[{"start":{"line":145,"column":8},"end":{"line":148,"column":null}},{"start":{},"end":{}}],"line":145},"14":{"loc":{"start":{"line":163,"column":4},"end":{"line":163,"column":null}},"type":"if","locations":[{"start":{"line":163,"column":4},"end":{"line":163,"column":null}},{"start":{},"end":{}}],"line":163}},"s":{"0":25,"1":25,"2":25,"3":25,"4":25,"5":25,"6":25,"7":23,"8":22,"9":22,"10":1,"11":1,"12":18,"13":18,"14":1,"15":17,"16":20,"17":5,"18":20,"19":15,"20":4,"21":15,"22":6,"23":5,"24":6,"25":6,"26":1,"27":7,"28":6,"29":7,"30":7,"31":1,"32":9,"33":3,"34":21,"35":21,"36":21,"37":21,"38":18,"39":18,"40":18,"41":18,"42":18,"43":18,"44":18,"45":18,"46":18,"47":18,"48":18,"49":18,"50":18,"51":1,"52":21,"53":3,"54":21,"55":29,"56":29,"57":1,"58":1,"59":28,"60":3,"61":2,"62":3,"63":25,"64":2,"65":2,"66":2,"67":2,"68":2,"69":2,"70":1,"71":1,"72":2,"73":23,"74":23,"75":23,"76":17,"77":17,"78":2,"79":2,"80":2,"81":2,"82":2,"83":15,"84":15,"85":15,"86":17,"87":17,"88":15,"89":14,"90":14,"91":1,"92":1,"93":1,"94":1,"95":18,"96":18,"97":1,"98":1,"99":2,"100":1,"101":1},"f":{"0":23,"1":1,"2":18,"3":20,"4":5,"5":15,"6":4,"7":6,"8":1,"9":7,"10":1,"11":9,"12":3,"13":21,"14":21,"15":18,"16":3,"17":29,"18":2,"19":23,"20":17,"21":1,"22":18,"23":1},"b":{"0":[22,1],"1":[1,17],"2":[5,1],"3":[6,1],"4":[21],"5":[1,28],"6":[3,25],"7":[2,1],"8":[2,23],"9":[25,25],"10":[1,1],"11":[17,2],"12":[2,15],"13":[14,1],"14":[1,1]},"meta":{"lastBranch":15,"lastFunction":24,"lastStatement":102,"seen":{"s:9:14:9:Infinity":0,"s:10:17:10:Infinity":1,"s:11:18:11:Infinity":2,"s:13:23:13:Infinity":3,"s:17:23:17:Infinity":4,"s:18:21:18:Infinity":5,"s:19:40:19:Infinity":6,"f:21:9:21:45":0,"b:22:2:25:Infinity:undefined:undefined:undefined:undefined":0,"s:22:2:25:Infinity":7,"s:23:4:23:Infinity":8,"s:24:4:24:Infinity":9,"s:26:2:26:Infinity":10,"f:26:13:26:21":1,"s:26:32:26:58":11,"f:29:9:29:36":2,"s:30:15:30:Infinity":12,"b:31:2:35:Infinity:33:9:35:Infinity":1,"s:31:2:35:Infinity":13,"s:32:4:32:Infinity":14,"s:34:4:34:Infinity":15,"f:38:9:38:16":3,"s:39:2:39:Infinity":16,"f:39:22:39:30":4,"s:39:36:39:45":17,"s:40:2:40:Infinity":18,"f:43:9:43:21":5,"s:44:2:44:Infinity":19,"f:44:27:44:35":6,"s:44:41:44:50":20,"s:45:2:45:Infinity":21,"f:48:16:48:30":7,"b:49:2:49:Infinity:undefined:undefined:undefined:undefined":2,"s:49:2:49:Infinity":22,"s:49:27:49:Infinity":23,"s:50:2:50:Infinity":24,"s:51:2:51:Infinity":25,"f:51:2:51:15":8,"s:51:17:51:48":26,"f:55:16:55:29":9,"b:56:2:56:Infinity:undefined:undefined:undefined:undefined":3,"s:56:2:56:Infinity":27,"s:56:32:56:Infinity":28,"s:57:2:57:Infinity":29,"s:58:2:58:Infinity":30,"f:58:2:58:15":10,"s:58:17:58:53":31,"f:61:16:61:26":11,"s:62:2:62:Infinity":32,"f:65:16:65:26":12,"s:66:2:66:Infinity":33,"f:70:16:70:28":13,"b:70:56:70:84":4,"s:71:2:92:Infinity":34,"f:71:13:71:21":14,"s:72:16:72:Infinity":35,"s:73:4:73:Infinity":36,"s:74:4:89:Infinity":37,"f:74:8:74:23":15,"s:75:6:88:Infinity":38,"s:76:23:76:Infinity":39,"s:77:8:77:Infinity":40,"s:78:8:78:Infinity":41,"s:79:20:79:Infinity":42,"s:80:18:80:Infinity":43,"s:81:14:81:Infinity":44,"s:82:14:82:Infinity":45,"s:83:8:83:Infinity":46,"s:84:8:84:Infinity":47,"s:85:8:85:Infinity":48,"s:86:8:86:Infinity":49,"s:87:8:87:Infinity":50,"s:88:16:88:30":51,"s:90:4:90:Infinity":52,"f:90:8:90:24":16,"s:90:24:90:Infinity":53,"s:91:4:91:Infinity":54,"f:95:16:95:Infinity":17,"s:103:17:103:Infinity":55,"b:104:2:104:Infinity:undefined:undefined:undefined:undefined":5,"s:104:2:104:Infinity":56,"s:104:16:104:36":57,"s:104:36:104:43":58,"b:106:2:109:Infinity:undefined:undefined:undefined:undefined":6,"s:106:2:109:Infinity":59,"b:107:4:107:Infinity:undefined:undefined:undefined:undefined":7,"s:107:4:107:Infinity":60,"s:107:18:107:Infinity":61,"s:108:4:108:Infinity":62,"b:112:2:122:Infinity:undefined:undefined:undefined:undefined":8,"s:112:2:122:Infinity":63,"b:112:6:112:17:112:17:112:63":9,"s:113:30:113:Infinity":64,"s:114:4:114:Infinity":65,"s:115:4:115:Infinity":66,"s:116:4:116:Infinity":67,"s:118:4:120:Infinity":68,"f:118:25:118:30":18,"b:119:6:119:Infinity:undefined:undefined:undefined:undefined":10,"s:119:6:119:Infinity":69,"s:119:19:119:47":70,"s:119:47:119:76":71,"s:121:4:121:Infinity":72,"s:124:2:124:Infinity":73,"s:125:2:157:Infinity":74,"f:125:23:125:Infinity":19,"s:126:4:156:Infinity":75,"f:127:12:127:19":20,"s:128:25:128:Infinity":76,"b:128:25:128:42:128:42:128:Infinity":11,"b:129:8:135:Infinity:undefined:undefined:undefined:undefined":12,"s:129:8:135:Infinity":77,"s:130:36:130:Infinity":78,"s:131:10:131:Infinity":79,"s:132:10:132:Infinity":80,"s:133:10:133:Infinity":81,"s:134:10:134:Infinity":82,"s:138:34:138:Infinity":83,"s:139:8:139:Infinity":84,"s:140:8:140:Infinity":85,"s:141:8:141:Infinity":86,"s:144:22:144:Infinity":87,"b:145:8:148:Infinity:undefined:undefined:undefined:undefined":13,"s:145:8:148:Infinity":88,"s:146:10:146:Infinity":89,"s:147:10:147:Infinity":90,"f:150:7:150:19":21,"s:151:34:151:Infinity":91,"s:152:8:152:Infinity":92,"s:153:8:153:Infinity":93,"s:154:8:154:Infinity":94,"f:156:7:156:21":22,"s:156:23:156:50":95,"s:156:50:156:71":96,"f:160:16:160:55":23,"s:161:36:161:Infinity":97,"s:162:2:164:Infinity":98,"b:163:4:163:Infinity:undefined:undefined:undefined:undefined":14,"s:163:4:163:Infinity":99,"s:163:24:163:Infinity":100,"s:165:2:165:Infinity":101},"fnNames":{}}} -,"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\store\\authStore.ts": {"path":"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\store\\authStore.ts","statementMap":{"0":{"start":{"line":75,"column":19},"end":{"line":75,"column":null}},"1":{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},"2":{"start":{"line":83,"column":2},"end":{"line":87,"column":null}},"3":{"start":{"line":84,"column":4},"end":{"line":84,"column":null}},"4":{"start":{"line":86,"column":4},"end":{"line":86,"column":null}},"5":{"start":{"line":90,"column":13},"end":{"line":369,"column":null}},"6":{"start":{"line":92,"column":17},"end":{"line":348,"column":null}},"7":{"start":{"line":111,"column":4},"end":{"line":111,"column":null}},"8":{"start":{"line":112,"column":4},"end":{"line":112,"column":null}},"9":{"start":{"line":113,"column":4},"end":{"line":136,"column":null}},"10":{"start":{"line":114,"column":19},"end":{"line":114,"column":null}},"11":{"start":{"line":115,"column":6},"end":{"line":118,"column":null}},"12":{"start":{"line":116,"column":8},"end":{"line":116,"column":null}},"13":{"start":{"line":117,"column":8},"end":{"line":117,"column":null}},"14":{"start":{"line":119,"column":6},"end":{"line":124,"column":null}},"15":{"start":{"line":125,"column":6},"end":{"line":125,"column":null}},"16":{"start":{"line":126,"column":6},"end":{"line":126,"column":null}},"17":{"start":{"line":127,"column":6},"end":{"line":127,"column":null}},"18":{"start":{"line":128,"column":6},"end":{"line":130,"column":null}},"19":{"start":{"line":129,"column":8},"end":{"line":129,"column":null}},"20":{"start":{"line":131,"column":6},"end":{"line":131,"column":null}},"21":{"start":{"line":133,"column":12},"end":{"line":133,"column":null}},"22":{"start":{"line":134,"column":6},"end":{"line":134,"column":null}},"23":{"start":{"line":135,"column":6},"end":{"line":135,"column":null}},"24":{"start":{"line":140,"column":4},"end":{"line":140,"column":null}},"25":{"start":{"line":141,"column":4},"end":{"line":141,"column":null}},"26":{"start":{"line":142,"column":4},"end":{"line":161,"column":null}},"27":{"start":{"line":143,"column":19},"end":{"line":143,"column":null}},"28":{"start":{"line":144,"column":6},"end":{"line":149,"column":null}},"29":{"start":{"line":150,"column":6},"end":{"line":150,"column":null}},"30":{"start":{"line":151,"column":6},"end":{"line":151,"column":null}},"31":{"start":{"line":152,"column":6},"end":{"line":152,"column":null}},"32":{"start":{"line":153,"column":6},"end":{"line":155,"column":null}},"33":{"start":{"line":154,"column":8},"end":{"line":154,"column":null}},"34":{"start":{"line":156,"column":6},"end":{"line":156,"column":null}},"35":{"start":{"line":158,"column":12},"end":{"line":158,"column":null}},"36":{"start":{"line":159,"column":6},"end":{"line":159,"column":null}},"37":{"start":{"line":160,"column":6},"end":{"line":160,"column":null}},"38":{"start":{"line":165,"column":4},"end":{"line":165,"column":null}},"39":{"start":{"line":166,"column":4},"end":{"line":166,"column":null}},"40":{"start":{"line":167,"column":4},"end":{"line":184,"column":null}},"41":{"start":{"line":168,"column":19},"end":{"line":168,"column":null}},"42":{"start":{"line":169,"column":6},"end":{"line":174,"column":null}},"43":{"start":{"line":175,"column":6},"end":{"line":175,"column":null}},"44":{"start":{"line":176,"column":6},"end":{"line":176,"column":null}},"45":{"start":{"line":177,"column":6},"end":{"line":177,"column":null}},"46":{"start":{"line":178,"column":6},"end":{"line":178,"column":null}},"47":{"start":{"line":179,"column":6},"end":{"line":179,"column":null}},"48":{"start":{"line":181,"column":12},"end":{"line":181,"column":null}},"49":{"start":{"line":182,"column":6},"end":{"line":182,"column":null}},"50":{"start":{"line":183,"column":6},"end":{"line":183,"column":null}},"51":{"start":{"line":189,"column":4},"end":{"line":189,"column":null}},"52":{"start":{"line":190,"column":4},"end":{"line":190,"column":null}},"53":{"start":{"line":192,"column":4},"end":{"line":192,"column":null}},"54":{"start":{"line":194,"column":4},"end":{"line":194,"column":null}},"55":{"start":{"line":195,"column":4},"end":{"line":195,"column":null}},"56":{"start":{"line":198,"column":4},"end":{"line":198,"column":null}},"57":{"start":{"line":201,"column":4},"end":{"line":201,"column":null}},"58":{"start":{"line":203,"column":4},"end":{"line":203,"column":null}},"59":{"start":{"line":205,"column":4},"end":{"line":210,"column":null}},"60":{"start":{"line":206,"column":6},"end":{"line":209,"column":null}},"61":{"start":{"line":212,"column":4},"end":{"line":212,"column":null}},"62":{"start":{"line":214,"column":4},"end":{"line":219,"column":null}},"63":{"start":{"line":223,"column":16},"end":{"line":223,"column":null}},"64":{"start":{"line":224,"column":19},"end":{"line":224,"column":null}},"65":{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},"66":{"start":{"line":225,"column":17},"end":{"line":225,"column":null}},"67":{"start":{"line":226,"column":4},"end":{"line":261,"column":null}},"68":{"start":{"line":227,"column":19},"end":{"line":227,"column":null}},"69":{"start":{"line":228,"column":6},"end":{"line":228,"column":null}},"70":{"start":{"line":228,"column":32},"end":{"line":228,"column":null}},"71":{"start":{"line":229,"column":6},"end":{"line":234,"column":null}},"72":{"start":{"line":235,"column":6},"end":{"line":235,"column":null}},"73":{"start":{"line":236,"column":6},"end":{"line":236,"column":null}},"74":{"start":{"line":238,"column":6},"end":{"line":238,"column":null}},"75":{"start":{"line":238,"column":32},"end":{"line":238,"column":null}},"76":{"start":{"line":239,"column":21},"end":{"line":241,"column":null}},"77":{"start":{"line":242,"column":6},"end":{"line":260,"column":null}},"78":{"start":{"line":244,"column":8},"end":{"line":249,"column":null}},"79":{"start":{"line":250,"column":13},"end":{"line":260,"column":null}},"80":{"start":{"line":253,"column":8},"end":{"line":253,"column":null}},"81":{"start":{"line":259,"column":8},"end":{"line":259,"column":null}},"82":{"start":{"line":265,"column":4},"end":{"line":273,"column":null}},"83":{"start":{"line":266,"column":6},"end":{"line":266,"column":null}},"84":{"start":{"line":267,"column":6},"end":{"line":270,"column":null}},"85":{"start":{"line":267,"column":22},"end":{"line":270,"column":8}},"86":{"start":{"line":272,"column":6},"end":{"line":272,"column":null}},"87":{"start":{"line":277,"column":4},"end":{"line":285,"column":null}},"88":{"start":{"line":278,"column":19},"end":{"line":278,"column":null}},"89":{"start":{"line":279,"column":6},"end":{"line":279,"column":null}},"90":{"start":{"line":280,"column":6},"end":{"line":282,"column":null}},"91":{"start":{"line":281,"column":8},"end":{"line":281,"column":null}},"92":{"start":{"line":284,"column":6},"end":{"line":284,"column":null}},"93":{"start":{"line":289,"column":4},"end":{"line":294,"column":null}},"94":{"start":{"line":290,"column":19},"end":{"line":290,"column":null}},"95":{"start":{"line":291,"column":6},"end":{"line":291,"column":null}},"96":{"start":{"line":293,"column":6},"end":{"line":293,"column":null}},"97":{"start":{"line":298,"column":21},"end":{"line":298,"column":null}},"98":{"start":{"line":299,"column":4},"end":{"line":299,"column":null}},"99":{"start":{"line":300,"column":17},"end":{"line":300,"column":null}},"100":{"start":{"line":301,"column":4},"end":{"line":301,"column":null}},"101":{"start":{"line":301,"column":20},"end":{"line":301,"column":97}},"102":{"start":{"line":302,"column":4},"end":{"line":302,"column":null}},"103":{"start":{"line":306,"column":4},"end":{"line":306,"column":null}},"104":{"start":{"line":307,"column":4},"end":{"line":307,"column":null}},"105":{"start":{"line":307,"column":20},"end":{"line":307,"column":86}},"106":{"start":{"line":311,"column":4},"end":{"line":312,"column":null}},"107":{"start":{"line":311,"column":13},"end":{"line":311,"column":null}},"108":{"start":{"line":312,"column":9},"end":{"line":312,"column":null}},"109":{"start":{"line":313,"column":4},"end":{"line":313,"column":null}},"110":{"start":{"line":316,"column":32},"end":{"line":316,"column":null}},"111":{"start":{"line":317,"column":37},"end":{"line":317,"column":null}},"112":{"start":{"line":318,"column":34},"end":{"line":318,"column":null}},"113":{"start":{"line":319,"column":35},"end":{"line":319,"column":null}},"114":{"start":{"line":320,"column":37},"end":{"line":320,"column":null}},"115":{"start":{"line":321,"column":38},"end":{"line":321,"column":null}},"116":{"start":{"line":322,"column":45},"end":{"line":322,"column":null}},"117":{"start":{"line":323,"column":44},"end":{"line":323,"column":null}},"118":{"start":{"line":324,"column":50},"end":{"line":324,"column":null}},"119":{"start":{"line":325,"column":45},"end":{"line":325,"column":null}},"120":{"start":{"line":328,"column":4},"end":{"line":328,"column":null}},"121":{"start":{"line":329,"column":4},"end":{"line":329,"column":null}},"122":{"start":{"line":330,"column":4},"end":{"line":346,"column":null}},"123":{"start":{"line":331,"column":19},"end":{"line":331,"column":null}},"124":{"start":{"line":332,"column":6},"end":{"line":338,"column":null}},"125":{"start":{"line":339,"column":6},"end":{"line":339,"column":null}},"126":{"start":{"line":340,"column":6},"end":{"line":340,"column":null}},"127":{"start":{"line":341,"column":6},"end":{"line":341,"column":null}},"128":{"start":{"line":343,"column":12},"end":{"line":343,"column":null}},"129":{"start":{"line":344,"column":6},"end":{"line":344,"column":null}},"130":{"start":{"line":345,"column":6},"end":{"line":345,"column":null}},"131":{"start":{"line":356,"column":28},"end":{"line":367,"column":null}}},"fnMap":{"0":{"name":"onAuthSuccess","decl":{"start":{"line":81,"column":15},"end":{"line":81,"column":29}},"loc":{"start":{"line":81,"column":60},"end":{"line":88,"column":null}},"line":81},"1":{"name":"(anonymous_1)","decl":{"start":{"line":91,"column":2},"end":{"line":91,"column":null}},"loc":{"start":{"line":92,"column":17},"end":{"line":348,"column":null}},"line":92},"2":{"name":"(anonymous_2)","decl":{"start":{"line":110,"column":9},"end":{"line":110,"column":16}},"loc":{"start":{"line":110,"column":74},"end":{"line":137,"column":null}},"line":110},"3":{"name":"(anonymous_3)","decl":{"start":{"line":139,"column":20},"end":{"line":139,"column":27}},"loc":{"start":{"line":139,"column":84},"end":{"line":162,"column":null}},"line":139},"4":{"name":"(anonymous_4)","decl":{"start":{"line":164,"column":12},"end":{"line":164,"column":19}},"loc":{"start":{"line":164,"column":96},"end":{"line":185,"column":null}},"line":164},"5":{"name":"(anonymous_5)","decl":{"start":{"line":187,"column":10},"end":{"line":187,"column":22}},"loc":{"start":{"line":187,"column":22},"end":{"line":220,"column":null}},"line":187},"6":{"name":"(anonymous_6)","decl":{"start":{"line":203,"column":80},"end":{"line":203,"column":92}},"loc":{"start":{"line":203,"column":92},"end":{"line":203,"column":94}},"line":203},"7":{"name":"(anonymous_7)","decl":{"start":{"line":207,"column":34},"end":{"line":207,"column":46}},"loc":{"start":{"line":207,"column":46},"end":{"line":207,"column":48}},"line":207},"8":{"name":"(anonymous_8)","decl":{"start":{"line":208,"column":38},"end":{"line":208,"column":50}},"loc":{"start":{"line":208,"column":50},"end":{"line":208,"column":52}},"line":208},"9":{"name":"(anonymous_9)","decl":{"start":{"line":222,"column":12},"end":{"line":222,"column":19}},"loc":{"start":{"line":222,"column":51},"end":{"line":262,"column":null}},"line":222},"10":{"name":"(anonymous_10)","decl":{"start":{"line":264,"column":17},"end":{"line":264,"column":24}},"loc":{"start":{"line":264,"column":47},"end":{"line":274,"column":null}},"line":264},"11":{"name":"(anonymous_11)","decl":{"start":{"line":267,"column":6},"end":{"line":267,"column":11}},"loc":{"start":{"line":267,"column":22},"end":{"line":270,"column":8}},"line":267},"12":{"name":"(anonymous_12)","decl":{"start":{"line":276,"column":17},"end":{"line":276,"column":24}},"loc":{"start":{"line":276,"column":64},"end":{"line":286,"column":null}},"line":276},"13":{"name":"(anonymous_13)","decl":{"start":{"line":288,"column":17},"end":{"line":288,"column":24}},"loc":{"start":{"line":288,"column":55},"end":{"line":295,"column":null}},"line":288},"14":{"name":"(anonymous_14)","decl":{"start":{"line":297,"column":16},"end":{"line":297,"column":23}},"loc":{"start":{"line":297,"column":38},"end":{"line":303,"column":null}},"line":297},"15":{"name":"(anonymous_15)","decl":{"start":{"line":301,"column":4},"end":{"line":301,"column":9}},"loc":{"start":{"line":301,"column":20},"end":{"line":301,"column":97}},"line":301},"16":{"name":"(anonymous_16)","decl":{"start":{"line":305,"column":16},"end":{"line":305,"column":28}},"loc":{"start":{"line":305,"column":28},"end":{"line":308,"column":null}},"line":305},"17":{"name":"(anonymous_17)","decl":{"start":{"line":307,"column":4},"end":{"line":307,"column":9}},"loc":{"start":{"line":307,"column":20},"end":{"line":307,"column":86}},"line":307},"18":{"name":"(anonymous_18)","decl":{"start":{"line":310,"column":2},"end":{"line":310,"column":16}},"loc":{"start":{"line":310,"column":33},"end":{"line":314,"column":null}},"line":310},"19":{"name":"(anonymous_19)","decl":{"start":{"line":316,"column":2},"end":{"line":316,"column":15}},"loc":{"start":{"line":316,"column":32},"end":{"line":316,"column":null}},"line":316},"20":{"name":"(anonymous_20)","decl":{"start":{"line":317,"column":2},"end":{"line":317,"column":20}},"loc":{"start":{"line":317,"column":37},"end":{"line":317,"column":null}},"line":317},"21":{"name":"(anonymous_21)","decl":{"start":{"line":318,"column":2},"end":{"line":318,"column":18}},"loc":{"start":{"line":318,"column":34},"end":{"line":318,"column":null}},"line":318},"22":{"name":"(anonymous_22)","decl":{"start":{"line":319,"column":2},"end":{"line":319,"column":18}},"loc":{"start":{"line":319,"column":35},"end":{"line":319,"column":null}},"line":319},"23":{"name":"(anonymous_23)","decl":{"start":{"line":320,"column":2},"end":{"line":320,"column":22}},"loc":{"start":{"line":320,"column":37},"end":{"line":320,"column":null}},"line":320},"24":{"name":"(anonymous_24)","decl":{"start":{"line":321,"column":2},"end":{"line":321,"column":21}},"loc":{"start":{"line":321,"column":38},"end":{"line":321,"column":null}},"line":321},"25":{"name":"(anonymous_25)","decl":{"start":{"line":322,"column":2},"end":{"line":322,"column":28}},"loc":{"start":{"line":322,"column":45},"end":{"line":322,"column":null}},"line":322},"26":{"name":"(anonymous_26)","decl":{"start":{"line":323,"column":2},"end":{"line":323,"column":27}},"loc":{"start":{"line":323,"column":44},"end":{"line":323,"column":null}},"line":323},"27":{"name":"(anonymous_27)","decl":{"start":{"line":324,"column":2},"end":{"line":324,"column":33}},"loc":{"start":{"line":324,"column":50},"end":{"line":324,"column":null}},"line":324},"28":{"name":"(anonymous_28)","decl":{"start":{"line":325,"column":2},"end":{"line":325,"column":28}},"loc":{"start":{"line":325,"column":45},"end":{"line":325,"column":null}},"line":325},"29":{"name":"(anonymous_29)","decl":{"start":{"line":327,"column":13},"end":{"line":327,"column":25}},"loc":{"start":{"line":327,"column":25},"end":{"line":347,"column":null}},"line":327},"30":{"name":"(anonymous_30)","decl":{"start":{"line":356,"column":4},"end":{"line":356,"column":17}},"loc":{"start":{"line":356,"column":28},"end":{"line":367,"column":null}},"line":356}},"branchMap":{"0":{"loc":{"start":{"line":115,"column":6},"end":{"line":118,"column":null}},"type":"if","locations":[{"start":{"line":115,"column":6},"end":{"line":118,"column":null}},{"start":{},"end":{}}],"line":115},"1":{"loc":{"start":{"line":115,"column":10},"end":{"line":115,"column":47}},"type":"binary-expr","locations":[{"start":{"line":115,"column":10},"end":{"line":115,"column":31}},{"start":{"line":115,"column":31},"end":{"line":115,"column":47}}],"line":115},"2":{"loc":{"start":{"line":128,"column":6},"end":{"line":130,"column":null}},"type":"if","locations":[{"start":{"line":128,"column":6},"end":{"line":130,"column":null}},{"start":{},"end":{}}],"line":128},"3":{"loc":{"start":{"line":153,"column":6},"end":{"line":155,"column":null}},"type":"if","locations":[{"start":{"line":153,"column":6},"end":{"line":155,"column":null}},{"start":{},"end":{}}],"line":153},"4":{"loc":{"start":{"line":205,"column":4},"end":{"line":210,"column":null}},"type":"if","locations":[{"start":{"line":205,"column":4},"end":{"line":210,"column":null}},{"start":{},"end":{}}],"line":205},"5":{"loc":{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},"type":"if","locations":[{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},{"start":{},"end":{}}],"line":225},"6":{"loc":{"start":{"line":228,"column":6},"end":{"line":228,"column":null}},"type":"if","locations":[{"start":{"line":228,"column":6},"end":{"line":228,"column":null}},{"start":{},"end":{}}],"line":228},"7":{"loc":{"start":{"line":238,"column":6},"end":{"line":238,"column":null}},"type":"if","locations":[{"start":{"line":238,"column":6},"end":{"line":238,"column":null}},{"start":{},"end":{}}],"line":238},"8":{"loc":{"start":{"line":239,"column":21},"end":{"line":241,"column":null}},"type":"cond-expr","locations":[{"start":{"line":240,"column":11},"end":{"line":240,"column":null}},{"start":{"line":241,"column":10},"end":{"line":241,"column":null}}],"line":239},"9":{"loc":{"start":{"line":239,"column":21},"end":{"line":239,"column":null}},"type":"binary-expr","locations":[{"start":{"line":239,"column":21},"end":{"line":239,"column":28}},{"start":{"line":239,"column":28},"end":{"line":239,"column":55}},{"start":{"line":239,"column":55},"end":{"line":239,"column":null}}],"line":239},"10":{"loc":{"start":{"line":242,"column":6},"end":{"line":260,"column":null}},"type":"if","locations":[{"start":{"line":242,"column":6},"end":{"line":260,"column":null}},{"start":{"line":250,"column":13},"end":{"line":260,"column":null}}],"line":242},"11":{"loc":{"start":{"line":250,"column":13},"end":{"line":260,"column":null}},"type":"if","locations":[{"start":{"line":250,"column":13},"end":{"line":260,"column":null}},{"start":{"line":254,"column":13},"end":{"line":260,"column":null}}],"line":250},"12":{"loc":{"start":{"line":250,"column":17},"end":{"line":250,"column":96}},"type":"binary-expr","locations":[{"start":{"line":250,"column":17},"end":{"line":250,"column":41}},{"start":{"line":250,"column":41},"end":{"line":250,"column":77}},{"start":{"line":250,"column":77},"end":{"line":250,"column":96}}],"line":250},"13":{"loc":{"start":{"line":268,"column":14},"end":{"line":268,"column":null}},"type":"cond-expr","locations":[{"start":{"line":268,"column":27},"end":{"line":268,"column":74}},{"start":{"line":268,"column":74},"end":{"line":268,"column":null}}],"line":268},"14":{"loc":{"start":{"line":268,"column":58},"end":{"line":268,"column":70}},"type":"binary-expr","locations":[{"start":{"line":268,"column":58},"end":{"line":268,"column":65}},{"start":{"line":268,"column":65},"end":{"line":268,"column":70}}],"line":268},"15":{"loc":{"start":{"line":280,"column":6},"end":{"line":282,"column":null}},"type":"if","locations":[{"start":{"line":280,"column":6},"end":{"line":282,"column":null}},{"start":{},"end":{}}],"line":280},"16":{"loc":{"start":{"line":301,"column":28},"end":{"line":301,"column":95}},"type":"cond-expr","locations":[{"start":{"line":301,"column":41},"end":{"line":301,"column":90}},{"start":{"line":301,"column":90},"end":{"line":301,"column":95}}],"line":301},"17":{"loc":{"start":{"line":307,"column":28},"end":{"line":307,"column":84}},"type":"cond-expr","locations":[{"start":{"line":307,"column":41},"end":{"line":307,"column":79}},{"start":{"line":307,"column":79},"end":{"line":307,"column":84}}],"line":307},"18":{"loc":{"start":{"line":311,"column":4},"end":{"line":312,"column":null}},"type":"if","locations":[{"start":{"line":311,"column":4},"end":{"line":312,"column":null}},{"start":{"line":312,"column":9},"end":{"line":312,"column":null}}],"line":311},"19":{"loc":{"start":{"line":358,"column":12},"end":{"line":366,"column":null}},"type":"cond-expr","locations":[{"start":{"line":358,"column":25},"end":{"line":366,"column":10}},{"start":{"line":366,"column":10},"end":{"line":366,"column":null}}],"line":358}},"s":{"0":1,"1":12,"2":12,"3":12,"4":1,"5":1,"6":1,"7":8,"8":8,"9":8,"10":8,"11":7,"12":1,"13":1,"14":6,"15":6,"16":6,"17":6,"18":6,"19":5,"20":6,"21":1,"22":1,"23":1,"24":3,"25":3,"26":3,"27":3,"28":2,"29":2,"30":2,"31":2,"32":2,"33":1,"34":2,"35":1,"36":1,"37":1,"38":2,"39":2,"40":2,"41":2,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"51":3,"52":3,"53":3,"54":3,"55":3,"56":3,"57":3,"58":3,"59":3,"60":1,"61":3,"62":3,"63":10,"64":10,"65":10,"66":9,"67":10,"68":10,"69":3,"70":1,"71":2,"72":2,"73":2,"74":7,"75":1,"76":6,"77":7,"78":2,"79":4,"80":1,"81":3,"82":4,"83":4,"84":3,"85":3,"86":1,"87":4,"88":4,"89":3,"90":3,"91":2,"92":1,"93":2,"94":2,"95":1,"96":1,"97":2,"98":2,"99":2,"100":2,"101":2,"102":2,"103":2,"104":2,"105":2,"106":2,"107":1,"108":1,"109":2,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"116":1,"117":1,"118":1,"119":1,"120":2,"121":2,"122":2,"123":2,"124":1,"125":1,"126":1,"127":1,"128":1,"129":1,"130":1,"131":140},"f":{"0":12,"1":1,"2":8,"3":3,"4":2,"5":3,"6":1,"7":1,"8":1,"9":10,"10":4,"11":3,"12":4,"13":2,"14":2,"15":2,"16":2,"17":2,"18":2,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":2,"30":140},"b":{"0":[1,6],"1":[7,1],"2":[5,1],"3":[1,1],"4":[1,2],"5":[9,1],"6":[1,2],"7":[1,6],"8":[4,2],"9":[6,6,6],"10":[2,4],"11":[1,3],"12":[4,2,2],"13":[2,1],"14":[2,1],"15":[2,1],"16":[1,1],"17":[1,1],"18":[1,1],"19":[50,90]},"meta":{"lastBranch":20,"lastFunction":31,"lastStatement":132,"seen":{"s:75:19:75:Infinity":0,"f:81:15:81:29":0,"s:82:2:82:Infinity":1,"s:83:2:87:Infinity":2,"s:84:4:84:Infinity":3,"s:86:4:86:Infinity":4,"s:90:13:369:Infinity":5,"f:91:2:91:Infinity":1,"s:92:17:348:Infinity":6,"f:110:9:110:16":2,"s:111:4:111:Infinity":7,"s:112:4:112:Infinity":8,"s:113:4:136:Infinity":9,"s:114:19:114:Infinity":10,"b:115:6:118:Infinity:undefined:undefined:undefined:undefined":0,"s:115:6:118:Infinity":11,"b:115:10:115:31:115:31:115:47":1,"s:116:8:116:Infinity":12,"s:117:8:117:Infinity":13,"s:119:6:124:Infinity":14,"s:125:6:125:Infinity":15,"s:126:6:126:Infinity":16,"s:127:6:127:Infinity":17,"b:128:6:130:Infinity:undefined:undefined:undefined:undefined":2,"s:128:6:130:Infinity":18,"s:129:8:129:Infinity":19,"s:131:6:131:Infinity":20,"s:133:12:133:Infinity":21,"s:134:6:134:Infinity":22,"s:135:6:135:Infinity":23,"f:139:20:139:27":3,"s:140:4:140:Infinity":24,"s:141:4:141:Infinity":25,"s:142:4:161:Infinity":26,"s:143:19:143:Infinity":27,"s:144:6:149:Infinity":28,"s:150:6:150:Infinity":29,"s:151:6:151:Infinity":30,"s:152:6:152:Infinity":31,"b:153:6:155:Infinity:undefined:undefined:undefined:undefined":3,"s:153:6:155:Infinity":32,"s:154:8:154:Infinity":33,"s:156:6:156:Infinity":34,"s:158:12:158:Infinity":35,"s:159:6:159:Infinity":36,"s:160:6:160:Infinity":37,"f:164:12:164:19":4,"s:165:4:165:Infinity":38,"s:166:4:166:Infinity":39,"s:167:4:184:Infinity":40,"s:168:19:168:Infinity":41,"s:169:6:174:Infinity":42,"s:175:6:175:Infinity":43,"s:176:6:176:Infinity":44,"s:177:6:177:Infinity":45,"s:178:6:178:Infinity":46,"s:179:6:179:Infinity":47,"s:181:12:181:Infinity":48,"s:182:6:182:Infinity":49,"s:183:6:183:Infinity":50,"f:187:10:187:22":5,"s:189:4:189:Infinity":51,"s:190:4:190:Infinity":52,"s:192:4:192:Infinity":53,"s:194:4:194:Infinity":54,"s:195:4:195:Infinity":55,"s:198:4:198:Infinity":56,"s:201:4:201:Infinity":57,"s:203:4:203:Infinity":58,"f:203:80:203:92":6,"b:205:4:210:Infinity:undefined:undefined:undefined:undefined":4,"s:205:4:210:Infinity":59,"s:206:6:209:Infinity":60,"f:207:34:207:46":7,"f:208:38:208:50":8,"s:212:4:212:Infinity":61,"s:214:4:219:Infinity":62,"f:222:12:222:19":9,"s:223:16:223:Infinity":63,"s:224:19:224:Infinity":64,"b:225:4:225:Infinity:undefined:undefined:undefined:undefined":5,"s:225:4:225:Infinity":65,"s:225:17:225:Infinity":66,"s:226:4:261:Infinity":67,"s:227:19:227:Infinity":68,"b:228:6:228:Infinity:undefined:undefined:undefined:undefined":6,"s:228:6:228:Infinity":69,"s:228:32:228:Infinity":70,"s:229:6:234:Infinity":71,"s:235:6:235:Infinity":72,"s:236:6:236:Infinity":73,"b:238:6:238:Infinity:undefined:undefined:undefined:undefined":7,"s:238:6:238:Infinity":74,"s:238:32:238:Infinity":75,"s:239:21:241:Infinity":76,"b:240:11:240:Infinity:241:10:241:Infinity":8,"b:239:21:239:28:239:28:239:55:239:55:239:Infinity":9,"b:242:6:260:Infinity:250:13:260:Infinity":10,"s:242:6:260:Infinity":77,"s:244:8:249:Infinity":78,"b:250:13:260:Infinity:254:13:260:Infinity":11,"s:250:13:260:Infinity":79,"b:250:17:250:41:250:41:250:77:250:77:250:96":12,"s:253:8:253:Infinity":80,"s:259:8:259:Infinity":81,"f:264:17:264:24":10,"s:265:4:273:Infinity":82,"s:266:6:266:Infinity":83,"s:267:6:270:Infinity":84,"f:267:6:267:11":11,"s:267:22:270:8":85,"b:268:27:268:74:268:74:268:Infinity":13,"b:268:58:268:65:268:65:268:70":14,"s:272:6:272:Infinity":86,"f:276:17:276:24":12,"s:277:4:285:Infinity":87,"s:278:19:278:Infinity":88,"s:279:6:279:Infinity":89,"b:280:6:282:Infinity:undefined:undefined:undefined:undefined":15,"s:280:6:282:Infinity":90,"s:281:8:281:Infinity":91,"s:284:6:284:Infinity":92,"f:288:17:288:24":13,"s:289:4:294:Infinity":93,"s:290:19:290:Infinity":94,"s:291:6:291:Infinity":95,"s:293:6:293:Infinity":96,"f:297:16:297:23":14,"s:298:21:298:Infinity":97,"s:299:4:299:Infinity":98,"s:300:17:300:Infinity":99,"s:301:4:301:Infinity":100,"f:301:4:301:9":15,"s:301:20:301:97":101,"b:301:41:301:90:301:90:301:95":16,"s:302:4:302:Infinity":102,"f:305:16:305:28":16,"s:306:4:306:Infinity":103,"s:307:4:307:Infinity":104,"f:307:4:307:9":17,"s:307:20:307:86":105,"b:307:41:307:79:307:79:307:84":17,"f:310:2:310:16":18,"b:311:4:312:Infinity:312:9:312:Infinity":18,"s:311:4:312:Infinity":106,"s:311:13:311:Infinity":107,"s:312:9:312:Infinity":108,"s:313:4:313:Infinity":109,"f:316:2:316:15":19,"s:316:32:316:Infinity":110,"f:317:2:317:20":20,"s:317:37:317:Infinity":111,"f:318:2:318:18":21,"s:318:34:318:Infinity":112,"f:319:2:319:18":22,"s:319:35:319:Infinity":113,"f:320:2:320:22":23,"s:320:37:320:Infinity":114,"f:321:2:321:21":24,"s:321:38:321:Infinity":115,"f:322:2:322:28":25,"s:322:45:322:Infinity":116,"f:323:2:323:27":26,"s:323:44:323:Infinity":117,"f:324:2:324:33":27,"s:324:50:324:Infinity":118,"f:325:2:325:28":28,"s:325:45:325:Infinity":119,"f:327:13:327:25":29,"s:328:4:328:Infinity":120,"s:329:4:329:Infinity":121,"s:330:4:346:Infinity":122,"s:331:19:331:Infinity":123,"s:332:6:338:Infinity":124,"s:339:6:339:Infinity":125,"s:340:6:340:Infinity":126,"s:341:6:341:Infinity":127,"s:343:12:343:Infinity":128,"s:344:6:344:Infinity":129,"s:345:6:345:Infinity":130,"f:356:4:356:17":30,"s:356:28:367:Infinity":131,"b:358:25:366:10:366:10:366:Infinity":19},"fnNames":{}}} -} diff --git a/client/coverage-jsheet/coverage-final.json b/client/coverage-jsheet/coverage-final.json deleted file mode 100644 index c34c70058..000000000 --- a/client/coverage-jsheet/coverage-final.json +++ /dev/null @@ -1,2 +0,0 @@ -{"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\mobile\\screens\\journey\\MJourneyEntrySheet.tsx": {"path":"C:\\Users\\Hospi\\.local\\bin\\trek-coverage\\client\\src\\mobile\\screens\\journey\\MJourneyEntrySheet.tsx","statementMap":{"0":{"start":{"line":16,"column":18},"end":{"line":16,"column":null}},"1":{"start":{"line":17,"column":18},"end":{"line":17,"column":null}},"2":{"start":{"line":46,"column":10},"end":{"line":46,"column":null}},"3":{"start":{"line":47,"column":8},"end":{"line":47,"column":null}},"4":{"start":{"line":49,"column":16},"end":{"line":49,"column":null}},"5":{"start":{"line":50,"column":16},"end":{"line":50,"column":null}},"6":{"start":{"line":51,"column":20},"end":{"line":51,"column":null}},"7":{"start":{"line":52,"column":20},"end":{"line":52,"column":null}},"8":{"start":{"line":53,"column":23},"end":{"line":53,"column":null}},"9":{"start":{"line":54,"column":22},"end":{"line":54,"column":null}},"10":{"start":{"line":55,"column":22},"end":{"line":55,"column":null}},"11":{"start":{"line":56,"column":24},"end":{"line":56,"column":null}},"12":{"start":{"line":57,"column":26},"end":{"line":57,"column":null}},"13":{"start":{"line":58,"column":30},"end":{"line":58,"column":null}},"14":{"start":{"line":59,"column":8},"end":{"line":59,"column":null}},"15":{"start":{"line":60,"column":15},"end":{"line":60,"column":null}},"16":{"start":{"line":61,"column":18},"end":{"line":61,"column":null}},"17":{"start":{"line":62,"column":15},"end":{"line":62,"column":null}},"18":{"start":{"line":63,"column":15},"end":{"line":63,"column":null}},"19":{"start":{"line":64,"column":15},"end":{"line":64,"column":null}},"20":{"start":{"line":65,"column":19},"end":{"line":65,"column":null}},"21":{"start":{"line":66,"column":17},"end":{"line":66,"column":null}},"22":{"start":{"line":67,"column":23},"end":{"line":67,"column":null}},"23":{"start":{"line":68,"column":25},"end":{"line":68,"column":null}},"24":{"start":{"line":69,"column":26},"end":{"line":69,"column":null}},"25":{"start":{"line":70,"column":17},"end":{"line":70,"column":null}},"26":{"start":{"line":71,"column":22},"end":{"line":71,"column":null}},"27":{"start":{"line":72,"column":19},"end":{"line":72,"column":null}},"28":{"start":{"line":73,"column":24},"end":{"line":73,"column":null}},"29":{"start":{"line":74,"column":25},"end":{"line":74,"column":null}},"30":{"start":{"line":75,"column":8},"end":{"line":75,"column":null}},"31":{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},"32":{"start":{"line":78,"column":2},"end":{"line":113,"column":null}},"33":{"start":{"line":79,"column":4},"end":{"line":79,"column":null}},"34":{"start":{"line":79,"column":95},"end":{"line":79,"column":null}},"35":{"start":{"line":80,"column":4},"end":{"line":83,"column":null}},"36":{"start":{"line":81,"column":6},"end":{"line":81,"column":null}},"37":{"start":{"line":82,"column":6},"end":{"line":82,"column":null}},"38":{"start":{"line":85,"column":17},"end":{"line":85,"column":null}},"39":{"start":{"line":86,"column":4},"end":{"line":86,"column":null}},"40":{"start":{"line":87,"column":4},"end":{"line":110,"column":null}},"41":{"start":{"line":88,"column":18},"end":{"line":88,"column":null}},"42":{"start":{"line":89,"column":18},"end":{"line":89,"column":null}},"43":{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},"44":{"start":{"line":90,"column":19},"end":{"line":90,"column":null}},"45":{"start":{"line":91,"column":6},"end":{"line":91,"column":null}},"46":{"start":{"line":92,"column":6},"end":{"line":92,"column":null}},"47":{"start":{"line":94,"column":43},"end":{"line":97,"column":null}},"48":{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},"49":{"start":{"line":98,"column":19},"end":{"line":98,"column":null}},"50":{"start":{"line":99,"column":6},"end":{"line":101,"column":null}},"51":{"start":{"line":100,"column":8},"end":{"line":100,"column":null}},"52":{"start":{"line":102,"column":6},"end":{"line":104,"column":null}},"53":{"start":{"line":103,"column":8},"end":{"line":103,"column":null}},"54":{"start":{"line":103,"column":30},"end":{"line":103,"column":122}},"55":{"start":{"line":105,"column":6},"end":{"line":105,"column":null}},"56":{"start":{"line":107,"column":6},"end":{"line":107,"column":null}},"57":{"start":{"line":107,"column":19},"end":{"line":107,"column":null}},"58":{"start":{"line":108,"column":6},"end":{"line":108,"column":null}},"59":{"start":{"line":109,"column":6},"end":{"line":109,"column":null}},"60":{"start":{"line":112,"column":4},"end":{"line":112,"column":null}},"61":{"start":{"line":112,"column":19},"end":{"line":112,"column":34}},"62":{"start":{"line":116,"column":4},"end":{"line":127,"column":null}},"63":{"start":{"line":129,"column":8},"end":{"line":132,"column":null}},"64":{"start":{"line":130,"column":4},"end":{"line":130,"column":null}},"65":{"start":{"line":130,"column":108},"end":{"line":130,"column":null}},"66":{"start":{"line":131,"column":4},"end":{"line":131,"column":null}},"67":{"start":{"line":134,"column":21},"end":{"line":179,"column":null}},"68":{"start":{"line":135,"column":4},"end":{"line":135,"column":null}},"69":{"start":{"line":136,"column":4},"end":{"line":178,"column":null}},"70":{"start":{"line":137,"column":22},"end":{"line":152,"column":null}},"71":{"start":{"line":147,"column":33},"end":{"line":147,"column":43}},"72":{"start":{"line":148,"column":44},"end":{"line":148,"column":52}},"73":{"start":{"line":148,"column":78},"end":{"line":148,"column":86}},"74":{"start":{"line":153,"column":6},"end":{"line":169,"column":null}},"75":{"start":{"line":154,"column":25},"end":{"line":154,"column":null}},"76":{"start":{"line":155,"column":8},"end":{"line":155,"column":null}},"77":{"start":{"line":156,"column":8},"end":{"line":168,"column":null}},"78":{"start":{"line":157,"column":29},"end":{"line":159,"column":null}},"79":{"start":{"line":158,"column":29},"end":{"line":158,"column":null}},"80":{"start":{"line":160,"column":10},"end":{"line":160,"column":null}},"81":{"start":{"line":161,"column":10},"end":{"line":163,"column":null}},"82":{"start":{"line":162,"column":12},"end":{"line":162,"column":null}},"83":{"start":{"line":165,"column":10},"end":{"line":165,"column":null}},"84":{"start":{"line":167,"column":10},"end":{"line":167,"column":null}},"85":{"start":{"line":170,"column":6},"end":{"line":174,"column":null}},"86":{"start":{"line":171,"column":8},"end":{"line":173,"column":null}},"87":{"start":{"line":172,"column":10},"end":{"line":172,"column":null}},"88":{"start":{"line":172,"column":16},"end":{"line":172,"column":61}},"89":{"start":{"line":175,"column":6},"end":{"line":175,"column":null}},"90":{"start":{"line":177,"column":6},"end":{"line":177,"column":null}},"91":{"start":{"line":181,"column":27},"end":{"line":186,"column":null}},"92":{"start":{"line":182,"column":18},"end":{"line":182,"column":null}},"93":{"start":{"line":183,"column":4},"end":{"line":183,"column":null}},"94":{"start":{"line":183,"column":24},"end":{"line":183,"column":null}},"95":{"start":{"line":184,"column":23},"end":{"line":184,"column":null}},"96":{"start":{"line":185,"column":4},"end":{"line":185,"column":null}},"97":{"start":{"line":185,"column":28},"end":{"line":185,"column":52}},"98":{"start":{"line":188,"column":8},"end":{"line":206,"column":null}},"99":{"start":{"line":189,"column":4},"end":{"line":189,"column":null}},"100":{"start":{"line":190,"column":4},"end":{"line":190,"column":null}},"101":{"start":{"line":191,"column":4},"end":{"line":191,"column":null}},"102":{"start":{"line":191,"column":34},"end":{"line":191,"column":null}},"103":{"start":{"line":192,"column":4},"end":{"line":195,"column":null}},"104":{"start":{"line":193,"column":6},"end":{"line":193,"column":null}},"105":{"start":{"line":194,"column":6},"end":{"line":194,"column":null}},"106":{"start":{"line":196,"column":4},"end":{"line":205,"column":null}},"107":{"start":{"line":197,"column":6},"end":{"line":204,"column":null}},"108":{"start":{"line":198,"column":20},"end":{"line":198,"column":null}},"109":{"start":{"line":199,"column":8},"end":{"line":201,"column":null}},"110":{"start":{"line":199,"column":150},"end":{"line":201,"column":10}},"111":{"start":{"line":203,"column":8},"end":{"line":203,"column":null}},"112":{"start":{"line":208,"column":8},"end":{"line":213,"column":null}},"113":{"start":{"line":209,"column":18},"end":{"line":209,"column":null}},"114":{"start":{"line":210,"column":4},"end":{"line":210,"column":null}},"115":{"start":{"line":210,"column":16},"end":{"line":210,"column":null}},"116":{"start":{"line":211,"column":4},"end":{"line":211,"column":null}},"117":{"start":{"line":211,"column":31},"end":{"line":211,"column":null}},"118":{"start":{"line":211,"column":47},"end":{"line":211,"column":63}},"119":{"start":{"line":212,"column":4},"end":{"line":212,"column":null}},"120":{"start":{"line":215,"column":33},"end":{"line":215,"column":null}},"121":{"start":{"line":215,"column":60},"end":{"line":215,"column":93}},"122":{"start":{"line":215,"column":78},"end":{"line":215,"column":92}},"123":{"start":{"line":217,"column":18},"end":{"line":217,"column":null}},"124":{"start":{"line":218,"column":21},"end":{"line":218,"column":null}},"125":{"start":{"line":220,"column":2},"end":{"line":665,"column":null}},"126":{"start":{"line":243,"column":27},"end":{"line":243,"column":null}},"127":{"start":{"line":251,"column":149},"end":{"line":251,"column":191}},"128":{"start":{"line":252,"column":134},"end":{"line":252,"column":176}},"129":{"start":{"line":256,"column":16},"end":{"line":256,"column":null}},"130":{"start":{"line":274,"column":31},"end":{"line":274,"column":null}},"131":{"start":{"line":274,"column":96},"end":{"line":274,"column":98}},"132":{"start":{"line":291,"column":20},"end":{"line":309,"column":null}},"133":{"start":{"line":297,"column":24},"end":{"line":305,"column":null}},"134":{"start":{"line":298,"column":26},"end":{"line":301,"column":null}},"135":{"start":{"line":299,"column":43},"end":{"line":299,"column":null}},"136":{"start":{"line":300,"column":28},"end":{"line":300,"column":null}},"137":{"start":{"line":300,"column":40},"end":{"line":300,"column":null}},"138":{"start":{"line":300,"column":58},"end":{"line":300,"column":75}},"139":{"start":{"line":303,"column":26},"end":{"line":303,"column":null}},"140":{"start":{"line":303,"column":52},"end":{"line":303,"column":68}},"141":{"start":{"line":304,"column":26},"end":{"line":304,"column":null}},"142":{"start":{"line":304,"column":44},"end":{"line":304,"column":57}},"143":{"start":{"line":325,"column":14},"end":{"line":361,"column":null}},"144":{"start":{"line":331,"column":22},"end":{"line":337,"column":null}},"145":{"start":{"line":332,"column":37},"end":{"line":332,"column":null}},"146":{"start":{"line":333,"column":40},"end":{"line":333,"column":null}},"147":{"start":{"line":334,"column":24},"end":{"line":334,"column":null}},"148":{"start":{"line":335,"column":24},"end":{"line":335,"column":null}},"149":{"start":{"line":335,"column":50},"end":{"line":335,"column":115}},"150":{"start":{"line":336,"column":24},"end":{"line":336,"column":null}},"151":{"start":{"line":348,"column":22},"end":{"line":348,"column":null}},"152":{"start":{"line":348,"column":40},"end":{"line":348,"column":71}},"153":{"start":{"line":348,"column":57},"end":{"line":348,"column":70}},"154":{"start":{"line":349,"column":22},"end":{"line":353,"column":null}},"155":{"start":{"line":350,"column":24},"end":{"line":350,"column":null}},"156":{"start":{"line":350,"column":30},"end":{"line":350,"column":75}},"157":{"start":{"line":352,"column":24},"end":{"line":352,"column":null}},"158":{"start":{"line":352,"column":50},"end":{"line":352,"column":80}},"159":{"start":{"line":352,"column":68},"end":{"line":352,"column":79}},"160":{"start":{"line":364,"column":14},"end":{"line":374,"column":null}},"161":{"start":{"line":368,"column":33},"end":{"line":368,"column":null}},"162":{"start":{"line":368,"column":57},"end":{"line":368,"column":87}},"163":{"start":{"line":368,"column":79},"end":{"line":368,"column":86}},"164":{"start":{"line":390,"column":29},"end":{"line":390,"column":null}},"165":{"start":{"line":407,"column":18},"end":{"line":421,"column":null}},"166":{"start":{"line":412,"column":52},"end":{"line":412,"column":63}},"167":{"start":{"line":412,"column":63},"end":{"line":412,"column":89}},"168":{"start":{"line":412,"column":89},"end":{"line":412,"column":103}},"169":{"start":{"line":417,"column":59},"end":{"line":417,"column":100}},"170":{"start":{"line":417,"column":89},"end":{"line":417,"column":96}},"171":{"start":{"line":426,"column":35},"end":{"line":426,"column":null}},"172":{"start":{"line":440,"column":18},"end":{"line":454,"column":null}},"173":{"start":{"line":445,"column":52},"end":{"line":445,"column":63}},"174":{"start":{"line":445,"column":63},"end":{"line":445,"column":89}},"175":{"start":{"line":445,"column":89},"end":{"line":445,"column":103}},"176":{"start":{"line":450,"column":59},"end":{"line":450,"column":100}},"177":{"start":{"line":450,"column":89},"end":{"line":450,"column":96}},"178":{"start":{"line":459,"column":35},"end":{"line":459,"column":null}},"179":{"start":{"line":481,"column":31},"end":{"line":481,"column":null}},"180":{"start":{"line":493,"column":31},"end":{"line":493,"column":null}},"181":{"start":{"line":507,"column":29},"end":{"line":507,"column":null}},"182":{"start":{"line":508,"column":31},"end":{"line":508,"column":92}},"183":{"start":{"line":508,"column":63},"end":{"line":508,"column":92}},"184":{"start":{"line":517,"column":16},"end":{"line":535,"column":null}},"185":{"start":{"line":521,"column":20},"end":{"line":521,"column":null}},"186":{"start":{"line":522,"column":20},"end":{"line":522,"column":null}},"187":{"start":{"line":523,"column":20},"end":{"line":523,"column":null}},"188":{"start":{"line":524,"column":20},"end":{"line":524,"column":null}},"189":{"start":{"line":525,"column":20},"end":{"line":525,"column":null}},"190":{"start":{"line":526,"column":20},"end":{"line":526,"column":null}},"191":{"start":{"line":548,"column":29},"end":{"line":548,"column":null}},"192":{"start":{"line":549,"column":14},"end":{"line":562,"column":null}},"193":{"start":{"line":554,"column":33},"end":{"line":554,"column":null}},"194":{"start":{"line":572,"column":27},"end":{"line":572,"column":null}},"195":{"start":{"line":573,"column":12},"end":{"line":587,"column":null}},"196":{"start":{"line":578,"column":31},"end":{"line":578,"column":null}},"197":{"start":{"line":598,"column":16},"end":{"line":605,"column":null}},"198":{"start":{"line":601,"column":57},"end":{"line":601,"column":103}},"199":{"start":{"line":601,"column":73},"end":{"line":601,"column":100}},"200":{"start":{"line":601,"column":90},"end":{"line":601,"column":99}},"201":{"start":{"line":610,"column":33},"end":{"line":610,"column":null}},"202":{"start":{"line":612,"column":20},"end":{"line":615,"column":null}},"203":{"start":{"line":613,"column":22},"end":{"line":613,"column":null}},"204":{"start":{"line":614,"column":22},"end":{"line":614,"column":null}},"205":{"start":{"line":641,"column":27},"end":{"line":641,"column":null}}},"fnMap":{"0":{"name":"MJourneyEntrySheet","decl":{"start":{"line":43,"column":24},"end":{"line":43,"column":43}},"loc":{"start":{"line":45,"column":28},"end":{"line":667,"column":null}},"line":45},"1":{"name":"(anonymous_1)","decl":{"start":{"line":78,"column":2},"end":{"line":78,"column":18}},"loc":{"start":{"line":78,"column":18},"end":{"line":113,"column":5}},"line":78},"2":{"name":"(anonymous_2)","decl":{"start":{"line":87,"column":45},"end":{"line":87,"column":51}},"loc":{"start":{"line":87,"column":63},"end":{"line":106,"column":7}},"line":87},"3":{"name":"(anonymous_3)","decl":{"start":{"line":103,"column":8},"end":{"line":103,"column":19}},"loc":{"start":{"line":103,"column":30},"end":{"line":103,"column":122}},"line":103},"4":{"name":"(anonymous_4)","decl":{"start":{"line":106,"column":4},"end":{"line":106,"column":7}},"loc":{"start":{"line":106,"column":16},"end":{"line":110,"column":7}},"line":106},"5":{"name":"(anonymous_5)","decl":{"start":{"line":112,"column":4},"end":{"line":112,"column":17}},"loc":{"start":{"line":112,"column":17},"end":{"line":112,"column":null}},"line":112},"6":{"name":"(anonymous_6)","decl":{"start":{"line":129,"column":8},"end":{"line":129,"column":28}},"loc":{"start":{"line":129,"column":28},"end":{"line":132,"column":null}},"line":129},"7":{"name":"(anonymous_7)","decl":{"start":{"line":134,"column":21},"end":{"line":134,"column":33}},"loc":{"start":{"line":134,"column":33},"end":{"line":179,"column":null}},"line":134},"8":{"name":"(anonymous_8)","decl":{"start":{"line":147,"column":19},"end":{"line":147,"column":26}},"loc":{"start":{"line":147,"column":33},"end":{"line":147,"column":43}},"line":147},"9":{"name":"(anonymous_9)","decl":{"start":{"line":148,"column":32},"end":{"line":148,"column":39}},"loc":{"start":{"line":148,"column":44},"end":{"line":148,"column":52}},"line":148},"10":{"name":"(anonymous_10)","decl":{"start":{"line":148,"column":66},"end":{"line":148,"column":73}},"loc":{"start":{"line":148,"column":78},"end":{"line":148,"column":86}},"line":148},"11":{"name":"(anonymous_11)","decl":{"start":{"line":158,"column":12},"end":{"line":158,"column":24}},"loc":{"start":{"line":158,"column":29},"end":{"line":158,"column":null}},"line":158},"12":{"name":"(anonymous_12)","decl":{"start":{"line":181,"column":27},"end":{"line":181,"column":34}},"loc":{"start":{"line":181,"column":77},"end":{"line":186,"column":null}},"line":181},"13":{"name":"(anonymous_13)","decl":{"start":{"line":185,"column":4},"end":{"line":185,"column":20}},"loc":{"start":{"line":185,"column":28},"end":{"line":185,"column":52}},"line":185},"14":{"name":"(anonymous_14)","decl":{"start":{"line":188,"column":8},"end":{"line":188,"column":26}},"loc":{"start":{"line":188,"column":44},"end":{"line":206,"column":null}},"line":188},"15":{"name":"(anonymous_15)","decl":{"start":{"line":196,"column":42},"end":{"line":196,"column":54}},"loc":{"start":{"line":196,"column":54},"end":{"line":205,"column":7}},"line":196},"16":{"name":"(anonymous_16)","decl":{"start":{"line":199,"column":58},"end":{"line":199,"column":63}},"loc":{"start":{"line":199,"column":150},"end":{"line":201,"column":10}},"line":199},"17":{"name":"(anonymous_17)","decl":{"start":{"line":208,"column":8},"end":{"line":208,"column":23}},"loc":{"start":{"line":208,"column":23},"end":{"line":213,"column":null}},"line":208},"18":{"name":"(anonymous_18)","decl":{"start":{"line":211,"column":31},"end":{"line":211,"column":39}},"loc":{"start":{"line":211,"column":47},"end":{"line":211,"column":63}},"line":211},"19":{"name":"(anonymous_19)","decl":{"start":{"line":215,"column":47},"end":{"line":215,"column":54}},"loc":{"start":{"line":215,"column":60},"end":{"line":215,"column":93}},"line":215},"20":{"name":"(anonymous_20)","decl":{"start":{"line":215,"column":68},"end":{"line":215,"column":73}},"loc":{"start":{"line":215,"column":78},"end":{"line":215,"column":92}},"line":215},"21":{"name":"(anonymous_21)","decl":{"start":{"line":243,"column":12},"end":{"line":243,"column":22}},"loc":{"start":{"line":243,"column":27},"end":{"line":243,"column":null}},"line":243},"22":{"name":"(anonymous_22)","decl":{"start":{"line":251,"column":133},"end":{"line":251,"column":142}},"loc":{"start":{"line":251,"column":147},"end":{"line":251,"column":195}},"line":251},"23":{"name":"(anonymous_23)","decl":{"start":{"line":252,"column":118},"end":{"line":252,"column":127}},"loc":{"start":{"line":252,"column":132},"end":{"line":252,"column":180}},"line":252},"24":{"name":"(anonymous_24)","decl":{"start":{"line":256,"column":16},"end":{"line":256,"column":32}},"loc":{"start":{"line":256,"column":16},"end":{"line":256,"column":null}},"line":256},"25":{"name":"(anonymous_25)","decl":{"start":{"line":274,"column":16},"end":{"line":274,"column":31}},"loc":{"start":{"line":274,"column":31},"end":{"line":274,"column":null}},"line":274},"26":{"name":"(anonymous_26)","decl":{"start":{"line":274,"column":72},"end":{"line":274,"column":91}},"loc":{"start":{"line":274,"column":96},"end":{"line":274,"column":98}},"line":274},"27":{"name":"(anonymous_27)","decl":{"start":{"line":290,"column":42},"end":{"line":290,"column":46}},"loc":{"start":{"line":291,"column":20},"end":{"line":309,"column":null}},"line":291},"28":{"name":"(anonymous_28)","decl":{"start":{"line":296,"column":31},"end":{"line":296,"column":43}},"loc":{"start":{"line":296,"column":43},"end":{"line":306,"column":null}},"line":296},"29":{"name":"(anonymous_29)","decl":{"start":{"line":300,"column":40},"end":{"line":300,"column":50}},"loc":{"start":{"line":300,"column":58},"end":{"line":300,"column":75}},"line":300},"30":{"name":"(anonymous_30)","decl":{"start":{"line":303,"column":26},"end":{"line":303,"column":44}},"loc":{"start":{"line":303,"column":52},"end":{"line":303,"column":68}},"line":303},"31":{"name":"(anonymous_31)","decl":{"start":{"line":304,"column":26},"end":{"line":304,"column":36}},"loc":{"start":{"line":304,"column":44},"end":{"line":304,"column":57}},"line":304},"32":{"name":"(anonymous_32)","decl":{"start":{"line":324,"column":20},"end":{"line":324,"column":25}},"loc":{"start":{"line":325,"column":14},"end":{"line":361,"column":null}},"line":325},"33":{"name":"(anonymous_33)","decl":{"start":{"line":330,"column":20},"end":{"line":330,"column":35}},"loc":{"start":{"line":330,"column":35},"end":{"line":338,"column":null}},"line":330},"34":{"name":"(anonymous_34)","decl":{"start":{"line":331,"column":22},"end":{"line":331,"column":32}},"loc":{"start":{"line":331,"column":40},"end":{"line":337,"column":23}},"line":331},"35":{"name":"(anonymous_35)","decl":{"start":{"line":335,"column":29},"end":{"line":335,"column":38}},"loc":{"start":{"line":335,"column":48},"end":{"line":335,"column":116}},"line":335},"36":{"name":"(anonymous_36)","decl":{"start":{"line":335,"column":99},"end":{"line":335,"column":111}},"loc":{"start":{"line":335,"column":111},"end":{"line":335,"column":113}},"line":335},"37":{"name":"(anonymous_37)","decl":{"start":{"line":347,"column":29},"end":{"line":347,"column":41}},"loc":{"start":{"line":347,"column":41},"end":{"line":354,"column":null}},"line":347},"38":{"name":"(anonymous_38)","decl":{"start":{"line":348,"column":22},"end":{"line":348,"column":32}},"loc":{"start":{"line":348,"column":40},"end":{"line":348,"column":71}},"line":348},"39":{"name":"(anonymous_39)","decl":{"start":{"line":348,"column":45},"end":{"line":348,"column":52}},"loc":{"start":{"line":348,"column":57},"end":{"line":348,"column":70}},"line":348},"40":{"name":"(anonymous_40)","decl":{"start":{"line":352,"column":24},"end":{"line":352,"column":42}},"loc":{"start":{"line":352,"column":50},"end":{"line":352,"column":80}},"line":352},"41":{"name":"(anonymous_41)","decl":{"start":{"line":352,"column":55},"end":{"line":352,"column":62}},"loc":{"start":{"line":352,"column":68},"end":{"line":352,"column":79}},"line":352},"42":{"name":"(anonymous_42)","decl":{"start":{"line":363,"column":26},"end":{"line":363,"column":31}},"loc":{"start":{"line":364,"column":14},"end":{"line":374,"column":null}},"line":364},"43":{"name":"(anonymous_43)","decl":{"start":{"line":368,"column":18},"end":{"line":368,"column":33}},"loc":{"start":{"line":368,"column":33},"end":{"line":368,"column":null}},"line":368},"44":{"name":"(anonymous_44)","decl":{"start":{"line":368,"column":33},"end":{"line":368,"column":49}},"loc":{"start":{"line":368,"column":57},"end":{"line":368,"column":87}},"line":368},"45":{"name":"(anonymous_45)","decl":{"start":{"line":368,"column":62},"end":{"line":368,"column":70}},"loc":{"start":{"line":368,"column":79},"end":{"line":368,"column":86}},"line":368},"46":{"name":"(anonymous_46)","decl":{"start":{"line":390,"column":14},"end":{"line":390,"column":24}},"loc":{"start":{"line":390,"column":29},"end":{"line":390,"column":null}},"line":390},"47":{"name":"(anonymous_47)","decl":{"start":{"line":406,"column":22},"end":{"line":406,"column":27}},"loc":{"start":{"line":407,"column":18},"end":{"line":421,"column":null}},"line":407},"48":{"name":"(anonymous_48)","decl":{"start":{"line":412,"column":22},"end":{"line":412,"column":32}},"loc":{"start":{"line":412,"column":37},"end":{"line":412,"column":null}},"line":412},"49":{"name":"(anonymous_49)","decl":{"start":{"line":417,"column":44},"end":{"line":417,"column":59}},"loc":{"start":{"line":417,"column":59},"end":{"line":417,"column":100}},"line":417},"50":{"name":"(anonymous_50)","decl":{"start":{"line":417,"column":72},"end":{"line":417,"column":80}},"loc":{"start":{"line":417,"column":89},"end":{"line":417,"column":96}},"line":417},"51":{"name":"(anonymous_51)","decl":{"start":{"line":426,"column":20},"end":{"line":426,"column":35}},"loc":{"start":{"line":426,"column":35},"end":{"line":426,"column":null}},"line":426},"52":{"name":"(anonymous_52)","decl":{"start":{"line":439,"column":22},"end":{"line":439,"column":27}},"loc":{"start":{"line":440,"column":18},"end":{"line":454,"column":null}},"line":440},"53":{"name":"(anonymous_53)","decl":{"start":{"line":445,"column":22},"end":{"line":445,"column":32}},"loc":{"start":{"line":445,"column":37},"end":{"line":445,"column":null}},"line":445},"54":{"name":"(anonymous_54)","decl":{"start":{"line":450,"column":44},"end":{"line":450,"column":59}},"loc":{"start":{"line":450,"column":59},"end":{"line":450,"column":100}},"line":450},"55":{"name":"(anonymous_55)","decl":{"start":{"line":450,"column":72},"end":{"line":450,"column":80}},"loc":{"start":{"line":450,"column":89},"end":{"line":450,"column":96}},"line":450},"56":{"name":"(anonymous_56)","decl":{"start":{"line":459,"column":20},"end":{"line":459,"column":35}},"loc":{"start":{"line":459,"column":35},"end":{"line":459,"column":null}},"line":459},"57":{"name":"(anonymous_57)","decl":{"start":{"line":481,"column":16},"end":{"line":481,"column":26}},"loc":{"start":{"line":481,"column":31},"end":{"line":481,"column":null}},"line":481},"58":{"name":"(anonymous_58)","decl":{"start":{"line":493,"column":16},"end":{"line":493,"column":26}},"loc":{"start":{"line":493,"column":31},"end":{"line":493,"column":null}},"line":493},"59":{"name":"(anonymous_59)","decl":{"start":{"line":507,"column":14},"end":{"line":507,"column":24}},"loc":{"start":{"line":507,"column":29},"end":{"line":507,"column":null}},"line":507},"60":{"name":"(anonymous_60)","decl":{"start":{"line":508,"column":14},"end":{"line":508,"column":29}},"loc":{"start":{"line":508,"column":29},"end":{"line":508,"column":null}},"line":508},"61":{"name":"(anonymous_61)","decl":{"start":{"line":516,"column":31},"end":{"line":516,"column":36}},"loc":{"start":{"line":517,"column":16},"end":{"line":535,"column":null}},"line":517},"62":{"name":"(anonymous_62)","decl":{"start":{"line":520,"column":18},"end":{"line":520,"column":33}},"loc":{"start":{"line":520,"column":33},"end":{"line":527,"column":null}},"line":520},"63":{"name":"(anonymous_63)","decl":{"start":{"line":547,"column":26},"end":{"line":547,"column":30}},"loc":{"start":{"line":547,"column":35},"end":{"line":564,"column":13}},"line":547},"64":{"name":"(anonymous_64)","decl":{"start":{"line":554,"column":18},"end":{"line":554,"column":33}},"loc":{"start":{"line":554,"column":33},"end":{"line":554,"column":null}},"line":554},"65":{"name":"(anonymous_65)","decl":{"start":{"line":571,"column":27},"end":{"line":571,"column":31}},"loc":{"start":{"line":571,"column":36},"end":{"line":589,"column":11}},"line":571},"66":{"name":"(anonymous_66)","decl":{"start":{"line":578,"column":16},"end":{"line":578,"column":31}},"loc":{"start":{"line":578,"column":31},"end":{"line":578,"column":null}},"line":578},"67":{"name":"(anonymous_67)","decl":{"start":{"line":597,"column":20},"end":{"line":597,"column":24}},"loc":{"start":{"line":598,"column":16},"end":{"line":605,"column":null}},"line":598},"68":{"name":"(anonymous_68)","decl":{"start":{"line":601,"column":42},"end":{"line":601,"column":57}},"loc":{"start":{"line":601,"column":57},"end":{"line":601,"column":103}},"line":601},"69":{"name":"(anonymous_69)","decl":{"start":{"line":601,"column":57},"end":{"line":601,"column":65}},"loc":{"start":{"line":601,"column":73},"end":{"line":601,"column":100}},"line":601},"70":{"name":"(anonymous_70)","decl":{"start":{"line":601,"column":78},"end":{"line":601,"column":85}},"loc":{"start":{"line":601,"column":90},"end":{"line":601,"column":99}},"line":601},"71":{"name":"(anonymous_71)","decl":{"start":{"line":610,"column":18},"end":{"line":610,"column":28}},"loc":{"start":{"line":610,"column":33},"end":{"line":610,"column":null}},"line":610},"72":{"name":"(anonymous_72)","decl":{"start":{"line":611,"column":18},"end":{"line":611,"column":29}},"loc":{"start":{"line":611,"column":34},"end":{"line":616,"column":null}},"line":611},"73":{"name":"(anonymous_73)","decl":{"start":{"line":641,"column":12},"end":{"line":641,"column":27}},"loc":{"start":{"line":641,"column":27},"end":{"line":641,"column":null}},"line":641}},"branchMap":{"0":{"loc":{"start":{"line":44,"column":24},"end":{"line":44,"column":46}},"type":"default-arg","locations":[{"start":{"line":44,"column":39},"end":{"line":44,"column":46}}],"line":44},"1":{"loc":{"start":{"line":44,"column":46},"end":{"line":44,"column":64}},"type":"default-arg","locations":[{"start":{"line":44,"column":57},"end":{"line":44,"column":64}}],"line":44},"2":{"loc":{"start":{"line":49,"column":37},"end":{"line":49,"column":54}},"type":"binary-expr","locations":[{"start":{"line":49,"column":37},"end":{"line":49,"column":52}},{"start":{"line":49,"column":52},"end":{"line":49,"column":54}}],"line":49},"3":{"loc":{"start":{"line":50,"column":37},"end":{"line":50,"column":54}},"type":"binary-expr","locations":[{"start":{"line":50,"column":37},"end":{"line":50,"column":52}},{"start":{"line":50,"column":52},"end":{"line":50,"column":54}}],"line":50},"4":{"loc":{"start":{"line":51,"column":45},"end":{"line":51,"column":103}},"type":"binary-expr","locations":[{"start":{"line":51,"column":45},"end":{"line":51,"column":65}},{"start":{"line":51,"column":65},"end":{"line":51,"column":103}}],"line":51},"5":{"loc":{"start":{"line":52,"column":45},"end":{"line":52,"column":80}},"type":"binary-expr","locations":[{"start":{"line":52,"column":45},"end":{"line":52,"column":78}},{"start":{"line":52,"column":78},"end":{"line":52,"column":80}}],"line":52},"6":{"loc":{"start":{"line":53,"column":51},"end":{"line":53,"column":76}},"type":"binary-expr","locations":[{"start":{"line":53,"column":51},"end":{"line":53,"column":74}},{"start":{"line":53,"column":74},"end":{"line":53,"column":76}}],"line":53},"7":{"loc":{"start":{"line":54,"column":64},"end":{"line":54,"column":90}},"type":"binary-expr","locations":[{"start":{"line":54,"column":64},"end":{"line":54,"column":86}},{"start":{"line":54,"column":86},"end":{"line":54,"column":90}}],"line":54},"8":{"loc":{"start":{"line":55,"column":64},"end":{"line":55,"column":90}},"type":"binary-expr","locations":[{"start":{"line":55,"column":64},"end":{"line":55,"column":86}},{"start":{"line":55,"column":86},"end":{"line":55,"column":90}}],"line":55},"9":{"loc":{"start":{"line":60,"column":35},"end":{"line":60,"column":51}},"type":"binary-expr","locations":[{"start":{"line":60,"column":35},"end":{"line":60,"column":49}},{"start":{"line":60,"column":49},"end":{"line":60,"column":51}}],"line":60},"10":{"loc":{"start":{"line":61,"column":41},"end":{"line":61,"column":60}},"type":"binary-expr","locations":[{"start":{"line":61,"column":41},"end":{"line":61,"column":58}},{"start":{"line":61,"column":58},"end":{"line":61,"column":60}}],"line":61},"11":{"loc":{"start":{"line":62,"column":45},"end":{"line":62,"column":72}},"type":"binary-expr","locations":[{"start":{"line":62,"column":45},"end":{"line":62,"column":70}},{"start":{"line":62,"column":70},"end":{"line":62,"column":72}}],"line":62},"12":{"loc":{"start":{"line":63,"column":45},"end":{"line":63,"column":72}},"type":"binary-expr","locations":[{"start":{"line":63,"column":45},"end":{"line":63,"column":70}},{"start":{"line":63,"column":70},"end":{"line":63,"column":72}}],"line":63},"13":{"loc":{"start":{"line":64,"column":45},"end":{"line":64,"column":61}},"type":"binary-expr","locations":[{"start":{"line":64,"column":45},"end":{"line":64,"column":59}},{"start":{"line":64,"column":59},"end":{"line":64,"column":61}}],"line":64},"14":{"loc":{"start":{"line":66,"column":72},"end":{"line":66,"column":90}},"type":"binary-expr","locations":[{"start":{"line":66,"column":72},"end":{"line":66,"column":88}},{"start":{"line":66,"column":88},"end":{"line":66,"column":90}}],"line":66},"15":{"loc":{"start":{"line":79,"column":4},"end":{"line":79,"column":null}},"type":"if","locations":[{"start":{"line":79,"column":4},"end":{"line":79,"column":null}},{"start":{},"end":{}}],"line":79},"16":{"loc":{"start":{"line":79,"column":8},"end":{"line":79,"column":95}},"type":"binary-expr","locations":[{"start":{"line":79,"column":8},"end":{"line":79,"column":25}},{"start":{"line":79,"column":25},"end":{"line":79,"column":37}},{"start":{"line":79,"column":37},"end":{"line":79,"column":67}},{"start":{"line":79,"column":67},"end":{"line":79,"column":95}}],"line":79},"17":{"loc":{"start":{"line":80,"column":4},"end":{"line":83,"column":null}},"type":"if","locations":[{"start":{"line":80,"column":4},"end":{"line":83,"column":null}},{"start":{},"end":{}}],"line":80},"18":{"loc":{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},"type":"if","locations":[{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},{"start":{},"end":{}}],"line":90},"19":{"loc":{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},"type":"if","locations":[{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},{"start":{},"end":{}}],"line":98},"20":{"loc":{"start":{"line":99,"column":6},"end":{"line":101,"column":null}},"type":"if","locations":[{"start":{"line":99,"column":6},"end":{"line":101,"column":null}},{"start":{},"end":{}}],"line":99},"21":{"loc":{"start":{"line":100,"column":24},"end":{"line":100,"column":81}},"type":"binary-expr","locations":[{"start":{"line":100,"column":24},"end":{"line":100,"column":50}},{"start":{"line":100,"column":50},"end":{"line":100,"column":79}},{"start":{"line":100,"column":79},"end":{"line":100,"column":81}}],"line":100},"22":{"loc":{"start":{"line":102,"column":6},"end":{"line":104,"column":null}},"type":"if","locations":[{"start":{"line":102,"column":6},"end":{"line":104,"column":null}},{"start":{},"end":{}}],"line":102},"23":{"loc":{"start":{"line":102,"column":10},"end":{"line":102,"column":78}},"type":"binary-expr","locations":[{"start":{"line":102,"column":10},"end":{"line":102,"column":50}},{"start":{"line":102,"column":50},"end":{"line":102,"column":78}}],"line":102},"24":{"loc":{"start":{"line":103,"column":30},"end":{"line":103,"column":122}},"type":"binary-expr","locations":[{"start":{"line":103,"column":30},"end":{"line":103,"column":41}},{"start":{"line":103,"column":30},"end":{"line":103,"column":122}}],"line":103},"25":{"loc":{"start":{"line":107,"column":6},"end":{"line":107,"column":null}},"type":"if","locations":[{"start":{"line":107,"column":6},"end":{"line":107,"column":null}},{"start":{},"end":{}}],"line":107},"26":{"loc":{"start":{"line":108,"column":23},"end":{"line":108,"column":57}},"type":"binary-expr","locations":[{"start":{"line":108,"column":23},"end":{"line":108,"column":40}},{"start":{"line":108,"column":40},"end":{"line":108,"column":57}}],"line":108},"27":{"loc":{"start":{"line":116,"column":4},"end":{"line":127,"column":null}},"type":"binary-expr","locations":[{"start":{"line":116,"column":4},"end":{"line":116,"column":null}},{"start":{"line":117,"column":4},"end":{"line":117,"column":null}},{"start":{"line":118,"column":4},"end":{"line":118,"column":null}},{"start":{"line":119,"column":4},"end":{"line":119,"column":null}},{"start":{"line":120,"column":4},"end":{"line":120,"column":null}},{"start":{"line":121,"column":4},"end":{"line":121,"column":null}},{"start":{"line":122,"column":4},"end":{"line":122,"column":null}},{"start":{"line":123,"column":4},"end":{"line":123,"column":null}},{"start":{"line":124,"column":4},"end":{"line":124,"column":null}},{"start":{"line":125,"column":4},"end":{"line":125,"column":null}},{"start":{"line":126,"column":4},"end":{"line":126,"column":null}},{"start":{"line":127,"column":4},"end":{"line":127,"column":null}}],"line":116},"28":{"loc":{"start":{"line":130,"column":4},"end":{"line":130,"column":null}},"type":"if","locations":[{"start":{"line":130,"column":4},"end":{"line":130,"column":null}},{"start":{},"end":{}}],"line":130},"29":{"loc":{"start":{"line":130,"column":8},"end":{"line":130,"column":108}},"type":"binary-expr","locations":[{"start":{"line":130,"column":8},"end":{"line":130,"column":24}},{"start":{"line":130,"column":24},"end":{"line":130,"column":37}},{"start":{"line":130,"column":37},"end":{"line":130,"column":48}},{"start":{"line":130,"column":48},"end":{"line":130,"column":108}}],"line":130},"30":{"loc":{"start":{"line":138,"column":15},"end":{"line":138,"column":null}},"type":"binary-expr","locations":[{"start":{"line":138,"column":15},"end":{"line":138,"column":24}},{"start":{"line":138,"column":24},"end":{"line":138,"column":null}}],"line":138},"31":{"loc":{"start":{"line":139,"column":15},"end":{"line":139,"column":null}},"type":"binary-expr","locations":[{"start":{"line":139,"column":15},"end":{"line":139,"column":24}},{"start":{"line":139,"column":24},"end":{"line":139,"column":null}}],"line":139},"32":{"loc":{"start":{"line":141,"column":20},"end":{"line":141,"column":null}},"type":"binary-expr","locations":[{"start":{"line":141,"column":20},"end":{"line":141,"column":33}},{"start":{"line":141,"column":33},"end":{"line":141,"column":null}}],"line":141},"33":{"loc":{"start":{"line":142,"column":23},"end":{"line":142,"column":null}},"type":"binary-expr","locations":[{"start":{"line":142,"column":23},"end":{"line":142,"column":39}},{"start":{"line":142,"column":39},"end":{"line":142,"column":null}}],"line":142},"34":{"loc":{"start":{"line":145,"column":14},"end":{"line":145,"column":null}},"type":"binary-expr","locations":[{"start":{"line":145,"column":14},"end":{"line":145,"column":22}},{"start":{"line":145,"column":22},"end":{"line":145,"column":null}}],"line":145},"35":{"loc":{"start":{"line":146,"column":17},"end":{"line":146,"column":null}},"type":"binary-expr","locations":[{"start":{"line":146,"column":17},"end":{"line":146,"column":28}},{"start":{"line":146,"column":28},"end":{"line":146,"column":null}}],"line":146},"36":{"loc":{"start":{"line":149,"column":14},"end":{"line":151,"column":null}},"type":"cond-expr","locations":[{"start":{"line":150,"column":12},"end":{"line":150,"column":null}},{"start":{"line":151,"column":12},"end":{"line":151,"column":null}}],"line":149},"37":{"loc":{"start":{"line":149,"column":14},"end":{"line":149,"column":null}},"type":"binary-expr","locations":[{"start":{"line":149,"column":14},"end":{"line":149,"column":44}},{"start":{"line":149,"column":44},"end":{"line":149,"column":60}},{"start":{"line":149,"column":60},"end":{"line":149,"column":87}},{"start":{"line":149,"column":87},"end":{"line":149,"column":null}}],"line":149},"38":{"loc":{"start":{"line":153,"column":6},"end":{"line":169,"column":null}},"type":"if","locations":[{"start":{"line":153,"column":6},"end":{"line":169,"column":null}},{"start":{},"end":{}}],"line":153},"39":{"loc":{"start":{"line":153,"column":10},"end":{"line":153,"column":46}},"type":"binary-expr","locations":[{"start":{"line":153,"column":10},"end":{"line":153,"column":37}},{"start":{"line":153,"column":37},"end":{"line":153,"column":46}}],"line":153},"40":{"loc":{"start":{"line":161,"column":10},"end":{"line":163,"column":null}},"type":"if","locations":[{"start":{"line":161,"column":10},"end":{"line":163,"column":null}},{"start":{},"end":{}}],"line":161},"41":{"loc":{"start":{"line":170,"column":6},"end":{"line":174,"column":null}},"type":"if","locations":[{"start":{"line":170,"column":6},"end":{"line":174,"column":null}},{"start":{},"end":{}}],"line":170},"42":{"loc":{"start":{"line":170,"column":10},"end":{"line":170,"column":48}},"type":"binary-expr","locations":[{"start":{"line":170,"column":10},"end":{"line":170,"column":39}},{"start":{"line":170,"column":39},"end":{"line":170,"column":48}}],"line":170},"43":{"loc":{"start":{"line":183,"column":4},"end":{"line":183,"column":null}},"type":"if","locations":[{"start":{"line":183,"column":4},"end":{"line":183,"column":null}},{"start":{},"end":{}}],"line":183},"44":{"loc":{"start":{"line":191,"column":4},"end":{"line":191,"column":null}},"type":"if","locations":[{"start":{"line":191,"column":4},"end":{"line":191,"column":null}},{"start":{},"end":{}}],"line":191},"45":{"loc":{"start":{"line":192,"column":4},"end":{"line":195,"column":null}},"type":"if","locations":[{"start":{"line":192,"column":4},"end":{"line":195,"column":null}},{"start":{},"end":{}}],"line":192},"46":{"loc":{"start":{"line":199,"column":28},"end":{"line":199,"column":46}},"type":"binary-expr","locations":[{"start":{"line":199,"column":28},"end":{"line":199,"column":42}},{"start":{"line":199,"column":42},"end":{"line":199,"column":46}}],"line":199},"47":{"loc":{"start":{"line":210,"column":4},"end":{"line":210,"column":null}},"type":"if","locations":[{"start":{"line":210,"column":4},"end":{"line":210,"column":null}},{"start":{},"end":{}}],"line":210},"48":{"loc":{"start":{"line":211,"column":4},"end":{"line":211,"column":null}},"type":"if","locations":[{"start":{"line":211,"column":4},"end":{"line":211,"column":null}},{"start":{},"end":{}}],"line":211},"49":{"loc":{"start":{"line":226,"column":17},"end":{"line":226,"column":null}},"type":"cond-expr","locations":[{"start":{"line":226,"column":34},"end":{"line":226,"column":65}},{"start":{"line":226,"column":65},"end":{"line":226,"column":null}}],"line":226},"50":{"loc":{"start":{"line":230,"column":11},"end":{"line":230,"column":null}},"type":"cond-expr","locations":[{"start":{"line":230,"column":28},"end":{"line":230,"column":59}},{"start":{"line":230,"column":59},"end":{"line":230,"column":null}}],"line":230},"51":{"loc":{"start":{"line":238,"column":9},"end":{"line":246,"column":null}},"type":"binary-expr","locations":[{"start":{"line":238,"column":9},"end":{"line":238,"column":26}},{"start":{"line":238,"column":26},"end":{"line":246,"column":null}}],"line":238},"52":{"loc":{"start":{"line":238,"column":26},"end":{"line":246,"column":null}},"type":"cond-expr","locations":[{"start":{"line":239,"column":10},"end":{"line":241,"column":11}},{"start":{"line":241,"column":10},"end":{"line":246,"column":null}}],"line":238},"53":{"loc":{"start":{"line":239,"column":73},"end":{"line":239,"column":124}},"type":"binary-expr","locations":[{"start":{"line":239,"column":73},"end":{"line":239,"column":82}},{"start":{"line":239,"column":82},"end":{"line":239,"column":124}}],"line":239},"54":{"loc":{"start":{"line":249,"column":9},"end":{"line":319,"column":null}},"type":"binary-expr","locations":[{"start":{"line":249,"column":9},"end":{"line":249,"column":null}},{"start":{"line":250,"column":10},"end":{"line":319,"column":null}}],"line":249},"55":{"loc":{"start":{"line":256,"column":32},"end":{"line":256,"column":67}},"type":"cond-expr","locations":[{"start":{"line":256,"column":46},"end":{"line":256,"column":58}},{"start":{"line":256,"column":58},"end":{"line":256,"column":67}}],"line":256},"56":{"loc":{"start":{"line":260,"column":17},"end":{"line":269,"column":null}},"type":"cond-expr","locations":[{"start":{"line":261,"column":18},"end":{"line":266,"column":null}},{"start":{"line":266,"column":18},"end":{"line":269,"column":null}}],"line":260},"57":{"loc":{"start":{"line":267,"column":21},"end":{"line":268,"column":35}},"type":"cond-expr","locations":[{"start":{"line":267,"column":35},"end":{"line":267,"column":77}},{"start":{"line":267,"column":76},"end":{"line":268,"column":35}}],"line":267},"58":{"loc":{"start":{"line":268,"column":21},"end":{"line":268,"column":null}},"type":"cond-expr","locations":[{"start":{"line":268,"column":35},"end":{"line":268,"column":60}},{"start":{"line":268,"column":60},"end":{"line":268,"column":null}}],"line":268},"59":{"loc":{"start":{"line":274,"column":31},"end":{"line":274,"column":null}},"type":"cond-expr","locations":[{"start":{"line":274,"column":45},"end":{"line":274,"column":72}},{"start":{"line":274,"column":72},"end":{"line":274,"column":null}}],"line":274},"60":{"loc":{"start":{"line":275,"column":26},"end":{"line":275,"column":null}},"type":"binary-expr","locations":[{"start":{"line":275,"column":26},"end":{"line":275,"column":42}},{"start":{"line":275,"column":42},"end":{"line":275,"column":null}}],"line":275},"61":{"loc":{"start":{"line":277,"column":18},"end":{"line":279,"column":null}},"type":"cond-expr","locations":[{"start":{"line":278,"column":22},"end":{"line":278,"column":null}},{"start":{"line":279,"column":22},"end":{"line":279,"column":null}}],"line":277},"62":{"loc":{"start":{"line":277,"column":18},"end":{"line":277,"column":null}},"type":"binary-expr","locations":[{"start":{"line":277,"column":18},"end":{"line":277,"column":34}},{"start":{"line":277,"column":34},"end":{"line":277,"column":null}}],"line":277},"63":{"loc":{"start":{"line":283,"column":17},"end":{"line":283,"column":null}},"type":"cond-expr","locations":[{"start":{"line":283,"column":31},"end":{"line":283,"column":60}},{"start":{"line":283,"column":60},"end":{"line":283,"column":null}}],"line":283},"64":{"loc":{"start":{"line":287,"column":13},"end":{"line":317,"column":null}},"type":"binary-expr","locations":[{"start":{"line":287,"column":13},"end":{"line":287,"column":29}},{"start":{"line":287,"column":29},"end":{"line":287,"column":null}},{"start":{"line":288,"column":14},"end":{"line":317,"column":null}}],"line":287},"65":{"loc":{"start":{"line":297,"column":24},"end":{"line":305,"column":null}},"type":"if","locations":[{"start":{"line":297,"column":24},"end":{"line":305,"column":null}},{"start":{"line":302,"column":31},"end":{"line":305,"column":null}}],"line":297},"66":{"loc":{"start":{"line":300,"column":28},"end":{"line":300,"column":null}},"type":"if","locations":[{"start":{"line":300,"column":28},"end":{"line":300,"column":null}},{"start":{},"end":{}}],"line":300},"67":{"loc":{"start":{"line":311,"column":19},"end":{"line":316,"column":null}},"type":"binary-expr","locations":[{"start":{"line":311,"column":19},"end":{"line":311,"column":null}},{"start":{"line":312,"column":20},"end":{"line":316,"column":null}}],"line":311},"68":{"loc":{"start":{"line":322,"column":10},"end":{"line":376,"column":null}},"type":"binary-expr","locations":[{"start":{"line":322,"column":10},"end":{"line":322,"column":31}},{"start":{"line":322,"column":31},"end":{"line":322,"column":null}},{"start":{"line":323,"column":10},"end":{"line":376,"column":null}}],"line":322},"69":{"loc":{"start":{"line":327,"column":17},"end":{"line":342,"column":null}},"type":"binary-expr","locations":[{"start":{"line":327,"column":17},"end":{"line":327,"column":30}},{"start":{"line":327,"column":30},"end":{"line":327,"column":41}},{"start":{"line":327,"column":41},"end":{"line":327,"column":null}},{"start":{"line":328,"column":18},"end":{"line":342,"column":null}}],"line":327},"70":{"loc":{"start":{"line":344,"column":17},"end":{"line":359,"column":null}},"type":"binary-expr","locations":[{"start":{"line":344,"column":17},"end":{"line":344,"column":null}},{"start":{"line":345,"column":18},"end":{"line":359,"column":null}}],"line":344},"71":{"loc":{"start":{"line":349,"column":22},"end":{"line":353,"column":null}},"type":"if","locations":[{"start":{"line":349,"column":22},"end":{"line":353,"column":null}},{"start":{"line":351,"column":29},"end":{"line":353,"column":null}}],"line":349},"72":{"loc":{"start":{"line":379,"column":9},"end":{"line":470,"column":null}},"type":"binary-expr","locations":[{"start":{"line":379,"column":9},"end":{"line":379,"column":25}},{"start":{"line":379,"column":25},"end":{"line":470,"column":null}}],"line":379},"73":{"loc":{"start":{"line":380,"column":11},"end":{"line":397,"column":13}},"type":"cond-expr","locations":[{"start":{"line":381,"column":12},"end":{"line":387,"column":13}},{"start":{"line":387,"column":12},"end":{"line":397,"column":13}}],"line":380},"74":{"loc":{"start":{"line":381,"column":12},"end":{"line":387,"column":13}},"type":"binary-expr","locations":[{"start":{"line":381,"column":12},"end":{"line":381,"column":null}},{"start":{"line":382,"column":14},"end":{"line":387,"column":13}}],"line":381},"75":{"loc":{"start":{"line":397,"column":12},"end":{"line":470,"column":null}},"type":"binary-expr","locations":[{"start":{"line":397,"column":12},"end":{"line":397,"column":25}},{"start":{"line":397,"column":25},"end":{"line":397,"column":44}},{"start":{"line":397,"column":44},"end":{"line":397,"column":null}},{"start":{"line":398,"column":10},"end":{"line":470,"column":null}}],"line":397},"76":{"loc":{"start":{"line":416,"column":21},"end":{"line":419,"column":null}},"type":"binary-expr","locations":[{"start":{"line":416,"column":21},"end":{"line":416,"column":null}},{"start":{"line":417,"column":22},"end":{"line":419,"column":null}}],"line":416},"77":{"loc":{"start":{"line":423,"column":17},"end":{"line":431,"column":null}},"type":"binary-expr","locations":[{"start":{"line":423,"column":17},"end":{"line":423,"column":null}},{"start":{"line":424,"column":18},"end":{"line":431,"column":null}}],"line":423},"78":{"loc":{"start":{"line":449,"column":21},"end":{"line":452,"column":null}},"type":"binary-expr","locations":[{"start":{"line":449,"column":21},"end":{"line":449,"column":null}},{"start":{"line":450,"column":22},"end":{"line":452,"column":null}}],"line":449},"79":{"loc":{"start":{"line":456,"column":17},"end":{"line":464,"column":null}},"type":"binary-expr","locations":[{"start":{"line":456,"column":17},"end":{"line":456,"column":null}},{"start":{"line":457,"column":18},"end":{"line":464,"column":null}}],"line":456},"80":{"loc":{"start":{"line":505,"column":21},"end":{"line":505,"column":null}},"type":"binary-expr","locations":[{"start":{"line":505,"column":21},"end":{"line":505,"column":38}},{"start":{"line":505,"column":38},"end":{"line":505,"column":null}}],"line":505},"81":{"loc":{"start":{"line":508,"column":31},"end":{"line":508,"column":92}},"type":"if","locations":[{"start":{"line":508,"column":31},"end":{"line":508,"column":92}},{"start":{},"end":{}}],"line":508},"82":{"loc":{"start":{"line":512,"column":13},"end":{"line":513,"column":null}},"type":"binary-expr","locations":[{"start":{"line":512,"column":13},"end":{"line":512,"column":36}},{"start":{"line":512,"column":36},"end":{"line":513,"column":null}}],"line":512},"83":{"loc":{"start":{"line":514,"column":11},"end":{"line":537,"column":null}},"type":"binary-expr","locations":[{"start":{"line":514,"column":11},"end":{"line":514,"column":34}},{"start":{"line":514,"column":34},"end":{"line":514,"column":null}},{"start":{"line":515,"column":12},"end":{"line":537,"column":null}}],"line":514},"84":{"loc":{"start":{"line":533,"column":21},"end":{"line":534,"column":null}},"type":"binary-expr","locations":[{"start":{"line":533,"column":21},"end":{"line":533,"column":34}},{"start":{"line":533,"column":34},"end":{"line":534,"column":null}}],"line":533},"85":{"loc":{"start":{"line":539,"column":11},"end":{"line":539,"column":null}},"type":"binary-expr","locations":[{"start":{"line":539,"column":11},"end":{"line":539,"column":23}},{"start":{"line":539,"column":23},"end":{"line":539,"column":null}}],"line":539},"86":{"loc":{"start":{"line":540,"column":11},"end":{"line":540,"column":null}},"type":"binary-expr","locations":[{"start":{"line":540,"column":11},"end":{"line":540,"column":28}},{"start":{"line":540,"column":28},"end":{"line":540,"column":null}}],"line":540},"87":{"loc":{"start":{"line":544,"column":9},"end":{"line":566,"column":null}},"type":"binary-expr","locations":[{"start":{"line":544,"column":9},"end":{"line":544,"column":25}},{"start":{"line":544,"column":25},"end":{"line":566,"column":null}}],"line":544},"88":{"loc":{"start":{"line":554,"column":41},"end":{"line":554,"column":59}},"type":"cond-expr","locations":[{"start":{"line":554,"column":50},"end":{"line":554,"column":55}},{"start":{"line":554,"column":55},"end":{"line":554,"column":59}}],"line":554},"89":{"loc":{"start":{"line":556,"column":20},"end":{"line":556,"column":null}},"type":"cond-expr","locations":[{"start":{"line":556,"column":29},"end":{"line":556,"column":34}},{"start":{"line":556,"column":34},"end":{"line":556,"column":null}}],"line":556},"90":{"loc":{"start":{"line":558,"column":25},"end":{"line":558,"column":null}},"type":"cond-expr","locations":[{"start":{"line":558,"column":34},"end":{"line":558,"column":112}},{"start":{"line":558,"column":112},"end":{"line":558,"column":null}}],"line":558},"91":{"loc":{"start":{"line":578,"column":42},"end":{"line":578,"column":60}},"type":"cond-expr","locations":[{"start":{"line":578,"column":51},"end":{"line":578,"column":56}},{"start":{"line":578,"column":56},"end":{"line":578,"column":60}}],"line":578},"92":{"loc":{"start":{"line":580,"column":18},"end":{"line":582,"column":null}},"type":"cond-expr","locations":[{"start":{"line":581,"column":22},"end":{"line":581,"column":null}},{"start":{"line":582,"column":22},"end":{"line":582,"column":null}}],"line":580},"93":{"loc":{"start":{"line":593,"column":9},"end":{"line":623,"column":null}},"type":"binary-expr","locations":[{"start":{"line":593,"column":9},"end":{"line":593,"column":26}},{"start":{"line":593,"column":26},"end":{"line":593,"column":39}},{"start":{"line":593,"column":39},"end":{"line":593,"column":null}},{"start":{"line":594,"column":10},"end":{"line":623,"column":null}}],"line":593},"94":{"loc":{"start":{"line":600,"column":19},"end":{"line":605,"column":null}},"type":"binary-expr","locations":[{"start":{"line":600,"column":19},"end":{"line":600,"column":null}},{"start":{"line":601,"column":20},"end":{"line":605,"column":null}}],"line":600},"95":{"loc":{"start":{"line":607,"column":15},"end":{"line":622,"column":null}},"type":"binary-expr","locations":[{"start":{"line":607,"column":15},"end":{"line":607,"column":null}},{"start":{"line":608,"column":16},"end":{"line":622,"column":null}}],"line":607},"96":{"loc":{"start":{"line":612,"column":20},"end":{"line":615,"column":null}},"type":"if","locations":[{"start":{"line":612,"column":20},"end":{"line":615,"column":null}},{"start":{},"end":{}}],"line":612},"97":{"loc":{"start":{"line":612,"column":24},"end":{"line":612,"column":60}},"type":"binary-expr","locations":[{"start":{"line":612,"column":24},"end":{"line":612,"column":45}},{"start":{"line":612,"column":45},"end":{"line":612,"column":60}}],"line":612},"98":{"loc":{"start":{"line":628,"column":9},"end":{"line":636,"column":null}},"type":"binary-expr","locations":[{"start":{"line":628,"column":9},"end":{"line":628,"column":22}},{"start":{"line":628,"column":22},"end":{"line":628,"column":null}},{"start":{"line":629,"column":10},"end":{"line":636,"column":null}}],"line":628},"99":{"loc":{"start":{"line":638,"column":9},"end":{"line":645,"column":null}},"type":"binary-expr","locations":[{"start":{"line":638,"column":9},"end":{"line":638,"column":22}},{"start":{"line":638,"column":22},"end":{"line":638,"column":null}},{"start":{"line":639,"column":10},"end":{"line":645,"column":null}}],"line":638},"100":{"loc":{"start":{"line":654,"column":9},"end":{"line":662,"column":null}},"type":"binary-expr","locations":[{"start":{"line":654,"column":9},"end":{"line":654,"column":null}},{"start":{"line":655,"column":10},"end":{"line":662,"column":null}}],"line":654},"101":{"loc":{"start":{"line":661,"column":13},"end":{"line":661,"column":null}},"type":"cond-expr","locations":[{"start":{"line":661,"column":22},"end":{"line":661,"column":43}},{"start":{"line":661,"column":43},"end":{"line":661,"column":null}}],"line":661}},"s":{"0":1,"1":1,"2":199,"3":199,"4":199,"5":199,"6":199,"7":199,"8":199,"9":199,"10":199,"11":199,"12":199,"13":199,"14":199,"15":199,"16":199,"17":199,"18":199,"19":199,"20":199,"21":199,"22":199,"23":199,"24":199,"25":199,"26":199,"27":199,"28":199,"29":199,"30":199,"31":199,"32":199,"33":43,"34":32,"35":11,"36":1,"37":1,"38":10,"39":10,"40":10,"41":9,"42":9,"43":9,"44":1,"45":8,"46":8,"47":8,"48":8,"49":1,"50":7,"51":6,"52":7,"53":2,"54":2,"55":7,"56":3,"57":1,"58":2,"59":3,"60":10,"61":10,"62":199,"63":199,"64":5,"65":1,"66":4,"67":199,"68":15,"69":15,"70":15,"71":3,"72":4,"73":3,"74":15,"75":3,"76":3,"77":3,"78":3,"79":1,"80":2,"81":2,"82":1,"83":1,"84":3,"85":15,"86":1,"87":1,"88":1,"89":15,"90":15,"91":199,"92":5,"93":5,"94":1,"95":4,"96":4,"97":4,"98":199,"99":6,"100":6,"101":6,"102":2,"103":6,"104":1,"105":1,"106":5,"107":4,"108":4,"109":3,"110":3,"111":1,"112":199,"113":4,"114":4,"115":1,"116":3,"117":3,"118":2,"119":3,"120":199,"121":19,"122":7,"123":199,"124":199,"125":199,"126":5,"127":1,"128":2,"129":2,"130":6,"131":5,"132":8,"133":5,"134":3,"135":3,"136":2,"137":2,"138":1,"139":2,"140":2,"141":2,"142":2,"143":16,"144":2,"145":2,"146":2,"147":2,"148":2,"149":4,"150":2,"151":2,"152":2,"153":2,"154":2,"155":1,"156":1,"157":1,"158":1,"159":1,"160":10,"161":1,"162":1,"163":1,"164":17,"165":94,"166":11,"167":11,"168":11,"169":1,"170":2,"171":2,"172":60,"173":17,"174":17,"175":17,"176":1,"177":2,"178":1,"179":1,"180":1,"181":6,"182":2,"183":1,"184":4,"185":1,"186":1,"187":1,"188":1,"189":1,"190":1,"191":664,"192":664,"193":2,"194":1194,"195":1194,"196":2,"197":33,"198":1,"199":1,"200":2,"201":12,"202":15,"203":3,"204":3,"205":1},"f":{"0":199,"1":43,"2":9,"3":2,"4":3,"5":10,"6":5,"7":15,"8":3,"9":4,"10":3,"11":1,"12":5,"13":4,"14":6,"15":4,"16":3,"17":4,"18":2,"19":19,"20":7,"21":5,"22":1,"23":2,"24":2,"25":6,"26":5,"27":8,"28":5,"29":1,"30":2,"31":2,"32":16,"33":2,"34":2,"35":4,"36":2,"37":2,"38":2,"39":2,"40":1,"41":1,"42":10,"43":1,"44":1,"45":1,"46":17,"47":94,"48":11,"49":1,"50":2,"51":2,"52":60,"53":17,"54":1,"55":2,"56":1,"57":1,"58":1,"59":6,"60":2,"61":4,"62":1,"63":664,"64":2,"65":1194,"66":2,"67":33,"68":1,"69":1,"70":2,"71":12,"72":15,"73":1},"b":{"0":[199],"1":[199],"2":[199,190],"3":[199,197],"4":[199,1],"5":[199,174],"6":[199,196],"7":[199,196],"8":[199,196],"9":[199,192],"10":[199,192],"11":[199,173],"12":[199,173],"13":[199,193],"14":[199,2],"15":[32,11],"16":[43,12,12,11],"17":[1,10],"18":[1,8],"19":[1,7],"20":[6,1],"21":[6,3,3],"22":[2,5],"23":[7,7],"24":[2,2],"25":[1,2],"26":[2,1],"27":[199,149,139,136,136,129,125,124,102,102,99,89],"28":[1,4],"29":[5,4,3,2],"30":[15,13],"31":[15,13],"32":[15,12],"33":[15,13],"34":[15,14],"35":[15,13],"36":[1,14],"37":[15,2,1,1],"38":[3,12],"39":[15,3],"40":[1,1],"41":[1,14],"42":[15,1],"43":[1,4],"44":[2,4],"45":[1,5],"46":[3,1],"47":[1,3],"48":[2,1],"49":[113,86],"50":[113,86],"51":[199,166],"52":[3,163],"53":[3,1],"54":[199,196],"55":[1,1],"56":[1,195],"57":[33,162],"58":[33,162],"59":[1,5],"60":[196,163],"61":[13,183],"62":[196,163],"63":[33,163],"64":[196,163,13],"65":[3,2],"66":[1,1],"67":[13,5],"68":[199,188,21],"69":[16,14,4,4],"70":[16,14],"71":[1,1],"72":[199,166],"73":[3,163],"74":[3,1],"75":[166,3,2,164],"76":[94,93],"77":[164,163],"78":[60,59],"79":[164,163],"80":[199,188],"81":[1,1],"82":[199,25],"83":[199,11,3],"84":[4,3],"85":[199,10],"86":[199,5],"87":[199,166],"88":[1,1],"89":[7,657],"90":[7,657],"91":[1,1],"92":[11,1183],"93":[199,166,3,164],"94":[33,32],"95":[164,163],"96":[3,12],"97":[15,13],"98":[199,196,1],"99":[199,196,33],"100":[199,196],"101":[16,180]},"meta":{"lastBranch":102,"lastFunction":74,"lastStatement":206,"seen":{"s:16:18:16:Infinity":0,"s:17:18:17:Infinity":1,"f:43:24:43:43":0,"b:44:39:44:46":0,"b:44:57:44:64":1,"s:46:10:46:Infinity":2,"s:47:8:47:Infinity":3,"s:49:16:49:Infinity":4,"b:49:37:49:52:49:52:49:54":2,"s:50:16:50:Infinity":5,"b:50:37:50:52:50:52:50:54":3,"s:51:20:51:Infinity":6,"b:51:45:51:65:51:65:51:103":4,"s:52:20:52:Infinity":7,"b:52:45:52:78:52:78:52:80":5,"s:53:23:53:Infinity":8,"b:53:51:53:74:53:74:53:76":6,"s:54:22:54:Infinity":9,"b:54:64:54:86:54:86:54:90":7,"s:55:22:55:Infinity":10,"b:55:64:55:86:55:86:55:90":8,"s:56:24:56:Infinity":11,"s:57:26:57:Infinity":12,"s:58:30:58:Infinity":13,"s:59:8:59:Infinity":14,"s:60:15:60:Infinity":15,"b:60:35:60:49:60:49:60:51":9,"s:61:18:61:Infinity":16,"b:61:41:61:58:61:58:61:60":10,"s:62:15:62:Infinity":17,"b:62:45:62:70:62:70:62:72":11,"s:63:15:63:Infinity":18,"b:63:45:63:70:63:70:63:72":12,"s:64:15:64:Infinity":19,"b:64:45:64:59:64:59:64:61":13,"s:65:19:65:Infinity":20,"s:66:17:66:Infinity":21,"b:66:72:66:88:66:88:66:90":14,"s:67:23:67:Infinity":22,"s:68:25:68:Infinity":23,"s:69:26:69:Infinity":24,"s:70:17:70:Infinity":25,"s:71:22:71:Infinity":26,"s:72:19:72:Infinity":27,"s:73:24:73:Infinity":28,"s:74:25:74:Infinity":29,"s:75:8:75:Infinity":30,"s:76:8:76:Infinity":31,"s:78:2:113:Infinity":32,"f:78:2:78:18":1,"b:79:4:79:Infinity:undefined:undefined:undefined:undefined":15,"s:79:4:79:Infinity":33,"b:79:8:79:25:79:25:79:37:79:37:79:67:79:67:79:95":16,"s:79:95:79:Infinity":34,"b:80:4:83:Infinity:undefined:undefined:undefined:undefined":17,"s:80:4:83:Infinity":35,"s:81:6:81:Infinity":36,"s:82:6:82:Infinity":37,"s:85:17:85:Infinity":38,"s:86:4:86:Infinity":39,"s:87:4:110:Infinity":40,"f:87:45:87:51":2,"s:88:18:88:Infinity":41,"s:89:18:89:Infinity":42,"b:90:6:90:Infinity:undefined:undefined:undefined:undefined":18,"s:90:6:90:Infinity":43,"s:90:19:90:Infinity":44,"s:91:6:91:Infinity":45,"s:92:6:92:Infinity":46,"s:94:43:97:Infinity":47,"b:98:6:98:Infinity:undefined:undefined:undefined:undefined":19,"s:98:6:98:Infinity":48,"s:98:19:98:Infinity":49,"b:99:6:101:Infinity:undefined:undefined:undefined:undefined":20,"s:99:6:101:Infinity":50,"s:100:8:100:Infinity":51,"b:100:24:100:50:100:50:100:79:100:79:100:81":21,"b:102:6:104:Infinity:undefined:undefined:undefined:undefined":22,"s:102:6:104:Infinity":52,"b:102:10:102:50:102:50:102:78":23,"s:103:8:103:Infinity":53,"f:103:8:103:19":3,"s:103:30:103:122":54,"b:103:30:103:41:103:30:103:122":24,"s:105:6:105:Infinity":55,"f:106:4:106:7":4,"b:107:6:107:Infinity:undefined:undefined:undefined:undefined":25,"s:107:6:107:Infinity":56,"s:107:19:107:Infinity":57,"s:108:6:108:Infinity":58,"b:108:23:108:40:108:40:108:57":26,"s:109:6:109:Infinity":59,"s:112:4:112:Infinity":60,"f:112:4:112:17":5,"s:112:19:112:34":61,"s:116:4:127:Infinity":62,"b:116:4:116:Infinity:117:4:117:Infinity:118:4:118:Infinity:119:4:119:Infinity:120:4:120:Infinity:121:4:121:Infinity:122:4:122:Infinity:123:4:123:Infinity:124:4:124:Infinity:125:4:125:Infinity:126:4:126:Infinity:127:4:127:Infinity":27,"s:129:8:132:Infinity":63,"f:129:8:129:28":6,"b:130:4:130:Infinity:undefined:undefined:undefined:undefined":28,"s:130:4:130:Infinity":64,"b:130:8:130:24:130:24:130:37:130:37:130:48:130:48:130:108":29,"s:130:108:130:Infinity":65,"s:131:4:131:Infinity":66,"s:134:21:179:Infinity":67,"f:134:21:134:33":7,"s:135:4:135:Infinity":68,"s:136:4:178:Infinity":69,"s:137:22:152:Infinity":70,"b:138:15:138:24:138:24:138:Infinity":30,"b:139:15:139:24:139:24:139:Infinity":31,"b:141:20:141:33:141:33:141:Infinity":32,"b:142:23:142:39:142:39:142:Infinity":33,"b:145:14:145:22:145:22:145:Infinity":34,"b:146:17:146:28:146:28:146:Infinity":35,"f:147:19:147:26":8,"s:147:33:147:43":71,"f:148:32:148:39":9,"s:148:44:148:52":72,"f:148:66:148:73":10,"s:148:78:148:86":73,"b:150:12:150:Infinity:151:12:151:Infinity":36,"b:149:14:149:44:149:44:149:60:149:60:149:87:149:87:149:Infinity":37,"b:153:6:169:Infinity:undefined:undefined:undefined:undefined":38,"s:153:6:169:Infinity":74,"b:153:10:153:37:153:37:153:46":39,"s:154:25:154:Infinity":75,"s:155:8:155:Infinity":76,"s:156:8:168:Infinity":77,"s:157:29:159:Infinity":78,"f:158:12:158:24":11,"s:158:29:158:Infinity":79,"s:160:10:160:Infinity":80,"b:161:10:163:Infinity:undefined:undefined:undefined:undefined":40,"s:161:10:163:Infinity":81,"s:162:12:162:Infinity":82,"s:165:10:165:Infinity":83,"s:167:10:167:Infinity":84,"b:170:6:174:Infinity:undefined:undefined:undefined:undefined":41,"s:170:6:174:Infinity":85,"b:170:10:170:39:170:39:170:48":42,"s:171:8:173:Infinity":86,"s:172:10:172:Infinity":87,"s:172:16:172:61":88,"s:175:6:175:Infinity":89,"s:177:6:177:Infinity":90,"s:181:27:186:Infinity":91,"f:181:27:181:34":12,"s:182:18:182:Infinity":92,"b:183:4:183:Infinity:undefined:undefined:undefined:undefined":43,"s:183:4:183:Infinity":93,"s:183:24:183:Infinity":94,"s:184:23:184:Infinity":95,"s:185:4:185:Infinity":96,"f:185:4:185:20":13,"s:185:28:185:52":97,"s:188:8:206:Infinity":98,"f:188:8:188:26":14,"s:189:4:189:Infinity":99,"s:190:4:190:Infinity":100,"b:191:4:191:Infinity:undefined:undefined:undefined:undefined":44,"s:191:4:191:Infinity":101,"s:191:34:191:Infinity":102,"b:192:4:195:Infinity:undefined:undefined:undefined:undefined":45,"s:192:4:195:Infinity":103,"s:193:6:193:Infinity":104,"s:194:6:194:Infinity":105,"s:196:4:205:Infinity":106,"f:196:42:196:54":15,"s:197:6:204:Infinity":107,"s:198:20:198:Infinity":108,"s:199:8:201:Infinity":109,"b:199:28:199:42:199:42:199:46":46,"f:199:58:199:63":16,"s:199:150:201:10":110,"s:203:8:203:Infinity":111,"s:208:8:213:Infinity":112,"f:208:8:208:23":17,"s:209:18:209:Infinity":113,"b:210:4:210:Infinity:undefined:undefined:undefined:undefined":47,"s:210:4:210:Infinity":114,"s:210:16:210:Infinity":115,"b:211:4:211:Infinity:undefined:undefined:undefined:undefined":48,"s:211:4:211:Infinity":116,"s:211:31:211:Infinity":117,"f:211:31:211:39":18,"s:211:47:211:63":118,"s:212:4:212:Infinity":119,"s:215:33:215:Infinity":120,"f:215:47:215:54":19,"s:215:60:215:93":121,"f:215:68:215:73":20,"s:215:78:215:92":122,"s:217:18:217:Infinity":123,"s:218:21:218:Infinity":124,"s:220:2:665:Infinity":125,"b:226:34:226:65:226:65:226:Infinity":49,"b:230:28:230:59:230:59:230:Infinity":50,"b:238:9:238:26:238:26:246:Infinity":51,"b:239:10:241:11:241:10:246:Infinity":52,"b:239:73:239:82:239:82:239:124":53,"f:243:12:243:22":21,"s:243:27:243:Infinity":126,"b:249:9:249:Infinity:250:10:319:Infinity":54,"f:251:133:251:142":22,"s:251:149:251:191":127,"f:252:118:252:127":23,"s:252:134:252:176":128,"f:256:16:256:32":24,"s:256:16:256:Infinity":129,"b:256:46:256:58:256:58:256:67":55,"b:261:18:266:Infinity:266:18:269:Infinity":56,"b:267:35:267:77:267:76:268:35":57,"b:268:35:268:60:268:60:268:Infinity":58,"f:274:16:274:31":25,"s:274:31:274:Infinity":130,"b:274:45:274:72:274:72:274:Infinity":59,"f:274:72:274:91":26,"s:274:96:274:98":131,"b:275:26:275:42:275:42:275:Infinity":60,"b:278:22:278:Infinity:279:22:279:Infinity":61,"b:277:18:277:34:277:34:277:Infinity":62,"b:283:31:283:60:283:60:283:Infinity":63,"b:287:13:287:29:287:29:287:Infinity:288:14:317:Infinity":64,"f:290:42:290:46":27,"s:291:20:309:Infinity":132,"f:296:31:296:43":28,"b:297:24:305:Infinity:302:31:305:Infinity":65,"s:297:24:305:Infinity":133,"s:298:26:301:Infinity":134,"s:299:43:299:Infinity":135,"b:300:28:300:Infinity:undefined:undefined:undefined:undefined":66,"s:300:28:300:Infinity":136,"s:300:40:300:Infinity":137,"f:300:40:300:50":29,"s:300:58:300:75":138,"s:303:26:303:Infinity":139,"f:303:26:303:44":30,"s:303:52:303:68":140,"s:304:26:304:Infinity":141,"f:304:26:304:36":31,"s:304:44:304:57":142,"b:311:19:311:Infinity:312:20:316:Infinity":67,"b:322:10:322:31:322:31:322:Infinity:323:10:376:Infinity":68,"f:324:20:324:25":32,"s:325:14:361:Infinity":143,"b:327:17:327:30:327:30:327:41:327:41:327:Infinity:328:18:342:Infinity":69,"f:330:20:330:35":33,"s:331:22:337:Infinity":144,"f:331:22:331:32":34,"s:332:37:332:Infinity":145,"s:333:40:333:Infinity":146,"s:334:24:334:Infinity":147,"s:335:24:335:Infinity":148,"f:335:29:335:38":35,"s:335:50:335:115":149,"f:335:99:335:111":36,"s:336:24:336:Infinity":150,"b:344:17:344:Infinity:345:18:359:Infinity":70,"f:347:29:347:41":37,"s:348:22:348:Infinity":151,"f:348:22:348:32":38,"s:348:40:348:71":152,"f:348:45:348:52":39,"s:348:57:348:70":153,"b:349:22:353:Infinity:351:29:353:Infinity":71,"s:349:22:353:Infinity":154,"s:350:24:350:Infinity":155,"s:350:30:350:75":156,"s:352:24:352:Infinity":157,"f:352:24:352:42":40,"s:352:50:352:80":158,"f:352:55:352:62":41,"s:352:68:352:79":159,"f:363:26:363:31":42,"s:364:14:374:Infinity":160,"f:368:18:368:33":43,"s:368:33:368:Infinity":161,"f:368:33:368:49":44,"s:368:57:368:87":162,"f:368:62:368:70":45,"s:368:79:368:86":163,"b:379:9:379:25:379:25:470:Infinity":72,"b:381:12:387:13:387:12:397:13":73,"b:381:12:381:Infinity:382:14:387:13":74,"f:390:14:390:24":46,"s:390:29:390:Infinity":164,"b:397:12:397:25:397:25:397:44:397:44:397:Infinity:398:10:470:Infinity":75,"f:406:22:406:27":47,"s:407:18:421:Infinity":165,"f:412:22:412:32":48,"s:412:52:412:63":166,"s:412:63:412:89":167,"s:412:89:412:103":168,"b:416:21:416:Infinity:417:22:419:Infinity":76,"f:417:44:417:59":49,"s:417:59:417:100":169,"f:417:72:417:80":50,"s:417:89:417:96":170,"b:423:17:423:Infinity:424:18:431:Infinity":77,"f:426:20:426:35":51,"s:426:35:426:Infinity":171,"f:439:22:439:27":52,"s:440:18:454:Infinity":172,"f:445:22:445:32":53,"s:445:52:445:63":173,"s:445:63:445:89":174,"s:445:89:445:103":175,"b:449:21:449:Infinity:450:22:452:Infinity":78,"f:450:44:450:59":54,"s:450:59:450:100":176,"f:450:72:450:80":55,"s:450:89:450:96":177,"b:456:17:456:Infinity:457:18:464:Infinity":79,"f:459:20:459:35":56,"s:459:35:459:Infinity":178,"f:481:16:481:26":57,"s:481:31:481:Infinity":179,"f:493:16:493:26":58,"s:493:31:493:Infinity":180,"b:505:21:505:38:505:38:505:Infinity":80,"f:507:14:507:24":59,"s:507:29:507:Infinity":181,"f:508:14:508:29":60,"b:508:31:508:92:undefined:undefined:undefined:undefined":81,"s:508:31:508:92":182,"s:508:63:508:92":183,"b:512:13:512:36:512:36:513:Infinity":82,"b:514:11:514:34:514:34:514:Infinity:515:12:537:Infinity":83,"f:516:31:516:36":61,"s:517:16:535:Infinity":184,"f:520:18:520:33":62,"s:521:20:521:Infinity":185,"s:522:20:522:Infinity":186,"s:523:20:523:Infinity":187,"s:524:20:524:Infinity":188,"s:525:20:525:Infinity":189,"s:526:20:526:Infinity":190,"b:533:21:533:34:533:34:534:Infinity":84,"b:539:11:539:23:539:23:539:Infinity":85,"b:540:11:540:28:540:28:540:Infinity":86,"b:544:9:544:25:544:25:566:Infinity":87,"f:547:26:547:30":63,"s:548:29:548:Infinity":191,"s:549:14:562:Infinity":192,"f:554:18:554:33":64,"s:554:33:554:Infinity":193,"b:554:50:554:55:554:55:554:59":88,"b:556:29:556:34:556:34:556:Infinity":89,"b:558:34:558:112:558:112:558:Infinity":90,"f:571:27:571:31":65,"s:572:27:572:Infinity":194,"s:573:12:587:Infinity":195,"f:578:16:578:31":66,"s:578:31:578:Infinity":196,"b:578:51:578:56:578:56:578:60":91,"b:581:22:581:Infinity:582:22:582:Infinity":92,"b:593:9:593:26:593:26:593:39:593:39:593:Infinity:594:10:623:Infinity":93,"f:597:20:597:24":67,"s:598:16:605:Infinity":197,"b:600:19:600:Infinity:601:20:605:Infinity":94,"f:601:42:601:57":68,"s:601:57:601:103":198,"f:601:57:601:65":69,"s:601:73:601:100":199,"f:601:78:601:85":70,"s:601:90:601:99":200,"b:607:15:607:Infinity:608:16:622:Infinity":95,"f:610:18:610:28":71,"s:610:33:610:Infinity":201,"f:611:18:611:29":72,"b:612:20:615:Infinity:undefined:undefined:undefined:undefined":96,"s:612:20:615:Infinity":202,"b:612:24:612:45:612:45:612:60":97,"s:613:22:613:Infinity":203,"s:614:22:614:Infinity":204,"b:628:9:628:22:628:22:628:Infinity:629:10:636:Infinity":98,"b:638:9:638:22:638:22:638:Infinity:639:10:645:Infinity":99,"f:641:12:641:27":73,"s:641:27:641:Infinity":205,"b:654:9:654:Infinity:655:10:662:Infinity":100,"b:661:22:661:43:661:43:661:Infinity":101},"fnNames":{}}} -} diff --git a/client/src/api/client.behavior.test.ts b/client/src/api/client.behavior.test.ts index a5ff0cf08..71837160a 100644 --- a/client/src/api/client.behavior.test.ts +++ b/client/src/api/client.behavior.test.ts @@ -1,4 +1,4 @@ -// FE-APIWIRE-001 to FE-APIWIRE-035 +// FE-APIWIRE-001 to FE-APIWIRE-036 import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { AxiosError, type AxiosAdapter, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' import { http, HttpResponse } from 'msw' @@ -184,6 +184,20 @@ describe('client > rate-limit translation', () => { const err = await captureError(() => apiClient.get('/limited')) expect(err.response?.data).toEqual({ error: 'Too many attempts. Please try again later.' }) }) + + it('FE-APIWIRE-035: an array 429 body is replaced, not grafted onto', async () => { + server.use(http.get('/api/limited', () => HttpResponse.json([{ field: 'email' }], { status: 429 }))) + const err = await captureError(() => apiClient.get('/limited')) + expect(err.response?.data).toEqual({ error: 'Too many attempts. Please try again later.' }) + }) + + it('FE-APIWIRE-036: Catalan, Greek and Vietnamese have their own 429 message', async () => { + for (const lang of ['ca', 'gr', 'vi']) { + localStorage.setItem('app_language', lang) + const err = await captureError(() => apiClient.get('/limited')) + expect(err.message).not.toBe('Too many attempts. Please try again later.') + } + }) }) describe('client > proxy auth challenges', () => { @@ -398,6 +412,7 @@ describe('client > adminApi.llmLocalPull', () => { read: async () => (i < chunks.length ? { done: false, value: encoder.encode(chunks[i++]) } : { done: true, value: undefined }), + cancel: async () => {}, }), }, } as unknown as Response @@ -450,13 +465,26 @@ describe('client > adminApi.llmLocalPull', () => { .rejects.toThrow('Pull failed (500)') }) - it('FE-APIWIRE-033: a 200 without a readable body is treated as a failure', async () => { + it('FE-APIWIRE-036: a throw from onProgress aborts the pull', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(streamingResponse([ + '{"status":"pulling manifest"}\n{"error":"manifest not found"}\n{"status":"success"}\n', + ])) + const onProgress = vi.fn((p: { error?: string }) => { + if (p.error) throw new Error(p.error) + }) + + await expect(adminApi.llmLocalPull('http://ollama:11434', 'x', onProgress)) + .rejects.toThrow('manifest not found') + expect(onProgress).toHaveBeenCalledTimes(2) + }) + + it('FE-APIWIRE-033: a 200 without a readable body reports the missing stream', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: true, status: 200, body: null, json: async () => ({}), } as unknown as Response) await expect(adminApi.llmLocalPull('http://ollama:11434', 'x', vi.fn())) - .rejects.toThrow('Pull failed (200)') + .rejects.toThrow('Pull returned no progress stream') }) }) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index f3d9c833f..8d355bf48 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -105,6 +105,9 @@ const RATE_LIMIT_MESSAGES: Record = { ko: '시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.', uk: 'Занадто багато спроб. Спробуйте пізніше.', sv: 'För många försök. Prova igen senare.', + ca: 'Massa intents. Torneu-ho a provar més tard.', + gr: 'Πάρα πολλές προσπάθειες. Δοκιμάστε ξανά αργότερα.', + vi: 'Quá nhiều lần thử. Vui lòng thử lại sau.', } function translateRateLimit(): string { @@ -228,9 +231,11 @@ apiClient.interceptors.response.use( } if (error.response?.status === 429) { const translated = translateRateLimit() - const data = error.response.data as { error?: string } | undefined - if (data && typeof data === 'object') { - data.error = translated + const data = error.response.data + // Only a plain object body carries an `error` field worth overwriting; + // an array (a validation-error list) or a string is replaced outright. + if (data && typeof data === 'object' && !Array.isArray(data)) { + (data as { error?: string }).error = translated } else { error.response.data = { error: translated } } @@ -551,24 +556,35 @@ export const adminApi = { headers: { 'content-type': 'application/json' }, body: JSON.stringify({ baseUrl, model }), }) - if (!res.ok || !res.body) { + if (!res.ok) { let msg = `Pull failed (${res.status})` try { msg = (await res.json())?.error ?? msg } catch { /* non-json */ } throw new Error(msg) } + // An accepted request without a stream can't be followed to completion. + if (!res.body) throw new Error('Pull returned no progress stream') const reader = res.body.getReader() const dec = new TextDecoder() let buf = '' - for (;;) { - const { done, value } = await reader.read() - if (done) break - buf += dec.decode(value, { stream: true }) - const lines = buf.split('\n') - buf = lines.pop() ?? '' - for (const line of lines) { - if (!line.trim()) continue - try { onProgress(JSON.parse(line)) } catch { /* skip partial */ } + try { + for (;;) { + const { done, value } = await reader.read() + if (done) break + buf += dec.decode(value, { stream: true }) + // split() always yields at least one element; the last one is the + // trailing (possibly partial) line carried into the next chunk. + const lines = buf.split('\n') + buf = lines.pop()! + for (const line of lines) { + if (!line.trim()) continue + // Only the parse is swallowed — a throw from onProgress aborts the pull. + let frame: { status?: string; total?: number; completed?: number; error?: string } + try { frame = JSON.parse(line) } catch { continue } + onProgress(frame) + } } + } finally { + reader.cancel().catch(() => {}) } }, checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data), diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index 20c6c1a5c..6f13fcf7d 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -560,7 +560,7 @@ describe('AddonManager', () => { }); }); - it('FE-ADMIN-ADDON-027: an error frame in the pull stream is tolerated and the row returns to idle', async () => { + it('FE-ADMIN-ADDON-027: an error frame in the pull stream aborts the pull and is reported', async () => { const user = userEvent.setup(); server.use( addonsRoute([llmAddon({ provider: 'local' })]), @@ -575,6 +575,8 @@ describe('AddonManager', () => { await screen.findByText('No models installed yet — pull one below.'); await user.click(screen.getByRole('button', { name: 'Pull' })); + await screen.findByText('manifest not found'); + expect(screen.queryByText('Model pulled')).not.toBeInTheDocument(); await waitFor(() => expect(screen.getByRole('button', { name: 'Pull' })).toBeEnabled()); expect(screen.queryByText('Pulling…')).not.toBeInTheDocument(); }); diff --git a/client/src/components/Admin/AdminPluginsPanel.test.tsx b/client/src/components/Admin/AdminPluginsPanel.test.tsx index 74da6b02f..7fff27480 100644 --- a/client/src/components/Admin/AdminPluginsPanel.test.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.test.tsx @@ -1242,6 +1242,21 @@ describe('AdminPluginsPanel — Discover cards and the detail modal', () => { expect(screen.queryByText('Setup')).not.toBeInTheDocument() }) + it('FE-COMP-PLUGINS-PANEL-043b: a manifest that omits the lists still renders the modal', async () => { + const detail = manifestDetail({ size: 0 }) + delete (detail.manifest as Record).egress + delete (detail.manifest as Record).settings + delete (detail.manifest as Record).permissions + discoverWith({}, detail) + render() + await clickDiscover() + fireEvent.click(await screen.findByText('Gotify')) + + expect(await screen.findByText('Needs no special access.')).toBeInTheDocument() + expect(screen.queryByText('Connects to')).not.toBeInTheDocument() + expect(screen.queryByText('Setup')).not.toBeInTheDocument() + }) + it('FE-COMP-PLUGINS-PANEL-044: an incompatible entry explains the blocked button in the modal body', async () => { discoverWith({ compatible: false, latestCompatible: null, trek: '>=4.0.0', hostVersion: '3.3.0' }) render() diff --git a/client/src/components/Admin/AdminPluginsPanel.tsx b/client/src/components/Admin/AdminPluginsPanel.tsx index 2d0baa95b..de5d42c54 100644 --- a/client/src/components/Admin/AdminPluginsPanel.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.tsx @@ -96,15 +96,19 @@ interface RegistryDetail extends RegistryItem { size: number | null publishedAt: string | null manifest: { - permissions: string[] - egress: string[] + // Optional because a registry may omit an empty list — the detail view has to + // degrade rather than throw halfway through its render. + permissions?: string[] + egress?: string[] /** The plugin needs OPERATOR-supplied hosts — its egress list is not the whole story. */ operatorEgress?: boolean - settings: Array<{ key: string; label: string; inputType: string; scope: string; required: boolean }> + settings?: Array<{ key: string; label: string; inputType: string; scope: string; required: boolean }> license: string | null icon: string | null requiredAddons?: string[] pluginDependencies?: PluginDep[] + /** Display slice of the manifest's capabilities — drives the same chips as an installed row. */ + capabilities?: { widget?: { slot?: string }; tripPage?: { replaces?: string[] } } } | null } @@ -300,13 +304,8 @@ function installOffer(item: RegistryItem, t: T): { blocked: boolean; version?: s return { blocked: true, label: t('admin.plugins.incompatible'), title } } -function ReviewedBadge({ t, compact }: { t: T; compact?: boolean }) { - if (compact) return - return ( - - {t('admin.plugins.reviewed')} - - ) +function ReviewedBadge({ t }: { t: T }) { + return } /** Marks a manually-uploaded (sideloaded) plugin: no registry, unsigned, not reviewed. */ @@ -1149,7 +1148,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, blocked
{p.name} {p.version && v{p.version}} - {p.reviewed_at && } + {p.reviewed_at && } {p.source_repo === 'local:upload' && } {p.source_repo === 'local:link' && } {/* Registry plugins only — a sideloaded/dev-linked plugin already says something @@ -1401,7 +1400,10 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local }, [item.id]) const manifest = detail?.manifest ?? null - const caps = manifest ? deriveCaps(manifest.permissions, {}, t) : [] + const permissions = manifest?.permissions ?? [] + const egress = manifest?.egress ?? [] + const settings = manifest?.settings ?? [] + const caps = manifest ? deriveCaps(permissions, manifest.capabilities ?? {}, t) : [] const repoUrl = `https://github.com/${item.repo}` const homepage = item.homepage && /^https?:\/\//i.test(item.homepage) && item.homepage !== repoUrl ? item.homepage : null const sizeKb = detail?.size ? Math.max(1, Math.round(detail.size / 1024)) : null @@ -1425,7 +1427,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local

{item.name}

- {item.reviewedAt && } + {item.reviewedAt && }

{item.author}{item.latest ? ` · v${item.latest}` : ''}

@@ -1451,7 +1453,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local {manifest && (

{t('admin.plugins.accessTitle')}

- {caps.filter(c => !c.net).length === 0 && !manifest.permissions.includes('db:own') ? ( + {caps.filter(c => !c.net).length === 0 && !permissions.includes('db:own') ? (

{t('admin.plugins.noAccess')}

) : (
@@ -1460,7 +1462,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local {c.label}
))} - {manifest.permissions.includes('db:own') && ( + {permissions.includes('db:own') && (
{t('admin.plugins.perm.db:own')}
@@ -1470,11 +1472,11 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
)} - {manifest && (manifest.egress.length > 0 || manifest.operatorEgress) && ( + {manifest && (egress.length > 0 || manifest.operatorEgress) && (

{t('admin.plugins.connectsTitle')}

- {manifest.egress.map(h => ( + {egress.map(h => ( {h} ))} {/* The hosts above are NOT the whole story for this plugin: it talks to a @@ -1493,11 +1495,11 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
)} - {manifest && manifest.settings.length > 0 && ( + {manifest && settings.length > 0 && (

{t('admin.plugins.setupTitle')}

    - {manifest.settings.map(s => ( + {settings.map(s => (
  • {s.label} {t(`admin.plugins.scope.${s.scope}` as never)} diff --git a/client/src/components/Admin/CategoryManager.tsx b/client/src/components/Admin/CategoryManager.tsx index 687378966..6c68f4bc0 100644 --- a/client/src/components/Admin/CategoryManager.tsx +++ b/client/src/components/Admin/CategoryManager.tsx @@ -56,8 +56,8 @@ export default function CategoryManager() { setEditingId(null) } + // The Save button carries disabled={… || !form.name.trim()}, so the name is set here. const handleSave = async () => { - if (!form.name.trim()) { toast.error(t('categories.toast.nameRequired')); return } setIsSaving(true) try { if (editingId) { diff --git a/client/src/components/Admin/DefaultUserSettingsTab.tsx b/client/src/components/Admin/DefaultUserSettingsTab.tsx index c9121edec..d7bd03b2b 100644 --- a/client/src/components/Admin/DefaultUserSettingsTab.tsx +++ b/client/src/components/Admin/DefaultUserSettingsTab.tsx @@ -48,8 +48,8 @@ function normalizeProvider(value: unknown): MapProvider { return value === 'mapbox-gl' || value === 'maplibre-gl' ? value : 'leaflet' } -function styleForProvider(provider: MapProvider, style?: string | null): string { - if (provider === 'leaflet') return style || MAPBOX_DEFAULT_STYLE +/** Only the GL providers keep a style — Leaflet is handled by its callers. */ +function styleForProvider(provider: GlMapProvider, style?: string | null): string { if (provider === 'mapbox-gl' && isOpenFreeMapStyle(style)) return MAPBOX_DEFAULT_STYLE return normalizeStyleForProvider(provider, style) } diff --git a/client/src/components/Admin/DevNotificationsPanel.test.tsx b/client/src/components/Admin/DevNotificationsPanel.test.tsx index 9402586ab..35932e52e 100644 --- a/client/src/components/Admin/DevNotificationsPanel.test.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.test.tsx @@ -110,7 +110,20 @@ describe('DevNotificationsPanel', () => { }); }); - it('FE-ADMIN-DEVNOTIF-008: error toast shown on API failure', async () => { + it('FE-ADMIN-DEVNOTIF-008: the server error field is what the toast shows', async () => { + server.use( + http.post('/api/admin/dev/test-notification', () => + HttpResponse.json({ error: 'No channel configured' }, { status: 500 }), + ), + ); + const user = userEvent.setup(); + render(<>); + await screen.findByText('Type Testing'); + await user.click(screen.getByText('Simple → Me').closest('button')!); + await screen.findByText('No channel configured'); + }); + + it('FE-ADMIN-DEVNOTIF-008b: a failure without an error field falls back to the generic text', async () => { server.use( http.post('/api/admin/dev/test-notification', () => HttpResponse.json({ message: 'Server error' }, { status: 500 }), @@ -120,7 +133,7 @@ describe('DevNotificationsPanel', () => { render(<>); await screen.findByText('Type Testing'); await user.click(screen.getByText('Simple → Me').closest('button')!); - await screen.findByText(/failed|error/i); + await screen.findByText('Failed'); }); it('FE-ADMIN-DEVNOTIF-009: changing trip selector updates payload targetId', async () => { diff --git a/client/src/components/Admin/DevNotificationsPanel.tsx b/client/src/components/Admin/DevNotificationsPanel.tsx index f5322e739..7237b23ae 100644 --- a/client/src/components/Admin/DevNotificationsPanel.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react' import { adminApi, tripsApi } from '../../api/client' +import { getApiErrorMessage } from '../../utils/apiError' import { useAuthStore } from '../../store/authStore' import { useToast } from '../shared/Toast' import { @@ -46,8 +47,8 @@ export default function DevNotificationsPanel(): React.ReactElement { try { await adminApi.sendTestNotification(payload) toast.success(`Sent: ${label}`) - } catch (err: any) { - toast.error(err.message || 'Failed') + } catch (err: unknown) { + toast.error(getApiErrorMessage(err, 'Failed')) } finally { setSending(null) } diff --git a/client/src/components/Admin/PackingTemplateManager.test.tsx b/client/src/components/Admin/PackingTemplateManager.test.tsx index 1e150a2aa..8abb51cde 100644 --- a/client/src/components/Admin/PackingTemplateManager.test.tsx +++ b/client/src/components/Admin/PackingTemplateManager.test.tsx @@ -697,7 +697,7 @@ describe('PackingTemplateManager', () => { await user.click(categoryButtons('Clothing')[2]); - expect(await screen.findByText('Failed to delete template')).toBeInTheDocument(); + expect(await screen.findByText('Failed to delete category')).toBeInTheDocument(); expect(screen.getByText('Clothing')).toBeInTheDocument(); expect(screen.getByText('T-shirt')).toBeInTheDocument(); }); @@ -798,7 +798,7 @@ describe('PackingTemplateManager', () => { await user.click(itemButtons('T-shirt')[1]); - expect(await screen.findByText('Failed to delete template')).toBeInTheDocument(); + expect(await screen.findByText('Failed to delete item')).toBeInTheDocument(); expect(screen.getByText('T-shirt')).toBeInTheDocument(); }); diff --git a/client/src/components/Admin/PackingTemplateManager.tsx b/client/src/components/Admin/PackingTemplateManager.tsx index adcbe9a0c..9773b1999 100644 --- a/client/src/components/Admin/PackingTemplateManager.tsx +++ b/client/src/components/Admin/PackingTemplateManager.tsx @@ -115,12 +115,13 @@ export default function PackingTemplateManager() { await adminApi.deleteTemplateCategory(expandedId, catId) setCategories(prev => prev.filter(c => c.id !== catId)) setItems(prev => prev.filter(i => i.category_id !== catId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.packingTemplates.deleteCategoryError')) } } // Item CRUD const handleAddItem = async (catId: number) => { - if (!newItemName.trim() || !expandedId) return + // The name is already guaranteed non-empty by the button and the Enter handler. + if (!expandedId) return try { const data = await adminApi.addTemplateItem(expandedId, catId, { name: newItemName.trim() }) setItems(prev => [...prev, data.item]) @@ -143,7 +144,7 @@ export default function PackingTemplateManager() { try { await adminApi.deleteTemplateItem(expandedId, itemId) setItems(prev => prev.filter(i => i.id !== itemId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.packingTemplates.deleteItemError')) } } const inputStyle = 'w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent outline-none' diff --git a/client/src/components/Budget/BudgetPanel.helpers.test.ts b/client/src/components/Budget/BudgetPanel.helpers.test.ts index 438affa14..4a72cf3bb 100644 --- a/client/src/components/Budget/BudgetPanel.helpers.test.ts +++ b/client/src/components/Budget/BudgetPanel.helpers.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { calcPP, hasCustomMemberSplit } from './BudgetPanel.helpers' +import { calcPP, hasCustomMemberSplit, normalizePastedAmount } from './BudgetPanel.helpers' describe('BudgetPanel.helpers', () => { describe('hasCustomMemberSplit (#1458)', () => { @@ -25,4 +25,18 @@ describe('BudgetPanel.helpers', () => { expect(calcPP(100, 0)).toBeNull() expect(calcPP(100, null)).toBeNull() }) + + describe('normalizePastedAmount', () => { + it('keeps the last separator as the decimal point', () => { + expect(normalizePastedAmount('1.234,56 €')).toBe('1234.56') + expect(normalizePastedAmount('$1,234.56')).toBe('1234.56') + expect(normalizePastedAmount(' -12,5 ')).toBe('-12.5') + }) + + it('drops everything that is not part of the number', () => { + expect(normalizePastedAmount('EUR 1 234 567')).toBe('1234567') + expect(normalizePastedAmount('42')).toBe('42') + expect(normalizePastedAmount('abc')).toBe('') + }) + }) }) diff --git a/client/src/components/Budget/BudgetPanel.helpers.ts b/client/src/components/Budget/BudgetPanel.helpers.ts index c3b13fdc9..987b4a18d 100644 --- a/client/src/components/Budget/BudgetPanel.helpers.ts +++ b/client/src/components/Budget/BudgetPanel.helpers.ts @@ -77,3 +77,15 @@ export function splitColorFor(userId: number, order: number) { export function colorForUserId(userId: number) { return SPLIT_COLORS[((userId | 0) - 1 + SPLIT_COLORS.length * 1000) % SPLIT_COLORS.length] } + +/** + * Normalises a pasted amount to a plain `1234.56` string: drops currency + * symbols and spaces, treats the last comma/dot as the decimal separator and + * removes every thousand separator before it. + */ +export function normalizePastedAmount(raw: string): string { + const text = raw.trim().replace(/[^\d.,-]/g, '') + const decimalPos = Math.max(text.lastIndexOf(','), text.lastIndexOf('.')) + if (decimalPos === -1) return text.replace(/[.,]/g, '') + return text.substring(0, decimalPos).replace(/[.,]/g, '') + '.' + text.substring(decimalPos + 1) +} diff --git a/client/src/components/Budget/BudgetPanelAddItemRow.tsx b/client/src/components/Budget/BudgetPanelAddItemRow.tsx index 484cddaa2..5f9deb4fa 100644 --- a/client/src/components/Budget/BudgetPanelAddItemRow.tsx +++ b/client/src/components/Budget/BudgetPanelAddItemRow.tsx @@ -1,6 +1,7 @@ import { useState, useRef } from 'react' import { Plus } from 'lucide-react' import { CustomDatePicker } from '../shared/CustomDateTimePicker' +import { normalizePastedAmount } from './BudgetPanel.helpers' interface AddItemRowProps { onAdd: (data: { name: string; total_price: number; persons: number | null; days: number | null; note: string | null; expense_date: string | null }) => void @@ -33,7 +34,7 @@ export default function AddItemRow({ onAdd, t }: AddItemRowProps) { setPrice(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - onPaste={e => { e.preventDefault(); let t = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = t.lastIndexOf(','), ld = t.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { t = t.substring(0, dp).replace(/[.,]/g, '') + '.' + t.substring(dp + 1) } else { t = t.replace(/[.,]/g, '') } setPrice(t) }} + onPaste={e => { e.preventDefault(); setPrice(normalizePastedAmount(e.clipboardData.getData('text'))) }} placeholder="0,00" inputMode="decimal" style={{ ...inp, textAlign: 'center' }} /> diff --git a/client/src/components/Budget/BudgetPanelInlineEditCell.tsx b/client/src/components/Budget/BudgetPanelInlineEditCell.tsx index 5dae2a574..159bd500d 100644 --- a/client/src/components/Budget/BudgetPanelInlineEditCell.tsx +++ b/client/src/components/Budget/BudgetPanelInlineEditCell.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react' +import { normalizePastedAmount } from './BudgetPanel.helpers' interface InlineEditCellProps { value: string | number | null | undefined @@ -29,21 +30,7 @@ export default function InlineEditCell({ value, onSave, type = 'text', style = { const handlePaste = (e: React.ClipboardEvent) => { if (type !== 'number') return e.preventDefault() - let text = e.clipboardData.getData('text').trim() - // Strip everything except digits, dots, commas, minus - text = text.replace(/[^\d.,-]/g, '') - // Remove all thousand separators (dots or commas before 3-digit groups), keep last separator as decimal - const lastComma = text.lastIndexOf(',') - const lastDot = text.lastIndexOf('.') - const decimalPos = Math.max(lastComma, lastDot) - if (decimalPos > -1) { - const intPart = text.substring(0, decimalPos).replace(/[.,]/g, '') - const decPart = text.substring(decimalPos + 1) - text = intPart + '.' + decPart - } else { - text = text.replace(/[.,]/g, '') - } - setEditValue(text) + setEditValue(normalizePastedAmount(e.clipboardData.getData('text'))) } if (editing) { diff --git a/client/src/components/Budget/CostsPanel.test.tsx b/client/src/components/Budget/CostsPanel.test.tsx index 19e1b2686..d84f687e7 100644 --- a/client/src/components/Budget/CostsPanel.test.tsx +++ b/client/src/components/Budget/CostsPanel.test.tsx @@ -170,6 +170,35 @@ describe('CostsPanel — settlements in the ledger', () => { expect(screen.getByText('Unfinished')).toBeInTheDocument() }) + it('shows the net hint on a settled expense row and hides it while unfinished', async () => { + seedStore(useAuthStore, { user: buildUser({ id: 1, username: 'alice' }), isAuthenticated: true }) + seedStore(useSettingsStore, { settings: { ...useSettingsStore.getState().settings, default_currency: 'EUR' } }) + // alice fronted the whole 90 for a two-way split, so she is 45 up. + const lent = { + ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Dinner' }), + total_price: 90, + payers: [{ user_id: 1, amount: 90, username: 'alice' }], + members: [{ user_id: 1, username: 'alice', paid: 1 }, { user_id: 2, username: 'bob', paid: 0 }], + } + const unpaid = { + ...buildBudgetItem({ trip_id: 1, category: 'lodging', name: 'Hotel' }), + total_price: 90, + payers: [], + members: [{ user_id: 1, username: 'alice', paid: 0 }, { user_id: 2, username: 'bob', paid: 0 }], + } + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [lent, unpaid] })), + http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), + ) + render() + + const dinnerRow = (await screen.findByText('Dinner')).closest('.exp-row') as HTMLElement + expect(within(dinnerRow).getByText(/you lent/)).toHaveTextContent('45') + + const hotelRow = (await screen.findByText('Hotel')).closest('.exp-row') as HTMLElement + expect(within(hotelRow).queryByText(/you lent|you borrowed/)).toBeNull() + }) + it('sums only unfinished expenses in the Outstanding amount card', async () => { // Display in the trip's own currency so FX conversion is an identity — keeps the asserted sum deterministic. seedStore(useSettingsStore, { settings: { ...useSettingsStore.getState().settings, default_currency: 'EUR' } }) diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx index edc2723b3..d0e99bb18 100644 --- a/client/src/components/Budget/CostsPanel.tsx +++ b/client/src/components/Budget/CostsPanel.tsx @@ -11,6 +11,7 @@ import { budgetApi } from '../../api/client' import { useExchangeRates } from '../../hooks/useExchangeRates' import { useIsMobile } from '../../hooks/useIsMobile' import { formatMoney, currencyDecimals, currencyLocale, localizeAmountInput } from '../../utils/formatters' +import { downloadBlob } from '../../utils/fileDownload' import Modal from '../shared/Modal' import CustomSelect from '../shared/CustomSelect' import { CustomDatePicker } from '../shared/CustomDateTimePicker' @@ -319,13 +320,8 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps const bom = '' const blob = new Blob([bom + rows.join('\r\n')], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url const safeName = (trip?.title || 'trip').replace(/[^a-zA-Z0-9À-ɏ _-]/g, '').trim() - a.download = `costs-${safeName}.csv` - a.click() - URL.revokeObjectURL(url) + downloadBlob(blob, `costs-${safeName}.csv`) } // ── small presentational helpers ──────────────────────────────────────── @@ -763,7 +759,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
    {fmt(baseTotal(e))}
    - {!isUnfinished && (e.members || []).length > 0 && Math.abs(net) > 0.01 && ( + {!unfinished && (e.members || []).length > 0 && Math.abs(net) > 0.01 && (
    0 ? '#16a34a' : '#dc2626' }}> {net > 0 ? t('costs.youLent', { amount: fmt(net) }) : t('costs.youBorrowed', { amount: fmt(-net) })}
    diff --git a/client/src/components/Collab/CollabChatMessages.tsx b/client/src/components/Collab/CollabChatMessages.tsx index 1fe7b658a..b335c0bef 100644 --- a/client/src/components/Collab/CollabChatMessages.tsx +++ b/client/src/components/Collab/CollabChatMessages.tsx @@ -218,12 +218,9 @@ export function ChatMessages(props: any) { borderRadius: 99, background: 'var(--bg-card)', boxShadow: '0 1px 6px rgba(0,0,0,0.12)', border: '1px solid var(--border-faint)', }}> - {msg.reactions.map(r => { - const myReaction = r.users.some(u => String(u.user_id) === String(currentUser.id)) - return ( - { if (canEdit) handleReact(msg.id, r.emoji) }} /> - ) - })} + {msg.reactions.map(r => ( + { if (canEdit) handleReact(msg.id, r.emoji) }} /> + ))}
    )} diff --git a/client/src/components/Collab/CollabChatSmall.test.tsx b/client/src/components/Collab/CollabChatSmall.test.tsx index 5087f171a..43269b718 100644 --- a/client/src/components/Collab/CollabChatSmall.test.tsx +++ b/client/src/components/Collab/CollabChatSmall.test.tsx @@ -1,4 +1,4 @@ -// FE-W4CCS-001 to FE-W4CCS-014 +// FE-W4CCS-001 to FE-W4CCS-016 import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render' import type { ChatReaction } from './CollabChat.types' @@ -155,4 +155,33 @@ describe('WebsiteThumbnail', () => { await waitFor(() => expect(linkPreview).toHaveBeenCalled()) expect(screen.getByRole('link')).toHaveAttribute('title', 'not a url') }) + + it('FE-W4CCS-015: caches per trip, because the preview endpoint is trip-scoped', async () => { + linkPreview.mockResolvedValue({ title: 'Trip 4', image: null }) + const shared = 'https://example.com/shared' + const first = render() + await waitFor(() => expect(screen.getByRole('link')).toHaveAttribute('title', 'Trip 4')) + first.unmount() + + linkPreview.mockResolvedValue({ title: 'Trip 9', image: null }) + render() + + await waitFor(() => expect(screen.getByRole('link')).toHaveAttribute('title', 'Trip 9')) + expect(linkPreview).toHaveBeenCalledTimes(2) + expect(linkPreview).toHaveBeenLastCalledWith(9, shared) + }) + + it('FE-W4CCS-016: a broken OG image does not poison the next url', async () => { + linkPreview.mockResolvedValue({ title: 'A', image: 'https://cdn.example/a.png' }) + const { container, rerender } = render() + await waitFor(() => expect(container.querySelector('img')).not.toBeNull()) + fireEvent.error(container.querySelector('img')!) + expect(container.querySelector('img')).toBeNull() + + linkPreview.mockResolvedValue({ title: 'B', image: 'https://cdn.example/b.png' }) + rerender() + + await waitFor(() => expect(container.querySelector('img')).not.toBeNull()) + expect(container.querySelector('img')).toHaveAttribute('src', 'https://cdn.example/b.png') + }) }) diff --git a/client/src/components/Collab/CollabNotes.test.tsx b/client/src/components/Collab/CollabNotes.test.tsx index 0bb6129f2..618699e98 100644 --- a/client/src/components/Collab/CollabNotes.test.tsx +++ b/client/src/components/Collab/CollabNotes.test.tsx @@ -1270,7 +1270,7 @@ describe('CollabNotes', () => { }); }); -// FE-W5CNT-001 to FE-W5CNT-028 +// FE-W5CNT-001 to FE-W5CNT-029 // Fills in the load/error/attachment/category branches of useCollabNotes and the // view modal that the smoke tests above do not reach. @@ -1802,4 +1802,21 @@ describe('CollabNotes details', () => { await screen.findByText('Newer note'); expect(screen.getByText('Older note')).toBeInTheDocument(); }); + + it('FE-W5CNT-029: a failing delete reports an error and keeps the note in the list', async () => { + const user = userEvent.setup(); + serveNotes({ notes: [buildNote({ id: 30, title: 'Stubborn note' })] }); + server.use( + http.delete('/api/trips/1/collab/notes/30', () => new HttpResponse(null, { status: 500 })), + ); + render(); + await screen.findByText('Stubborn note'); + + await user.click(screen.getByTitle('Delete')); + const dialog = (await screen.findByText('Delete note?')).closest('div.trek-modal-enter') as HTMLElement; + await user.click(within(dialog).getByRole('button', { name: 'Delete' })); + + await waitFor(() => expect(addToast).toHaveBeenCalledWith('Error', 'error', undefined)); + expect(screen.getByText('Stubborn note')).toBeInTheDocument(); + }); }); diff --git a/client/src/components/Collab/CollabNotes.tsx b/client/src/components/Collab/CollabNotes.tsx index 04deb0a85..b5e8fb0e2 100644 --- a/client/src/components/Collab/CollabNotes.tsx +++ b/client/src/components/Collab/CollabNotes.tsx @@ -271,7 +271,7 @@ function CollabNotesHeader({ t, canEdit, setShowSettings, setShowNewModal }: Not {t('collab.notes.title')}
    - {canEdit &&
    diff --git a/client/src/components/Collections/CollectionPicker.tsx b/client/src/components/Collections/CollectionPicker.tsx index 5626ed87c..5cc071c82 100644 --- a/client/src/components/Collections/CollectionPicker.tsx +++ b/client/src/components/Collections/CollectionPicker.tsx @@ -31,6 +31,28 @@ function distanceTo(p: CollectionPlace, center: { lat: number; lng: number }): n interface Opt { key: string | number; label: string; icon?: React.ReactNode; count?: number } +/** Detail requests fired at once while building the union — keeps a user with + * many lists from opening the modal with a burst of parallel requests. */ +const DETAIL_BATCH = 4 + +/** Union of every list's places, in list order, without duplicates. */ +async function loadSavedPlaces(ids: number[]): Promise { + const merged: CollectionPlace[] = [] + const seen = new Set() + for (let i = 0; i < ids.length; i += DETAIL_BATCH) { + const batch = await Promise.all(ids.slice(i, i + DETAIL_BATCH).map(id => collectionsApi.get(id).catch(() => null))) + for (const d of batch) { + if (!d) continue + for (const p of d.places) { + if (seen.has(p.id)) continue + seen.add(p.id) + merged.push(p) + } + } + } + return merged +} + /** Compact click-away dropdown (Tailwind — this panel lives outside .trek-dash). */ function FilterDropdown({ current, options, onSelect, lead }: { current: string | number @@ -93,13 +115,8 @@ export default function CollectionPicker({ bias, onSelect, t }: CollectionPicker setLoading(true) collectionsApi.list() .then(async (res) => { - const detail = await Promise.all(res.collections.map(c => collectionsApi.get(c.id).catch(() => null))) + const merged = await loadSavedPlaces(res.collections.map(c => c.id)) if (cancelled) return - const merged: CollectionPlace[] = [] - for (const d of detail) { - if (!d) continue - for (const p of d.places) merged.push(p) - } setLists(res.collections.map(c => ({ id: c.id, name: c.name, color: c.color ?? null }))) setPlaces(merged) }) diff --git a/client/src/components/Collections/CollectionPlaceDetail.test.tsx b/client/src/components/Collections/CollectionPlaceDetail.test.tsx index 440097c64..9fd473998 100644 --- a/client/src/components/Collections/CollectionPlaceDetail.test.tsx +++ b/client/src/components/Collections/CollectionPlaceDetail.test.tsx @@ -233,7 +233,8 @@ describe('CollectionPlaceDetail', () => { const input = document.querySelector('input[type="file"]') as HTMLInputElement; fireEvent.change(input, { target: { files: [new File(['x'], 'cover.png', { type: 'image/png' })] } }); - await waitFor(() => expect(addToast).toHaveBeenCalledWith('Upload rejected', 'error', undefined)); + // A rejection without a server payload falls back to the localized message. + await waitFor(() => expect(addToast).toHaveBeenCalledWith('Could not upload image', 'error', undefined)); // The spinner is gone again, so the control accepts a retry. await waitFor(() => expect(document.querySelector('.animate-spin')).toBeNull()); }); diff --git a/client/src/components/Collections/CollectionPlaceDetail.tsx b/client/src/components/Collections/CollectionPlaceDetail.tsx index 0d8e3612f..42a7991da 100644 --- a/client/src/components/Collections/CollectionPlaceDetail.tsx +++ b/client/src/components/Collections/CollectionPlaceDetail.tsx @@ -15,7 +15,7 @@ import { useToast } from '../shared/Toast' import { Tooltip } from '../shared/Tooltip' import PlaceRating from '../shared/StarRating' import { normalizeImageFile } from '../../utils/convertHeic' -import { getApiErrorMessage } from '../../types' +import { getApiErrorMessage } from '../../utils/apiError' function linkHost(url: string): string { try { return new URL(url).hostname.replace(/^www\./, '') } catch { return url } diff --git a/client/src/components/Collections/ListEditorModal.test.tsx b/client/src/components/Collections/ListEditorModal.test.tsx index 7b965a37e..bac602ebd 100644 --- a/client/src/components/Collections/ListEditorModal.test.tsx +++ b/client/src/components/Collections/ListEditorModal.test.tsx @@ -322,7 +322,8 @@ describe('ListEditorModal', () => { const { props } = setup() fireEvent.change(screen.getByPlaceholderText('e.g. Tokyo 2025'), { target: { value: 'Oslo' } }) fireEvent.click(screen.getByRole('button', { name: 'Create' })) - await waitFor(() => expect(addToast).toHaveBeenCalledWith('boom', 'error', undefined)) + // A plain failure has no server-provided message, so the translated fallback shows. + await waitFor(() => expect(addToast).toHaveBeenCalledWith('Error', 'error', undefined)) expect(props.onClose).not.toHaveBeenCalled() // The save button releases again so the user can retry. await waitFor(() => expect(screen.getByRole('button', { name: 'Create' })).not.toBeDisabled()) diff --git a/client/src/components/Collections/ListEditorModal.tsx b/client/src/components/Collections/ListEditorModal.tsx index 2b0f5cf86..9b0609cb1 100644 --- a/client/src/components/Collections/ListEditorModal.tsx +++ b/client/src/components/Collections/ListEditorModal.tsx @@ -4,7 +4,7 @@ import Modal from '../shared/Modal' import { useCollectionStore } from '../../store/collectionStore' import { useToast } from '../shared/Toast' import { tripsApi } from '../../api/client' -import { getApiErrorMessage } from '../../types' +import { getApiErrorMessage } from '../../utils/apiError' import { normalizeLinkUrl } from '../../pages/collections/collectionsModel' import type { TranslationFn } from '../../types' import type { Collection, CollectionLink } from '@trek/shared' diff --git a/client/src/components/Collections/SaveToCollectionModal.test.tsx b/client/src/components/Collections/SaveToCollectionModal.test.tsx index 61e6637e5..e3487b8cc 100644 --- a/client/src/components/Collections/SaveToCollectionModal.test.tsx +++ b/client/src/components/Collections/SaveToCollectionModal.test.tsx @@ -219,7 +219,7 @@ describe('SaveToCollectionModal', () => { expect(useSaveToCollectionStore.getState().version).toBe(0); }); - it('FE-COMP-SAVETOCOL-013: a second click on the busy row is ignored while the save runs', async () => { + it('FE-COMP-SAVETOCOL-013: every row locks while a save runs', async () => { let resolve!: (v: CollectionSaveResult) => void; const save = vi.spyOn(collectionsApi, 'savePlace').mockReturnValue(new Promise(r => { resolve = r; })); openFor(); @@ -228,7 +228,9 @@ describe('SaveToCollectionModal', () => { const wishlist = await screen.findByRole('button', { name: /Wishlist/ }); fireEvent.click(wishlist); expect(wishlist).toBeDisabled(); - fireEvent.click(screen.getByRole('button', { name: /Favorites/ })); + const favorites = screen.getByRole('button', { name: /Favorites/ }); + expect(favorites).toBeDisabled(); + fireEvent.click(favorites); expect(save).toHaveBeenCalledTimes(1); resolve({}); diff --git a/client/src/components/Collections/SaveToCollectionModal.tsx b/client/src/components/Collections/SaveToCollectionModal.tsx index 521fd84d4..77d3bc476 100644 --- a/client/src/components/Collections/SaveToCollectionModal.tsx +++ b/client/src/components/Collections/SaveToCollectionModal.tsx @@ -169,7 +169,7 @@ export default function SaveToCollectionModal(): React.ReactElement | null { key={list.id} type="button" onClick={() => handleToggle(list)} - disabled={busy} + disabled={busyId != null} className={`flex items-center gap-3 px-3 py-2.5 rounded-xl border text-left transition-colors disabled:opacity-60 ${saved ? 'border-accent bg-accent-subtle' : 'border-edge bg-surface-card hover:bg-surface-hover'}`} > diff --git a/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx b/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx index 4f7f23f8b..f588d579c 100644 --- a/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx +++ b/client/src/components/Collections/SaveTripPlacesToListModal.test.tsx @@ -98,7 +98,7 @@ describe('SaveTripPlacesToListModal', () => { vi.spyOn(collectionsApi, 'list').mockResolvedValue(listResponse(many)); renderModal(); - const search = await screen.findByPlaceholderText('Search trips'); + const search = await screen.findByPlaceholderText('Search lists'); fireEvent.change(search, { target: { value: 'list 4' } }); expect(screen.getByText('List 4')).toBeInTheDocument(); expect(screen.queryByText('List 5')).not.toBeInTheDocument(); @@ -110,7 +110,7 @@ describe('SaveTripPlacesToListModal', () => { it('FE-COMP-SAVETRIPPL-007: five lists or fewer need no search box', async () => { renderModal(); await screen.findByText('Favorites'); - expect(screen.queryByPlaceholderText('Search trips')).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText('Search lists')).not.toBeInTheDocument(); }); it('FE-COMP-SAVETRIPPL-008: picking a list copies every selected place, reports it and closes', async () => { diff --git a/client/src/components/Collections/SaveTripPlacesToListModal.tsx b/client/src/components/Collections/SaveTripPlacesToListModal.tsx index 120f18f9c..3063d0d8e 100644 --- a/client/src/components/Collections/SaveTripPlacesToListModal.tsx +++ b/client/src/components/Collections/SaveTripPlacesToListModal.tsx @@ -77,7 +77,7 @@ export default function SaveTripPlacesToListModal({ isOpen, tripId, placeIds, on autoFocus value={search} onChange={e => setSearch(e.target.value)} - placeholder={t('collections.copyToTripSearch')} + placeholder={t('collections.searchLists')} className="w-full pl-8 pr-3 py-2 rounded-lg border border-edge bg-surface-input text-content text-[13px] outline-none focus:border-accent" />
diff --git a/client/src/components/Collections/ShareCollectionModal.tsx b/client/src/components/Collections/ShareCollectionModal.tsx index b9ff7868e..5ade54945 100644 --- a/client/src/components/Collections/ShareCollectionModal.tsx +++ b/client/src/components/Collections/ShareCollectionModal.tsx @@ -114,6 +114,7 @@ export default function ShareCollectionModal({ } const handleSetRole = async (userId: number, role: CollectionRole) => { + if (settingRoleId != null) return setSettingRoleId(userId) try { await setMemberRole(collectionId, userId, role) diff --git a/client/src/components/Files/FileManagerImageLightbox.test.tsx b/client/src/components/Files/FileManagerImageLightbox.test.tsx index ce25174db..e49ff1f5f 100644 --- a/client/src/components/Files/FileManagerImageLightbox.test.tsx +++ b/client/src/components/Files/FileManagerImageLightbox.test.tsx @@ -1,4 +1,4 @@ -// FE-W4LBX-001 to FE-W4LBX-017 +// FE-W4LBX-001 to FE-W4LBX-019 import { describe, it, expect, vi, beforeEach } from 'vitest' import type { TripFile } from '../../types' import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render' @@ -208,4 +208,41 @@ describe('ImageLightbox', () => { expect(onClose).not.toHaveBeenCalled() }) + + it('FE-W4LBX-018: Escape reaches the current onClose after a re-render', () => { + const first = vi.fn() + const second = vi.fn() + const { rerender } = render() + + rerender() + fireEvent.keyDown(window, { key: 'Escape' }) + + expect(second).toHaveBeenCalledOnce() + expect(first).not.toHaveBeenCalled() + }) + + it('FE-W4LBX-019: the strip mints thumbnail tokens only for thumbs in view', async () => { + // Every thumbnail costs its own one-shot token, so an off-screen thumb must + // stay quiet. The default observer stub never intersects. + const { unmount } = render( {}} />) + await waitFor(() => expect(getAuthUrl).toHaveBeenCalledWith('/f/a.jpg', 'download')) + expect(getAuthUrl).toHaveBeenCalledTimes(1) + unmount() + + getAuthUrl.mockClear() + const original = globalThis.IntersectionObserver + globalThis.IntersectionObserver = class { + constructor(private cb: IntersectionObserverCallback) {} + observe() { this.cb([{ isIntersecting: true } as IntersectionObserverEntry], this as unknown as IntersectionObserver) } + disconnect() {} + unobserve() {} + takeRecords() { return [] } + } as unknown as typeof IntersectionObserver + try { + render( {}} />) + await waitFor(() => expect(getAuthUrl).toHaveBeenCalledWith('/f/c.jpg', 'download')) + } finally { + globalThis.IntersectionObserver = original + } + }) }) diff --git a/client/src/components/Files/FileManagerImageLightbox.tsx b/client/src/components/Files/FileManagerImageLightbox.tsx index 5492d412c..0afaa127a 100644 --- a/client/src/components/Files/FileManagerImageLightbox.tsx +++ b/client/src/components/Files/FileManagerImageLightbox.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { ExternalLink, Download, X, ChevronLeft, ChevronRight, Play } from 'lucide-react' import { useTranslation } from '../../i18n' import type { TripFile } from '../../types' @@ -41,7 +41,7 @@ export function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxPro } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) - }, []) + }, [onClose]) if (!file) return null @@ -128,11 +128,30 @@ export function ImageLightbox({ files, initialIndex, onClose }: ImageLightboxPro function ThumbImg({ file, active, onClick }: { file: TripFile & { url: string }; active: boolean; onClick: () => void }) { const fileIsVideo = isVideo(file.mime_type) const [src, setSrc] = useState('') + const [visible, setVisible] = useState(false) + const ref = useRef(null) + + // Each thumbnail costs its own one-shot download token, so the strip only + // mints them for the thumbs that actually scroll into view. + useEffect(() => { + const el = ref.current + if (!el || typeof IntersectionObserver !== 'function') { setVisible(true); return } + const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVisible(true); io.disconnect() } }, { rootMargin: '200px' }) + io.observe(el) + return () => io.disconnect() + }, []) + // Videos have no stored thumbnail and can't render as an ; show a play // placeholder and don't mint a download token for them (#823). - useEffect(() => { if (!fileIsVideo) getAuthUrl(file.url, 'download').then(setSrc) }, [file.url, fileIsVideo]) + useEffect(() => { + if (!visible || fileIsVideo) return + let current = true + getAuthUrl(file.url, 'download').then(u => { if (current) setSrc(u) }) + return () => { current = false } + }, [file.url, fileIsVideo, visible]) + return ( -
`, + ">${svg}${labelHtml}
`, iconSize: [estWidth, 22], iconAnchor: [estWidth / 2, 11], popupAnchor: [0, -11], @@ -135,182 +133,18 @@ interface TransportItem { type: TransportType arcs: [number, number][][] transitSegs: TransitMapSegment[] - primaryArc: [number, number][] - fallback: [number, number] + // Route ("VIE → LHR") and duration/distance line. Computed on every update but + // not drawn since the stats badge was dropped; computeDuration still guards the + // non-finite date that used to blank the trip (#1620). mainLabel: string | null subLabel: string | null } -function buildStatsHtml(color: string, mainLabel: string | null, subLabel: string | null): { html: string; width: number; height: number } { - const estWidth = Math.max( - mainLabel ? mainLabel.length * 6.5 : 0, - subLabel ? subLabel.length * 5.5 : 0, - ) + 22 - const hasBoth = !!mainLabel && !!subLabel - const height = hasBoth ? 36 : 22 - const main = mainLabel ? `${escapeHtml(mainLabel)}` : '' - const sub = subLabel ? `${escapeHtml(subLabel)}` : '' - const html = `
${main}${sub}
` - return { html, width: estWidth, height } -} - -function StatsLabel({ item }: { item: TransportItem }) { - const map = useMap() - const markerRef = useRef(null) - const innerRef = useRef(null) - - const arc = item.primaryArc - const color = TYPE_META[item.type].color - - const { html, width, height } = useMemo(() => buildStatsHtml(color, item.mainLabel, item.subLabel), [color, item.mainLabel, item.subLabel]) - const buffer = AIRPORT_BADGE_HALF_PX + width / 2 + BADGE_GAP_PX - - const compute = () => { - if (arc.length < 2) return null - const size = map.getSize() - const pts = arc.map(p => map.latLngToContainerPoint(p as L.LatLngTuple)) - const cum: number[] = [0] - let total = 0 - for (let i = 1; i < pts.length; i++) { - total += pts[i].distanceTo(pts[i - 1]) - cum.push(total) - } - if (total <= 0) return null - - const fromPx = map.latLngToContainerPoint([item.from.lat, item.from.lng]) - const toPx = map.latLngToContainerPoint([item.to.lat, item.to.lng]) - - const isIn = (p: L.Point) => { - if (p.x < -40 || p.x > size.x + 40 || p.y < -40 || p.y > size.y + 40) return false - if (p.distanceTo(fromPx) < buffer) return false - if (p.distanceTo(toPx) < buffer) return false - return true - } - - let firstIdx = -1 - let lastIdx = -1 - for (let i = 0; i < pts.length; i++) { - if (isIn(pts[i])) { - if (firstIdx < 0) firstIdx = i - lastIdx = i - } - } - if (firstIdx < 0) { - const target = total / 2 - let sIdx = 0 - while (sIdx < cum.length - 2 && cum[sIdx + 1] < target) sIdx++ - const span = cum[sIdx + 1] - cum[sIdx] - const tm = span > 0 ? (target - cum[sIdx]) / span : 0 - const pA = pts[sIdx] - const pB = pts[sIdx + 1] - const mx = pA.x + (pB.x - pA.x) * tm - const my = pA.y + (pB.y - pA.y) * tm - const latlng = map.containerPointToLatLng([mx, my]) - let angle = Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI - if (angle > 90) angle -= 180 - if (angle < -90) angle += 180 - return { point: [latlng.lat, latlng.lng] as [number, number], angle } - } - - const bisectFraction = (a: L.Point, b: L.Point) => { - let lo = 0, hi = 1 - for (let k = 0; k < 10; k++) { - const mid = (lo + hi) / 2 - const mp = L.point(a.x + (b.x - a.x) * mid, a.y + (b.y - a.y) * mid) - if (isIn(mp)) hi = mid - else lo = mid - } - return (lo + hi) / 2 - } - - let lowCum = cum[firstIdx] - if (firstIdx > 0) { - const t = bisectFraction(pts[firstIdx - 1], pts[firstIdx]) - lowCum = cum[firstIdx - 1] + (cum[firstIdx] - cum[firstIdx - 1]) * t - } - let highCum = cum[lastIdx] - if (lastIdx < pts.length - 1) { - const t = bisectFraction(pts[lastIdx + 1], pts[lastIdx]) - highCum = cum[lastIdx] + (cum[lastIdx + 1] - cum[lastIdx]) * (1 - t) - } - - const targetLen = (lowCum + highCum) / 2 - - let segIdx = 0 - while (segIdx < cum.length - 2 && cum[segIdx + 1] < targetLen) segIdx++ - const segSpan = cum[segIdx + 1] - cum[segIdx] - const t = segSpan > 0 ? (targetLen - cum[segIdx]) / segSpan : 0 - const pA = pts[segIdx] - const pB = pts[segIdx + 1] - const px = pA.x + (pB.x - pA.x) * t - const py = pA.y + (pB.y - pA.y) * t - const latlng = map.containerPointToLatLng([px, py]) - - let angle = Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI - if (angle > 90) angle -= 180 - if (angle < -90) angle += 180 - - return { point: [latlng.lat, latlng.lng] as [number, number], angle } - } - - const apply = () => { - const pose = compute() - const marker = markerRef.current - if (!marker) return - const el = marker.getElement() as HTMLElement | null - if (!pose) { - if (el) el.style.display = 'none' - return - } - if (el) el.style.display = '' - marker.setLatLng(pose.point as L.LatLngTuple) - if (!innerRef.current && el) innerRef.current = el.querySelector('.trek-stats-inner') as HTMLElement | null - if (innerRef.current) innerRef.current.style.transform = `rotate(${pose.angle}deg)` - } - - useEffect(() => { - const icon = L.divIcon({ - className: 'trek-endpoint-stats', - html, - iconSize: [width, height], - iconAnchor: [width / 2, height / 2], - }) - const marker = L.marker([0, 0], { icon, pane: ENDPOINT_PANE, interactive: false, keyboard: false }) - marker.addTo(map) - markerRef.current = marker - innerRef.current = null - apply() - return () => { - marker.remove() - markerRef.current = null - innerRef.current = null - } - }, [map, html, width, height]) - - useMapEvents({ - move: apply, - zoom: apply, - viewreset: apply, - resize: apply, - }) - - return null -} - interface Props { reservations: Reservation[] showConnections: boolean + // Accepted for call-site compatibility only: the floating route/duration badge + // on the arc is no longer drawn, so nothing is gated on this. showStats: boolean onEndpointClick?: (reservationId: number) => void // Real road-network geometry for car/bus/taxi/bicycle bookings, keyed by @@ -318,7 +152,7 @@ interface Props { roadRoutes?: Map } -export default function ReservationOverlay({ reservations, showConnections, showStats, onEndpointClick, roadRoutes }: Props) { +export default function ReservationOverlay({ reservations, showConnections, onEndpointClick, roadRoutes }: Props) { useEndpointPane() const map = useMap() const [zoom, setZoom] = useState(() => map.getZoom()) @@ -354,12 +188,6 @@ export default function ReservationOverlay({ reservations, showConnections, show arcs.push(...segArcs) distanceKm += haversineKm([a.lat, a.lng], [b.lat, b.lng]) } - const primaryIdx = arcs.reduce((best, seg, idx, all) => seg.length > all[best].length ? idx : best, 0) - const primaryArc = arcs[primaryIdx] ?? [] - const fallback: [number, number] = primaryArc.length > 0 - ? (primaryArc[Math.floor(primaryArc.length / 2)] ?? [(from.lat + to.lat) / 2, (from.lng + to.lng) / 2]) - : [(from.lat + to.lat) / 2, (from.lng + to.lng) / 2] - const duration = computeDuration(from, to, r.reservation_time || null, r.reservation_end_time || null) const distance = `${Math.round(distanceKm)} km` // Show the full route (FRA → BER → HND) when every waypoint has a code. @@ -369,7 +197,7 @@ export default function ReservationOverlay({ reservations, showConnections, show const subParts = [duration, distance].filter(Boolean) as string[] const subLabel = subParts.length > 0 ? subParts.join(' · ') : null - out.push({ res: r, from, to, waypoints, type, arcs, transitSegs: type === 'transit' ? getTransitMapSegments(r) : [], primaryArc, fallback, mainLabel, subLabel }) + out.push({ res: r, from, to, waypoints, type, arcs, transitSegs: type === 'transit' ? getTransitMapSegments(r) : [], mainLabel, subLabel }) } return out }, [reservations]) diff --git a/client/src/components/Map/placePhoto.ts b/client/src/components/Map/placePhoto.ts index a57294624..71874b82c 100644 --- a/client/src/components/Map/placePhoto.ts +++ b/client/src/components/Map/placePhoto.ts @@ -8,3 +8,21 @@ export function isCustomPlaceImage(url: string | null | undefined): boolean { return !!url && (url.startsWith('/uploads/') || url.startsWith('data:')) } + +/** + * Cache key under which a place's auto-fetched thumbnail is stored: the provider + * id when there is one, otherwise the coordinates. A place with neither has no + * identity to cache under, so it gets the empty string — callers skip it rather + * than filing every coordinate-less place under one shared entry. + */ +export function photoCacheKey(place: { + google_place_id?: string | null + osm_id?: string | null + lat?: number | null + lng?: number | null +}): string { + if (place.google_place_id) return place.google_place_id + if (place.osm_id) return place.osm_id + if (place.lat == null || place.lng == null) return '' + return `${place.lat},${place.lng}` +} diff --git a/client/src/components/Map/reservationsMapbox.test.ts b/client/src/components/Map/reservationsMapbox.test.ts index ce02a60bb..2a7a9f39f 100644 --- a/client/src/components/Map/reservationsMapbox.test.ts +++ b/client/src/components/Map/reservationsMapbox.test.ts @@ -264,7 +264,6 @@ describe('ReservationMapboxOverlay lifecycle', () => { expect(glMarkers.every(m => m.remove.mock.calls.length > 0)).toBe(true) expect(map.listeners('zoomend')).toBe(0) expect(map.listeners('moveend')).toBe(0) - expect(map.listeners('render')).toBe(0) expect(map.removeLayer).toHaveBeenCalledWith(RESERVATION_LINE_LAYER_ID) expect(map.removeSource).toHaveBeenCalledWith(RESERVATION_SOURCE_ID) }) @@ -287,20 +286,18 @@ describe('ReservationMapboxOverlay lifecycle', () => { expect(features(map)).toHaveLength(1) }) - it('FE-COMP-RESGL-006: the redraw and rotation handlers are inert once the overlay is destroyed', () => { + it('FE-COMP-RESGL-006: the redraw handler is inert once the overlay is destroyed', () => { const map = freshMap() const overlay = attach(map) const handlerFor = (event: string) => map.on.mock.calls.find(c => c[0] === event)?.[1] as Handler const rerender = handlerFor('moveend') - const rotate = handlerFor('render') overlay.update([booking()], opts) const drawsBefore = map._sources.get(RESERVATION_SOURCE_ID)!.setData.mock.calls.length overlay.destroy() rerender() - rotate() expect(map._sources.get(RESERVATION_SOURCE_ID)).toBeUndefined() expect(drawsBefore).toBeGreaterThan(0) }) @@ -700,7 +697,7 @@ describe('ReservationMapboxOverlay transit styling', () => { }) describe('ReservationMapboxOverlay frame handler', () => { - it('FE-COMP-RESGL-042: a paint frame rotates only — it does not redraw the overlay', () => { + it('FE-COMP-RESGL-042: the overlay does not subscribe to paint frames — only camera events redraw it', () => { const map = freshMap() const overlay = attach(map) overlay.update([booking()], opts) @@ -708,6 +705,7 @@ describe('ReservationMapboxOverlay frame handler', () => { map.fire('render') + expect(map.listeners('render')).toBe(0) expect(map._sources.get(RESERVATION_SOURCE_ID)!.setData.mock.calls).toHaveLength(drawsBefore) expect(glMarkers).toHaveLength(2) expect(glMarkers.every(m => m.remove.mock.calls.length === 0)).toBe(true) diff --git a/client/src/components/Map/reservationsMapbox.ts b/client/src/components/Map/reservationsMapbox.ts index bd5c849ee..91b73e960 100644 --- a/client/src/components/Map/reservationsMapbox.ts +++ b/client/src/components/Map/reservationsMapbox.ts @@ -102,7 +102,9 @@ interface TransportItem { waypoints: ReservationEndpoint[] type: TransportType arcs: [number, number][][] - primaryArc: [number, number][] + // Route ("VIE → LHR") and duration/distance line. Computed on every update but + // not drawn since the stats badge was dropped; computeDuration still guards the + // non-finite date that used to blank the trip (#1620). mainLabel: string | null subLabel: string | null } @@ -136,8 +138,6 @@ function buildItems(reservations: Reservation[]): TransportItem[] { arcs.push(...segArcs) distanceKm += haversineKm([a.lat, a.lng], [b.lat, b.lng]) } - const primaryIdx = arcs.reduce((best, seg, idx, all) => seg.length > all[best].length ? idx : best, 0) - const primaryArc = arcs[primaryIdx] ?? [] const duration = computeDuration(from, to, r.reservation_time || null, r.reservation_end_time || null) const distance = `${Math.round(distanceKm)} km` const mainLabel = waypoints.every(w => w.code) @@ -145,7 +145,7 @@ function buildItems(reservations: Reservation[]): TransportItem[] { : (from.code && to.code ? `${from.code} → ${to.code}` : null) const subParts = [duration, distance].filter(Boolean) as string[] const subLabel = subParts.length > 0 ? subParts.join(' · ') : null - out.push({ res: r, from, to, waypoints, type, arcs, primaryArc, mainLabel, subLabel }) + out.push({ res: r, from, to, waypoints, type, arcs, mainLabel, subLabel }) } return out } @@ -165,32 +165,11 @@ function endpointMarkerHtml(type: TransportType, label: string | null): string { ">${svg}${labelHtml}` } -function buildStatsHtml(mainLabel: string | null, subLabel: string | null): { html: string; width: number; height: number } { - const estWidth = Math.max( - mainLabel ? mainLabel.length * 6.5 : 0, - subLabel ? subLabel.length * 5.5 : 0, - ) + 22 - const hasBoth = !!mainLabel && !!subLabel - const height = hasBoth ? 36 : 22 - const main = mainLabel ? `${escapeHtml(mainLabel)}` : '' - const sub = subLabel ? `${escapeHtml(subLabel)}` : '' - const html = `
${main}${sub}
` - return { html, width: estWidth, height } -} - // ── overlay manager ────────────────────────────────────────────────────── export interface ReservationOverlayOptions { showConnections: boolean + // Accepted for call-site compatibility only: the floating route/duration badge + // on the arc is no longer drawn, so nothing is gated on this. showStats: boolean showEndpointLabels: boolean onEndpointClick?: (reservationId: number) => void @@ -212,7 +191,6 @@ export class ReservationMapboxOverlay { private opts: ReservationOverlayOptions private MarkerCtor: MarkerConstructor private endpointMarkers: GlMarker[] = [] - private statsMarkers: { marker: GlMarker; arc: [number, number][] }[] = [] private rerender: () => void private destroyed = false @@ -224,7 +202,6 @@ export class ReservationMapboxOverlay { this.setupLayer() map.on('zoomend', this.rerender) map.on('moveend', this.rerender) - map.on('render', this.updateStatsRotation) } update(reservations: Reservation[], opts: ReservationOverlayOptions, roadRoutes?: Map) { @@ -238,11 +215,8 @@ export class ReservationMapboxOverlay { this.destroyed = true this.map.off('zoomend', this.rerender) this.map.off('moveend', this.rerender) - this.map.off('render', this.updateStatsRotation) this.endpointMarkers.forEach(m => m.remove()) this.endpointMarkers = [] - this.statsMarkers.forEach(s => s.marker.remove()) - this.statsMarkers = [] try { if (this.map.getLayer(RESERVATION_LINE_LAYER_ID)) this.map.removeLayer(RESERVATION_LINE_LAYER_ID) if (this.map.getSource(RESERVATION_SOURCE_ID)) this.map.removeSource(RESERVATION_SOURCE_ID) @@ -386,34 +360,7 @@ export class ReservationMapboxOverlay { } } - // Stats badge removed — the floating route/duration label on the arc is no - // longer drawn; only the connection line and the airport markers remain. - this.statsMarkers.forEach(s => s.marker.remove()) - this.statsMarkers = [] - } - - // Match the Leaflet overlay's "rotate the label along the arc" look. - // We pick a short segment straddling the arc midpoint, measure the - // screen angle between those two projected points, and clamp it to - // [-90°, 90°] so text never renders upside-down. - private updateStatsRotation = () => { - if (this.destroyed) return - for (const entry of this.statsMarkers) { - const { marker, arc } = entry - if (arc.length < 2) continue - const midIdx = Math.floor(arc.length / 2) - const a = arc[Math.max(0, midIdx - 2)]! - const b = arc[Math.min(arc.length - 1, midIdx + 2)]! - try { - const pa = this.map.project([a[1], a[0]]) - const pb = this.map.project([b[1], b[0]]) - let angle = Math.atan2(pb.y - pa.y, pb.x - pa.x) * 180 / Math.PI - if (angle > 90) angle -= 180 - if (angle < -90) angle += 180 - const el = marker.getElement() - const inner = el.querySelector('.trek-stats-inner') as HTMLElement | null - if (inner) inner.style.transform = `rotate(${angle}deg)` - } catch { /* map not ready / projection failure */ } - } + // No stats badge: the floating route/duration label on the arc was removed, + // so a rendered overlay is the connection lines plus the airport markers. } } diff --git a/client/src/components/Map/usePoiExplore.test.tsx b/client/src/components/Map/usePoiExplore.test.tsx index 741ed9169..66d0346d5 100644 --- a/client/src/components/Map/usePoiExplore.test.tsx +++ b/client/src/components/Map/usePoiExplore.test.tsx @@ -1,4 +1,4 @@ -// FE-COMP-POIEXPLORE-001 to FE-COMP-POIEXPLORE-016 +// FE-COMP-POIEXPLORE-001 to FE-COMP-POIEXPLORE-018 import React from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { act, renderHook, waitFor } from '@testing-library/react' @@ -240,4 +240,34 @@ describe('usePoiExplore', () => { expect(result.current.errorKeys.size).toBe(0) expect(result.current.pois[0].osm_id).toBe('node/9') }) + + it('FE-COMP-POIEXPLORE-017: the abandoned category stops loading when the selection moves on', async () => { + const first = deferred() + pois.mockReturnValueOnce(first.promise) + const { result } = setup() + act(() => { result.current.onViewportChange(BBOX) }) + act(() => { result.current.toggle('cafe') }) + expect(result.current.loadingKeys.has('cafe')).toBe(true) + + pois.mockResolvedValue(response([poi({ osm_id: 'node/9', category: 'bar' })])) + act(() => { result.current.toggle('bar') }) + + await act(async () => { first.reject(Object.assign(new Error('canceled'), { name: 'CanceledError' })) }) + + await waitFor(() => expect(result.current.loadingKeys.has('cafe')).toBe(false)) + expect(result.current.loadingKeys.has('bar')).toBe(false) + }) + + it('FE-COMP-POIEXPLORE-018: turning the category off stops its spinner as well', async () => { + const d = deferred() + pois.mockReturnValue(d.promise) + const { result } = setup() + act(() => { result.current.onViewportChange(BBOX) }) + act(() => { result.current.toggle('cafe') }) + act(() => { result.current.toggle('cafe') }) + + await act(async () => { d.reject(Object.assign(new Error('canceled'), { name: 'CanceledError' })) }) + + await waitFor(() => expect(result.current.loadingKeys.size).toBe(0)) + }) }) diff --git a/client/src/components/Map/usePoiExplore.ts b/client/src/components/Map/usePoiExplore.ts index 710adc757..335f3afaa 100644 --- a/client/src/components/Map/usePoiExplore.ts +++ b/client/src/components/Map/usePoiExplore.ts @@ -74,6 +74,11 @@ export function usePoiExplore() { if (abortRef.current[key] === ctrl) { setLoading(key, false) delete abortRef.current[key] + } else if (!abortRef.current[key]) { + // Cancelled with nothing taking over (toggle switched the category, or + // turned it off) — no later request will clear this key, so do it here + // instead of leaving the pill spinning forever. + setLoading(key, false) } } }, [setLoading, setError, locale]) diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index 3c4350e8f..ce03819e9 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -899,4 +899,16 @@ describe('downloadTripPDF defaults', () => { expect(endSection).toContain('reservations.span.end: Overnight Bus') expect(endSection).not.toContain('22:00') }) + + it('FE-W5PDF-027: an export without a locale still renders, with lang="en"', async () => { + await downloadTripPDF({ ...minimalArgs, locale: '' }) + + expect(srcdoc()).toContain('') + }) + + it('FE-W5PDF-028: an export without assignments renders the days as empty', async () => { + await downloadTripPDF({ ...minimalArgs, assignments: undefined as any }) + + expect(srcdoc()).toContain('Day 1') + }) }) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index c3d084a38..a39515fb6 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -146,7 +146,9 @@ interface downloadTripPDFProps { locale: string } -export async function downloadTripPDF({ trip, days, places, assignments, categories, dayNotes, reservations = [], t: _t, locale: _locale }: downloadTripPDFProps) { +// `assignments` is normalised here once — every read below (and fetchPlacePhotos) +// relies on it being an object. +export async function downloadTripPDF({ trip, days, places, assignments = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale }: downloadTripPDFProps) { await ensureRenderer() const loc = _locale || undefined const tr = _t || (k => k) @@ -165,7 +167,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const photoMap = await fetchPlacePhotos(assignments, places) const totalAssigned = new Set( - Object.values(assignments || {}).flatMap(a => a.map(x => x.place?.id)).filter(Boolean) + Object.values(assignments).flatMap(a => a.map(x => x.place?.id)).filter(Boolean) ).size // The PDF is a trip-scoped, shareable document, so totals stay in the trip's // own currency. Rates are resolved ONCE before any HTML is built so the cover @@ -173,7 +175,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor // entirely (offline export keeps working), and a failed fetch degrades to // per-currency breakdowns instead of mislabeled sums (#1561). const tripCur = (trip?.currency || 'EUR').toUpperCase() - const allCostEntries: MoneyEntry[] = Object.values(assignments || {}) + const allCostEntries: MoneyEntry[] = Object.values(assignments) .flatMap(a => a) .map(a => ({ amount: Number(a.place?.price) || 0, currency: a.place?.currency || tripCur })) const needsFx = allCostEntries.some(e => e.amount > 0 && e.currency.toUpperCase() !== tripCur) @@ -433,7 +435,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ` const html = ` - + diff --git a/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx b/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx index 7c5f701a8..ebc2edbb2 100644 --- a/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx +++ b/client/src/components/Packing/PackingListPanelCategoryGroup.test.tsx @@ -229,24 +229,28 @@ describe('KategorieGruppe — bulk actions', () => { await waitFor(() => expect(puts).toEqual([1])) }) - it('FE-W5CAT-012: a throwing toggle during Check All surfaces a save error', async () => { - useTripStore.setState({ togglePackingItem: async () => { throw new Error('offline') } }) + it('FE-W5CAT-012: a failing PUT during Check All is reported by the store, once', async () => { + server.use(http.put('/api/trips/1/packing/:itemId', () => new HttpResponse(null, { status: 500 }))) + seedStore(useTripStore, { packingItems: [buildPackingItem({ id: 1, name: 'Tent', checked: 0 })] }) const { container } = setup({ items: [buildPackingItem({ id: 1, name: 'Tent', checked: 0 })] }) openCategoryMenu(container) fireEvent.click(screen.getByText('Check All')) - await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Failed to save', 'error', undefined)) + await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Error updating item', 'error', undefined)) + expect(toastSpy).toHaveBeenCalledTimes(1) }) - it('FE-W5CAT-013: a throwing toggle during Uncheck All surfaces a save error', async () => { - useTripStore.setState({ togglePackingItem: async () => { throw new Error('offline') } }) + it('FE-W5CAT-013: a failing PUT during Uncheck All is reported by the store, once', async () => { + server.use(http.put('/api/trips/1/packing/:itemId', () => new HttpResponse(null, { status: 500 }))) + seedStore(useTripStore, { packingItems: [buildPackingItem({ id: 1, name: 'Tent', checked: 1 })] }) const { container } = setup({ items: [buildPackingItem({ id: 1, name: 'Tent', checked: 1 })] }) openCategoryMenu(container) fireEvent.click(screen.getByText('Uncheck All')) - await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Failed to save', 'error', undefined)) + await waitFor(() => expect(toastSpy).toHaveBeenCalledWith('Error updating item', 'error', undefined)) + expect(toastSpy).toHaveBeenCalledTimes(1) }) it('FE-W5CAT-014: Delete List hands the whole category to the panel and closes the menu', async () => { diff --git a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx index 66145b21f..5579f0955 100644 --- a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx +++ b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx @@ -98,19 +98,17 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen catch { toast.error(t('packing.toast.renameError')) } } + // togglePackingItem rolls its own optimistic update back and reports the + // failure itself, so the bulk actions just drive it item by item. const handleCheckAll = async () => { - try { - for (const item of Array.from(items)) { - if (!item.checked) await togglePackingItem(tripId, item.id, true) - } - } catch { toast.error(t('packing.toast.saveError')) } + for (const item of Array.from(items)) { + if (!item.checked) await togglePackingItem(tripId, item.id, true) + } } const handleUncheckAll = async () => { - try { - for (const item of Array.from(items)) { - if (item.checked) await togglePackingItem(tripId, item.id, false) - } - } catch { toast.error(t('packing.toast.saveError')) } + for (const item of Array.from(items)) { + if (item.checked) await togglePackingItem(tripId, item.id, false) + } } const handleDeleteAll = async () => { await onDeleteAll(items) @@ -304,7 +302,8 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen placeholder={t('packing.addItemPlaceholder')} style={{ flex: 1, padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)', fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontFamily: 'inherit', outline: 'none', color: 'var(--text-primary)', background: 'var(--bg-input)' }} /> - ) : (
diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index 191601a42..1b99364d9 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -497,20 +497,6 @@ function Chip({ icon, text, color = 'var(--text-secondary)', bg = 'var(--bg-hove ) } -interface RowProps { - icon: React.ReactNode - children: React.ReactNode -} - -function Row({ icon, children }: RowProps) { - return ( -
-
{icon}
-
{children}
-
- ) -} - interface ActionButtonProps { onClick: () => void variant: 'primary' | 'ghost' | 'danger' diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index d1c1eed5f..8163dfde7 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -214,19 +214,6 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p // eslint-disable-next-line react-hooks/exhaustive-deps }, [reservation, prefill, isOpen, selectedDayId, defaultAssignmentId, days, places, accommodations]) - // Re-hydrate hotel day range when the accommodations prop arrives after the modal opens - // (race: tripAccommodations fetch may complete after isOpen fires, leaving hotel fields empty) - useEffect(() => { - if (!isOpen || !reservation || reservation.type !== 'hotel' || !reservation.accommodation_id) return - const acc = accommodations.find(a => a.id == reservation.accommodation_id) - if (!acc) return - setForm(prev => { - if (prev.hotel_place_id !== '' || prev.hotel_start_day !== '' || prev.hotel_end_day !== '') return prev - const accPlace = places.find(p => p.id == acc.place_id) - return { ...prev, hotel_place_id: acc.place_id, hotel_start_day: acc.start_day_id, hotel_end_day: acc.end_day_id, hotel_address: accPlace?.address || prev.hotel_address } - }) - }, [accommodations, isOpen, reservation, places]) - const set = (field, value) => setForm(prev => ({ ...prev, [field]: value })) const toggleTraveler = (id: number) => setTravelerIds(prev => { @@ -807,7 +794,6 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p } function formatDate(dateStr, locale) { - if (!dateStr) return '' const d = new Date(dateStr + 'T00:00:00Z') return d.toLocaleDateString(locale || undefined, { day: 'numeric', month: 'short', timeZone: 'UTC' }) } diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index 340201735..1ce7e4f01 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -90,7 +90,6 @@ interface ReservationCardProps { } function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateToFiles, assignmentLookup, canEdit, days = [], contributions = [], detailPlugins = [] }: ReservationCardProps) { - const { toggleReservationStatus } = useTripStore() const toast = useToast() const { t, locale } = useTranslation() const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h' @@ -117,10 +116,6 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo const linked = r.assignment_id ? assignmentLookup[r.assignment_id] : null const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) - const handleToggle = async () => { - try { await toggleReservationStatus(tripId, r.id) } - catch { toast.error(t('reservations.toast.updateError')) } - } const handleDelete = async () => { setShowDeleteConfirm(false) try { await onDelete(r.id) } catch { toast.error(t('reservations.toast.deleteError')) } diff --git a/client/src/components/Planner/TransitSearchPanel.test.tsx b/client/src/components/Planner/TransitSearchPanel.test.tsx index a9c003afb..3f4bdde6c 100644 --- a/client/src/components/Planner/TransitSearchPanel.test.tsx +++ b/client/src/components/Planner/TransitSearchPanel.test.tsx @@ -456,7 +456,7 @@ describe('TransitSearchPanel', () => { }) const mixed = { ...ITINERARY, - legs: [leg('BUS', 'B1'), leg('TRAM', 'M4'), leg('FERRY', 'F10'), leg('FUNICULAR', 'FU'), leg('AERIAL_LIFT', 'AL'), leg('HIGHSPEED_RAIL', 'ICE 599')], + legs: [leg('BUS', 'B1'), leg('TRAM', 'M4'), leg('FERRY', 'F10'), leg('FUNICULAR', 'FU'), leg('AERIAL_LIFT', 'AL'), leg('SUBWAY', 'U2'), leg('HIGHSPEED_RAIL', 'ICE 599')], } transitApiMock.plan.mockResolvedValueOnce({ itineraries: [mixed] }) render() @@ -469,9 +469,11 @@ describe('TransitSearchPanel', () => { expect(icon('F10')).toContain('lucide-sailboat') expect(icon('FU')).toContain('lucide-cable-car') expect(icon('AL')).toContain('lucide-cable-car') - // Anything else (rail and friends) falls through to lucide's `Train`, which - // is an alias of TramFront — so rail and tram currently share a glyph. - expect(icon('ICE 599')).toContain('lucide-tram-front') + expect(icon('U2')).toContain('lucide-train-front-tunnel') + // Anything else (rail and friends) falls through to TrainFront, which stays + // distinct from the tram glyph. + expect(icon('ICE 599')).toContain('lucide-train-front') + expect(icon('ICE 599')).not.toContain('tunnel') }) it('FE-PLANNER-TRANSIT-026: a leg without a usable timestamp renders an empty time cell, not "Invalid Date"', async () => { diff --git a/client/src/components/Planner/TransitSearchPanel.tsx b/client/src/components/Planner/TransitSearchPanel.tsx index a1cfce9e1..f610935a1 100644 --- a/client/src/components/Planner/TransitSearchPanel.tsx +++ b/client/src/components/Planner/TransitSearchPanel.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import tzlookup from 'tz-lookup' -import { ArrowLeftRight, ArrowRight, Bus, CableCar, ChevronDown, ChevronUp, Clock, Footprints, MapPin, Sailboat, Search, Train, TramFront, TrainFront } from 'lucide-react' +import { ArrowLeftRight, ArrowRight, Bus, CableCar, ChevronDown, ChevronUp, Clock, Footprints, MapPin, Sailboat, Search, TramFront, TrainFront, TrainFrontTunnel } from 'lucide-react' import CustomTimePicker from '../shared/CustomTimePicker' import { TransitMetaBadges } from './transitDisplay' import { transitApi } from '../../api/client' @@ -39,22 +39,24 @@ export interface PickedPlace { name: string; lat: number; lng: number } // ── helpers ────────────────────────────────────────────────────────────────── const MODE_GROUPS: { key: string; labelKey: string; Icon: React.ComponentType<{ size?: number; strokeWidth?: number }>; modes: string }[] = [ - { key: 'rail', labelKey: 'transit.mode.rail', Icon: Train, modes: 'HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN' }, - { key: 'subway', labelKey: 'transit.mode.subway', Icon: TrainFront, modes: 'SUBWAY' }, + // lucide's `Train` is an alias of TramFront, so rail keeps TrainFront and the + // subway takes the tunnel variant — otherwise the chips share a glyph. + { key: 'rail', labelKey: 'transit.mode.rail', Icon: TrainFront, modes: 'HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN' }, + { key: 'subway', labelKey: 'transit.mode.subway', Icon: TrainFrontTunnel, modes: 'SUBWAY' }, { key: 'tram', labelKey: 'transit.mode.tram', Icon: TramFront, modes: 'TRAM' }, { key: 'bus', labelKey: 'transit.mode.bus', Icon: Bus, modes: 'BUS,COACH' }, { key: 'ferry', labelKey: 'transit.mode.ferry', Icon: Sailboat, modes: 'FERRY' }, { key: 'cable', labelKey: 'transit.mode.cable', Icon: CableCar, modes: 'FUNICULAR,AERIAL_LIFT' }, ] +// Only called for non-WALK legs — walking renders its own Footprints inline. function legIcon(mode: string) { - if (mode === 'WALK') return Footprints if (mode === 'BUS' || mode === 'COACH') return Bus if (mode === 'TRAM') return TramFront - if (mode === 'SUBWAY') return TrainFront + if (mode === 'SUBWAY') return TrainFrontTunnel if (mode === 'FERRY') return Sailboat if (mode === 'FUNICULAR' || mode === 'AERIAL_LIFT') return CableCar - return Train + return TrainFront } function tzAt(lat: number, lng: number): string { diff --git a/client/src/components/Planner/usePlacesSidebar.ts b/client/src/components/Planner/usePlacesSidebar.ts index 6dbd04b41..65e904c3c 100644 --- a/client/src/components/Planner/usePlacesSidebar.ts +++ b/client/src/components/Planner/usePlacesSidebar.ts @@ -58,7 +58,6 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { const collectionsEnabled = useAddonStore((s) => s.isEnabled('collections')) // Places-API enrichment (#886) needs a Google Maps key; gate the toggle on it. const canEnrichImport = useAuthStore((s) => s.hasMapsKey) - const isNaverListImportEnabled = true const [fileImportOpen, setFileImportOpen] = useState(false) const [sidebarDropFile, setSidebarDropFile] = useState(null) @@ -106,19 +105,13 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { const [listImportLoading, setListImportLoading] = useState(false) const [listImportProvider, setListImportProvider] = useState<'google' | 'naver'>('google') const [listImportEnrich, setListImportEnrich] = useState(false) - const availableListImportProviders: Array<'google' | 'naver'> = isNaverListImportEnabled ? ['google', 'naver'] : ['google'] + const availableListImportProviders: Array<'google' | 'naver'> = ['google', 'naver'] const hasMultipleListImportProviders = availableListImportProviders.length > 1 - useEffect(() => { - if (!isNaverListImportEnabled && listImportProvider === 'naver') { - setListImportProvider('google') - } - }, [isNaverListImportEnabled, listImportProvider]) - const handleListImport = async () => { if (!listImportUrl.trim()) return setListImportLoading(true) - const provider = listImportProvider === 'naver' && isNaverListImportEnabled ? 'naver' : 'google' + const provider = listImportProvider try { const enrich = listImportEnrich && canEnrichImport const result = provider === 'google' @@ -186,7 +179,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { if (next.has(catId)) next.delete(catId); else next.add(catId) setCategoryFilters(next) } - const [dayPickerPlace, setDayPickerPlace] = useState(null) + const [dayPickerPlace, setDayPickerPlace] = useState(null) const [catDropOpen, setCatDropOpen] = useState(false) const [mobileShowDays, setMobileShowDays] = useState(false) diff --git a/client/src/components/Settings/AccountTab.tsx b/client/src/components/Settings/AccountTab.tsx index 84fc7d87d..59c601394 100644 --- a/client/src/components/Settings/AccountTab.tsx +++ b/client/src/components/Settings/AccountTab.tsx @@ -4,6 +4,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { useTranslation } from '../../i18n' import { useAuthStore } from '../../store/authStore' import { useToast } from '../shared/Toast' +import { escapeHtml } from '@trek/shared' import { authApi, adminApi } from '../../api/client' import { getApiErrorMessage } from '../../types' import type { UserWithOidc } from '../../types' @@ -106,7 +107,7 @@ export default function AccountTab(): React.ReactElement { if (!backupCodesText) return const html = `TREK MFA Backup Codes -

TREK MFA Backup Codes

${new Date().toLocaleString()}

${backupCodesText}
` +

TREK MFA Backup Codes

${escapeHtml(new Date().toLocaleString())}

${escapeHtml(backupCodesText)}
` const w = window.open('', '_blank', 'width=900,height=700') if (!w) return w.document.open() diff --git a/client/src/components/Settings/MapSettingsTab.tsx b/client/src/components/Settings/MapSettingsTab.tsx index 36601e362..995014770 100644 --- a/client/src/components/Settings/MapSettingsTab.tsx +++ b/client/src/components/Settings/MapSettingsTab.tsx @@ -196,7 +196,6 @@ export default function MapSettingsTab(): React.ReactElement { setSaving(true) try { const glStyle = provider === 'leaflet' ? mapboxStyle : normalizeStyleForProvider(provider, mapboxStyle) - setMapboxStyle(glStyle) // Save into the active provider's own slot so the other provider's style survives. const stylePatch = provider === 'maplibre-gl' ? { maplibre_style: glStyle } : { mapbox_style: glStyle } await updateSettings({ @@ -207,6 +206,8 @@ export default function MapSettingsTab(): React.ReactElement { mapbox_3d_enabled: mapbox3d, mapbox_quality_mode: mapboxQuality, }) + // Only mirror the normalized style into the form once it is actually persisted. + setMapboxStyle(glStyle) toast.success(t('settings.toast.mapSaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) diff --git a/client/src/components/Settings/NotificationsTab.tsx b/client/src/components/Settings/NotificationsTab.tsx index 7f6d7e80b..35f0d22f0 100644 --- a/client/src/components/Settings/NotificationsTab.tsx +++ b/client/src/components/Settings/NotificationsTab.tsx @@ -165,9 +165,9 @@ export default function NotificationsTab(): React.ReactElement { await settingsApi.setBulk({ ntfy_topic: ntfyTopic, ntfy_server: ntfyServer, - ...(ntfyToken && ntfyToken !== '••••••••' ? { ntfy_token: ntfyToken } : {}), + ...(ntfyToken ? { ntfy_token: ntfyToken } : {}), }) - if (ntfyToken && ntfyToken !== '••••••••') setNtfyTokenIsSet(true) + if (ntfyToken) setNtfyTokenIsSet(true) toast.success(t('settings.ntfyUrl.saved')) } catch { toast.error(t('common.error')) @@ -194,7 +194,9 @@ export default function NotificationsTab(): React.ReactElement { const result = await notificationsApi.testNtfy({ topic: ntfyTopic, server: ntfyServer || null, - token: ntfyToken && ntfyToken !== '••••••••' ? ntfyToken : null, + // A stored token is only masked in the placeholder, never in state — sending + // null makes the server fall back to the saved one. + token: ntfyToken || null, }) if (result.success) toast.success(t('settings.ntfyUrl.testSuccess')) else toast.error(result.error || t('settings.ntfyUrl.testFailed')) diff --git a/client/src/components/Settings/PasskeysSection.tsx b/client/src/components/Settings/PasskeysSection.tsx index a221ab6dd..a6db0329b 100644 --- a/client/src/components/Settings/PasskeysSection.tsx +++ b/client/src/components/Settings/PasskeysSection.tsx @@ -63,8 +63,9 @@ export default function PasskeysSection({ demoMode }: { demoMode?: boolean }): R const canAdd = enabled && configured + // Both step-up flows are gated by disabled={busy || !pwd} on their submit button, + // so the password is always present by the time these run. const handleAdd = async () => { - if (!addPwd) { toast.error(t('settings.passkey.passwordRequired')); return } setBusy(true) try { const options = await authApi.passkey.registerOptions(addPwd) @@ -94,7 +95,6 @@ export default function PasskeysSection({ demoMode }: { demoMode?: boolean }): R } const handleDelete = async (id: number) => { - if (!deletePwd) { toast.error(t('settings.passkey.passwordRequired')); return } setBusy(true) try { await authApi.passkey.delete(id, deletePwd) diff --git a/client/src/components/Settings/PhotoProvidersSection.tsx b/client/src/components/Settings/PhotoProvidersSection.tsx index bd1659a7a..c36fce449 100644 --- a/client/src/components/Settings/PhotoProvidersSection.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.tsx @@ -192,8 +192,11 @@ export default function PhotoProvidersSection(): React.ReactElement { if (!testPath) return setProviderTesting(prev => ({ ...prev, [provider.id]: true })) try { - const payload = buildProviderPayload(provider) - const res = cfg.test_post ? await apiClient.post(testPath, payload) : await apiClient.get(testPath) + // Only a POST probe carries the form values. A provider that declares just a + // GET (test_get / status_get) is probed against its saved credentials. + const res = cfg.test_post + ? await apiClient.post(testPath, buildProviderPayload(provider)) + : await apiClient.get(testPath) const ok = !!res.data?.connected setProviderConnected(prev => ({ ...prev, [provider.id]: ok })) if (ok) { diff --git a/client/src/components/SystemNotices/SystemNoticeBanner.tsx b/client/src/components/SystemNotices/SystemNoticeBanner.tsx index 36c3a8de4..5bfb41774 100644 --- a/client/src/components/SystemNotices/SystemNoticeBanner.tsx +++ b/client/src/components/SystemNotices/SystemNoticeBanner.tsx @@ -1,10 +1,9 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Info, AlertTriangle, AlertOctagon, X } from 'lucide-react'; import { useSystemNoticeStore } from '../../store/systemNoticeStore.js'; import type { SystemNoticeDTO } from '../../store/systemNoticeStore.js'; import { useTranslation } from '../../i18n/index.js'; -import { isRtlLanguage } from '../../i18n/index.js'; import { runNoticeAction } from './noticeActions.js'; const SEVERITY_ICONS: Record = { @@ -43,39 +42,37 @@ const SEVERITY = { interface BannerItemProps { notice: SystemNoticeDTO; onDismiss: () => void; - language: string; } function CTALink({ notice, + cta, label, onDismiss, }: { notice: SystemNoticeDTO; + cta: NonNullable; label: string; onDismiss: () => void; }) { const navigate = useNavigate(); function handleClick() { - if (!notice.cta) return; - if (notice.cta.kind === 'nav') { - navigate(notice.cta.href); + if (cta.kind === 'nav') { + navigate(cta.href); if (notice.dismissible) onDismiss(); - } else if (notice.cta.kind === 'link') { - window.open(notice.cta.href, '_blank', 'noopener,noreferrer'); + } else if (cta.kind === 'link') { + window.open(cta.href, '_blank', 'noopener,noreferrer'); } else { - runNoticeAction(notice.cta.actionId, { navigate }); - if (notice.cta.dismissOnAction !== false) onDismiss(); + runNoticeAction(cta.actionId, { navigate }); + if (cta.dismissOnAction !== false) onDismiss(); } } - if (!notice.cta) return null; - - if (notice.cta.kind === 'nav' || notice.cta.kind === 'link') { + if (cta.kind === 'nav' || cta.kind === 'link') { return ( { e.preventDefault(); handleClick(); }} className="underline hover:no-underline font-medium ml-3 shrink-0" > @@ -94,7 +91,7 @@ function CTALink({ ); } -function BannerItem({ notice, onDismiss, language }: BannerItemProps) { +function BannerItem({ notice, onDismiss }: BannerItemProps) { const { t } = useTranslation(); const s = SEVERITY[notice.severity] ?? SEVERITY.info; const title = t(notice.titleKey); @@ -121,7 +118,7 @@ function BannerItem({ notice, onDismiss, language }: BannerItemProps) { {body} )} {ctaLabel && notice.cta && ( - + )}
{notice.dismissible && ( @@ -140,10 +137,9 @@ function BannerItem({ notice, onDismiss, language }: BannerItemProps) { interface AnimatedBannerItemProps { notice: SystemNoticeDTO; onDismiss: () => void; - language: string; } -function AnimatedBannerItem({ notice, onDismiss, language }: AnimatedBannerItemProps) { +function AnimatedBannerItem({ notice, onDismiss }: AnimatedBannerItemProps) { const [mounted, setMounted] = useState(false); const prefersReducedMotion = typeof window !== 'undefined' && @@ -164,7 +160,7 @@ function AnimatedBannerItem({ notice, onDismiss, language }: AnimatedBannerItemP return (
- +
); } @@ -175,7 +171,6 @@ interface BannerRendererProps { export function BannerRenderer({ notices }: BannerRendererProps) { const { dismiss } = useSystemNoticeStore(); - const { language } = useTranslation(); const containerRef = useRef(null); // Show at most 2 highest-priority banners @@ -209,7 +204,6 @@ export function BannerRenderer({ notices }: BannerRendererProps) { dismiss(notice.id)} - language={language} /> ))} diff --git a/client/src/components/SystemNotices/SystemNoticeModal.test.tsx b/client/src/components/SystemNotices/SystemNoticeModal.test.tsx index e301473a8..74ac18938 100644 --- a/client/src/components/SystemNotices/SystemNoticeModal.test.tsx +++ b/client/src/components/SystemNotices/SystemNoticeModal.test.tsx @@ -1326,7 +1326,7 @@ describe('ModalRenderer', () => { expect(dismissSpy).not.toHaveBeenCalled(); }); - it('FE-SN-MODAL-056: pager buttons of the side slots cannot page past the ends', async () => { + it('FE-SN-MODAL-056: the side slots are inert previews, and their pagers cannot page past the ends', async () => { stubMatchMedia({ mobile: true }); const notices = [ makeNotice({ id: 'n1', titleKey: 'Notice A' }), @@ -1336,6 +1336,14 @@ describe('ModalRenderer', () => { render(); await flushGraceDelay(); + // the side slots carry a second copy of the pager and title, so they are + // kept out of the a11y tree and the tab order + const { prevSlot, nextSlot } = sheetParts(); + expect(prevSlot.hasAttribute('inert')).toBe(true); + expect(prevSlot.getAttribute('aria-hidden')).toBe('true'); + expect(nextSlot.hasAttribute('inert')).toBe(true); + expect(nextSlot.getAttribute('aria-hidden')).toBe('true'); + // the next slot renders page 2's pager, whose "previous" button is enabled — // pressing it must not move the sheet below the first page const prevButtons = screen.getAllByLabelText('Previous notice') as HTMLButtonElement[]; diff --git a/client/src/components/SystemNotices/SystemNoticeModal.tsx b/client/src/components/SystemNotices/SystemNoticeModal.tsx index a6c797eb4..293f247f2 100644 --- a/client/src/components/SystemNotices/SystemNoticeModal.tsx +++ b/client/src/components/SystemNotices/SystemNoticeModal.tsx @@ -77,7 +77,6 @@ interface ContentProps { titleId: string; bodyId: string; isDark: boolean; - onDismiss: () => void; onDismissAll: () => void; onCTA: () => void; onSecondaryCTA: () => void; @@ -90,7 +89,7 @@ interface ContentProps { onGoto: (i: number) => void; } -function NoticeContent({ notice, title, body, ctaLabel, secondaryCtaLabel, titleId, bodyId, isDark, onDismiss, onDismissAll, onCTA, onSecondaryCTA, total, currentPage, canPage, onPrev, onNext, onGoto }: ContentProps) { +function NoticeContent({ notice, title, body, ctaLabel, secondaryCtaLabel, titleId, bodyId, isDark, onDismissAll, onCTA, onSecondaryCTA, total, currentPage, canPage, onPrev, onNext, onGoto }: ContentProps) { const { t } = useTranslation(); const isLastPage = total <= 1 || currentPage === total - 1; @@ -383,9 +382,6 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { // Sheet scroll offset at the moment the touch began — used to suppress dismiss-drag // when the user is scrolled into content and pans down to scroll back up. const scrollTopAtTouchStart = useRef(0); - // Keep a ref to the current notice id so dismiss/CTA handlers see the latest value - const noticeIdRef = useRef(null); - noticeIdRef.current = notice?.id ?? null; // Page-slide animation refs. // isPageNavRef: set to true just before a user-initiated page change so the @@ -532,27 +528,13 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { ); } - // Dismiss current notice. The store removes it from the array, and the next - // notice naturally shifts into notices[idx]. The clamp effect handles the - // edge case where idx was pointing at the last item. - function handleDismissById(id: string) { - setVisible(false); - dismiss(id); - } - - function handleDismiss() { - const id = noticeIdRef.current; - if (id) handleDismissById(id); - } - // Dismiss every notice in the current modal list — used by the X button and ESC. function handleDismissAll() { setVisible(false); notices.forEach(n => dismiss(n.id)); } - function runCta(cta: SystemNoticeDTO['cta']) { - if (!cta) { handleDismissAll(); return; } + function runCta(cta: NonNullable) { if (cta.kind === 'nav') { navigate(cta.href); if (notice?.dismissible !== false) handleDismissAll(); @@ -565,8 +547,9 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { if (cta.dismissOnAction !== false) handleDismissAll(); } } - function handleCTA() { runCta(notice?.cta); } - function handleSecondaryCTA() { runCta(notice?.secondaryCta); } + // Both buttons only render when the notice carries the matching cta. + function handleCTA() { runCta(notice!.cta!); } + function handleSecondaryCTA() { runCta(notice!.secondaryCta!); } function animatedDismissAll() { const sheet = sheetRef.current; @@ -626,7 +609,7 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) { notice, canPage, isLastPage, language, t, dur, ease, touchStartX, touchStartY, dragLockRef, scrollTopAtTouchStart, isPageNavRef, stripRef, sheetRef, prevSlotRef, contentWrapperRef, nextSlotRef, - announceIndex, handleDismiss, handleDismissAll, handleCTA, handleSecondaryCTA, animatedDismissAll, + announceIndex, handleDismissAll, handleCTA, handleSecondaryCTA, animatedDismissAll, handlePrev, handleNext, handleGoto, }; } @@ -635,7 +618,7 @@ type NoticeState = ReturnType; // Build the NoticeContent props for a given notice + pager slot index. function makeContentProps(S: NoticeState, n: SystemNoticeDTO, slotIdx: number): ContentProps { - const { t, isDark, canPage, notices, handleDismiss, handleDismissAll, handleCTA, handleSecondaryCTA, handlePrev, handleNext, handleGoto } = S; + const { t, isDark, canPage, notices, handleDismissAll, handleCTA, handleSecondaryCTA, handlePrev, handleNext, handleGoto } = S; const rawBody = t(n.bodyKey); const body = n.bodyParams ? Object.entries(n.bodyParams).reduce( @@ -652,7 +635,6 @@ function makeContentProps(S: NoticeState, n: SystemNoticeDTO, slotIdx: number): titleId: `notice-title-${n.id}`, bodyId: `notice-body-${n.id}`, isDark, - onDismiss: handleDismiss, onDismissAll: handleDismissAll, onCTA: handleCTA, onSecondaryCTA: handleSecondaryCTA, @@ -823,13 +805,16 @@ function MobileNoticeSheet(S: NoticeState) { ref={stripRef} style={{ display: 'flex', width: '300%', height: '100%', alignItems: 'stretch', transform: 'translateX(-33.333%)' }} > -
+ {/* The side slots are drag previews only: their pager and CTA are a + second copy of the centre ones, so they stay out of the a11y tree + and the tab order until they become the centre slot. */} +
-
+
diff --git a/client/src/components/Todo/TodoListPanel.tsx b/client/src/components/Todo/TodoListPanel.tsx index 482a14e9a..146526e77 100644 --- a/client/src/components/Todo/TodoListPanel.tsx +++ b/client/src/components/Todo/TodoListPanel.tsx @@ -22,6 +22,53 @@ import TodoRow from './TodoRow' import { usePluginViewContributions, PluginCardFooter } from '../Plugins/PluginContributions' import EmptyState from '../shared/EmptyState' +// Sidebar filter row. Declared at module level so React keeps the same component +// type across renders — inline it and every re-render remounts the buttons, +// which drops clicks that are already in flight. +function SidebarItem({ id, icon: Icon, label, count, color, active, compact, onSelect }: { + id: string + icon: any + label: string + count: number + color?: string + active: boolean + compact: boolean + onSelect: (id: string) => void +}) { + return ( + + ) +} + export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tripId: number; items: TodoItem[]; addItemSignal?: number }) { // Layout component: state/effects/derived/handlers live in useTodoList. const { @@ -62,39 +109,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr reorderTodoItems(tripId, globalIds) } - // Sidebar filter item - const SidebarItem = ({ id, icon: Icon, label, count, color }: { id: string; icon: any; label: string; count: number; color?: string }) => ( - - ) + const selectFilter = (id: string) => setFilter(id as FilterType) // Filter title const filterTitle = (() => { @@ -138,10 +153,10 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr {!isMobile &&
{t('todo.sidebar.tasks')}
} - !i.checked).length} /> - - - + !i.checked).length} active={filter === 'all'} compact={isMobile} onSelect={selectFilter} /> + + + {/* Sort by */} {!isMobile &&
@@ -169,7 +184,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
} {isMobile &&
} {categories.map(cat => ( - + ))} {canEdit && ( diff --git a/client/src/components/Todo/useTodoList.ts b/client/src/components/Todo/useTodoList.ts index 1ccd1f7c0..afda45b4b 100644 --- a/client/src/components/Todo/useTodoList.ts +++ b/client/src/components/Todo/useTodoList.ts @@ -71,7 +71,8 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu let result: TodoItem[] if (filter === 'all') result = items.filter(i => !i.checked) else if (filter === 'done') result = items.filter(i => !!i.checked) - else if (filter === 'my') result = items.filter(i => !i.checked && i.assigned_user_id === currentUserId) + // No resolved user means nothing is "mine" — matching the myCount badge. + else if (filter === 'my') result = currentUserId ? items.filter(i => !i.checked && i.assigned_user_id === currentUserId) : [] else if (filter === 'overdue') result = items.filter(i => !i.checked && i.due_date && i.due_date < today) else result = items.filter(i => i.category === filter) if (sortByPrio) result = [...result].sort((a, b) => { diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx index 641df712b..fa28e3447 100644 --- a/client/src/components/Trips/TripFormModal.tsx +++ b/client/src/components/Trips/TripFormModal.tsx @@ -37,7 +37,7 @@ interface CoverSearchPhoto { export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUpdate }: TripFormModalProps) { const isEditing = !!trip - const fileRef = useRef(null) + const fileRef = useRef(null) const coverSearchSeq = useRef(0) const toast = useToast() const { t } = useTranslation() @@ -202,7 +202,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp } } - const handleCoverSelect = async (file) => { + const handleCoverSelect = async (file: File | null | undefined) => { if (!file) return // HEIC/HEIF from iOS can't be rendered or stored as-is — convert to JPEG first const normalized = await normalizeImageFile(file) @@ -217,12 +217,12 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp } } - const handleCoverChange = (e) => { - handleCoverSelect((e.target as HTMLInputElement).files?.[0]) + const handleCoverChange = (e: React.ChangeEvent) => { + handleCoverSelect(e.target.files?.[0]) e.target.value = '' } - const uploadCoverNow = async (file) => { + const uploadCoverNow = async (file: File) => { setUploadingCover(true) try { const fd = new FormData() @@ -291,7 +291,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp setCoverPreview(null) return } - if (!trip?.id) return + // Nothing pending left, so the preview is a saved trip's stored cover. try { await tripsApi.update(trip.id, { cover_image: null }) setCoverPreview(null) @@ -613,8 +613,7 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp {selectedMembers.length > 0 && (
{selectedMembers.map(uid => { - const user = allUsers.find(u => u.id === uid) - if (!user) return null + const user = allUsers.find(u => u.id === uid)! return ( setSelectedMembers(prev => prev.filter(id => id !== uid))} className="bg-surface-secondary text-content border border-edge cursor-pointer" @@ -633,7 +632,6 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp { - if (!value) return if (isEditing && trip?.id) { const user = allUsers.find(u => u.id === Number(value)) if (user) { diff --git a/client/src/components/Trips/TripMembersModal.test.tsx b/client/src/components/Trips/TripMembersModal.test.tsx index 0e8042e8d..47db5a5c9 100644 --- a/client/src/components/Trips/TripMembersModal.test.tsx +++ b/client/src/components/Trips/TripMembersModal.test.tsx @@ -869,6 +869,23 @@ describe('TripMembersModal', () => { await waitFor(() => expect(screen.queryByDisplayValue('Granny')).not.toBeInTheDocument()); }); + it('FE-COMP-MEMBERS-048b: Enter followed by a blur renames only once', async () => { + const user = userEvent.setup(); + mockRoster([guestRow]); + let renames = 0; + server.use(http.put('/api/trips/1/guests/:userId', () => { renames++; return HttpResponse.json({ success: true }); })); + render(); + + await user.click(await screen.findByTitle('Rename')); + const input = screen.getByDisplayValue('Grandma'); + fireEvent.change(input, { target: { value: 'Granny' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + fireEvent.blur(input); + + await waitFor(() => expect(screen.queryByDisplayValue('Granny')).not.toBeInTheDocument()); + expect(renames).toBe(1); + }); + it('FE-COMP-MEMBERS-049: Escape cancels a rename and a blank name saves nothing', async () => { const user = userEvent.setup(); mockRoster([guestRow]); diff --git a/client/src/components/Trips/TripMembersModal.tsx b/client/src/components/Trips/TripMembersModal.tsx index d9c32d8bd..b834e2127 100644 --- a/client/src/components/Trips/TripMembersModal.tsx +++ b/client/src/components/Trips/TripMembersModal.tsx @@ -202,7 +202,6 @@ function TripInviteLinkSection({ tripId, t }: { tripId: number; t: (key: string, } const copy = () => { - if (!inviteUrl) return navigator.clipboard.writeText(inviteUrl) setCopied(true) if (copyTimerRef.current) clearTimeout(copyTimerRef.current) @@ -324,7 +323,6 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o } const handleAdd = async () => { - if (!selectedUserId) return setAdding(true) try { const target = allUsers.find(u => String(u.id) === String(selectedUserId)) @@ -370,7 +368,14 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o } } + // Enter commits, and the blur that follows would send the same rename a second + // time. Only the first commit per editing session goes through; a failed one + // reopens the gate so the user can retry from the still-open input. + const renameCommittedRef = useRef(false) + const handleRenameGuest = async (userId) => { + if (renameCommittedRef.current) return + renameCommittedRef.current = true const name = renameValue.trim() if (!name) { setRenamingGuestId(null); return } try { @@ -378,6 +383,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o setRenamingGuestId(null) await loadMembers(true) } catch (err: unknown) { + renameCommittedRef.current = false toast.error(getApiErrorMessage(err, t('members.guestRenameError'))) } } @@ -572,7 +578,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o autoFocus value={renameValue} onChange={e => setRenameValue(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleRenameGuest(g.id); if (e.key === 'Escape') setRenamingGuestId(null) }} + onKeyDown={e => { if (e.key === 'Enter') handleRenameGuest(g.id); if (e.key === 'Escape') { renameCommittedRef.current = true; setRenamingGuestId(null) } }} onBlur={() => handleRenameGuest(g.id)} maxLength={50} className="bg-surface border border-edge text-content" @@ -589,7 +595,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle, o {isCurrentOwner && renamingGuestId !== g.id && ( <> + ) +} + // Dev-only notification testing panel, re-skinned to the mobile admin system. // All state, fetches and fire() payloads are ported verbatim from the desktop // DevNotificationsPanel — only the presentation layer changes. @@ -50,8 +86,8 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { try { await adminApi.sendTestNotification(payload) toast.success(`Sent: ${label}`) - } catch (err: any) { - toast.error(err.message || 'Failed') + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : 'Failed') } finally { setSending(null) } @@ -62,53 +98,6 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { const username = user?.username || 'Admin' const tripTitle = selectedTrip?.title || 'Test Trip' - // ── Helpers ────────────────────────────────────────────────────────────── - - const Btn = ({ - id, label, sub, icon: Icon, color, onClick, - }: { - id: string; label: string; sub: string; icon: LucideIcon; color: string; onClick: () => void - }) => ( - - ) - - const TripSelector = () => ( - - ) - - const UserSelector = () => ( - - ) - return (
{/* Dev header */} @@ -128,7 +117,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { hint="Test how each in-app notification type renders, sent to yourself." />
- fire('simple-me', { event: 'test_simple', scope: 'user', @@ -136,7 +125,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: {}, })} /> - fire('boolean-me', { event: 'test_boolean', scope: 'user', @@ -149,7 +138,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { }, })} /> - fire('navigate-me', { event: 'test_navigate', scope: 'user', @@ -157,7 +146,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: {}, })} /> - fire('simple-admins', { event: 'test_simple', scope: 'admin', @@ -176,9 +165,15 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { hint="Fires each trip event to all members of the selected trip (excluding yourself)." />
- +
- selectedTripId && fire('booking_change', { event: 'booking_change', scope: 'trip', @@ -186,7 +181,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { actor: username, trip: tripTitle, booking: 'Test Hotel', type: 'hotel', tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('trip_reminder', { event: 'trip_reminder', scope: 'trip', @@ -194,7 +189,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { trip: tripTitle, tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('photos_shared', { event: 'photos_shared', scope: 'trip', @@ -202,7 +197,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { actor: username, trip: tripTitle, count: '5', tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('collab_message', { event: 'collab_message', scope: 'trip', @@ -210,7 +205,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { params: { actor: username, trip: tripTitle, preview: 'This is a test message preview.', tripId: String(selectedTripId) }, })} /> - selectedTripId && fire('packing_tagged', { event: 'packing_tagged', scope: 'trip', @@ -231,9 +226,16 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement { hint="Fires each user event to the selected recipient." />
- +
- fire('version_available', { event: 'version_available', scope: 'admin', diff --git a/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx b/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx index 1b03a6ee4..034c0565d 100644 --- a/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminGitHubPanel.tsx @@ -6,6 +6,7 @@ import { MAdminButton, MAdminCard } from './MAdminUi' const REPO = 'mauriceboe/TREK' const PER_PAGE = 10 +const MAX_PAGES_PER_LOAD = 5 interface GithubRelease { id: number @@ -39,27 +40,49 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea const [hasMore, setHasMore] = useState(true) const [loadingMore, setLoadingMore] = useState(false) - const fetchReleases = async (pageNum = 1, append = false) => { + const isShown = (release: GithubRelease) => isPrerelease || !release.prerelease + + const fetchPage = async (pageNum: number) => { try { const res = await apiClient.get(`/admin/github-releases`, { params: { per_page: PER_PAGE, page: pageNum } }) - const data = Array.isArray(res.data) ? res.data : [] - setReleases(prev => append ? [...prev, ...data] : data) - setHasMore(data.length === PER_PAGE) + return Array.isArray(res.data) ? res.data as GithubRelease[] : [] } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Unknown error') + return null } } + // Keep pulling pages until at least one release survives the prerelease filter, + // otherwise a page of nothing but prereleases leaves an empty timeline behind a + // "Load more" button. MAX_PAGES_PER_LOAD bounds the walk. + const loadFrom = async (startPage: number, append: boolean) => { + const collected: GithubRelease[] = [] + let pageNum = startPage + let more = true + + for (let i = 0; i < MAX_PAGES_PER_LOAD; i++) { + const data = await fetchPage(pageNum) + if (!data) return + collected.push(...data) + more = data.length === PER_PAGE + if (!more || collected.some(isShown)) break + pageNum += 1 + } + + setReleases(prev => append ? [...prev, ...collected] : collected) + setHasMore(more) + setPage(pageNum) + setError(null) + } + useEffect(() => { setLoading(true) - fetchReleases(1).finally(() => setLoading(false)) + loadFrom(1, false).finally(() => setLoading(false)) }, []) const handleLoadMore = async () => { - const next = page + 1 setLoadingMore(true) - await fetchReleases(next, true) - setPage(next) + await loadFrom(page + 1, true) setLoadingMore(false) } @@ -152,7 +175,7 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea { href: 'https://github.com/mauriceboe/TREK/wiki', color: '#6366f1', icon: , title: 'Wiki', sub: t('settings.about.wikiHint') }, ] - const shownReleases = isPrerelease ? releases : releases.filter(r => !r.prerelease) + const shownReleases = releases.filter(isShown) return (
@@ -188,7 +211,7 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea
- ) : error ? ( + ) : error && releases.length === 0 ? (

{t('admin.github.error')}

@@ -301,6 +324,13 @@ export default function MAdminGitHubPanel({ isPrerelease = false }: { isPrerelea
+ {/* A failed "Load more" keeps the timeline and stays retryable */} + {error && ( +

+ {t('admin.github.error')} — {error} +

+ )} + {/* Load more */} {hasMore && (
diff --git a/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx b/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx index 0c35dca5b..f08873d5f 100644 --- a/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminMcpTokensPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { Key, Trash2, User, Loader2, Shield } from 'lucide-react' import { adminApi } from '../../../api/client' import { useToast } from '../../../components/shared/Toast' @@ -52,15 +52,20 @@ export default function MAdminMcpTokensPanel() { const toast = useToast() const { t, locale } = useTranslation() + // The loader runs once, but a language switch while the requests are in flight + // must still toast in the current locale — hence the ref instead of the closure. + const latest = useRef({ t, toast }) + latest.current = { t, toast } + useEffect(() => { adminApi.oauthSessions() .then(d => setSessions(d.sessions || [])) - .catch(() => toast.error(t('admin.oauthSessions.loadError'))) + .catch(() => latest.current.toast.error(latest.current.t('admin.oauthSessions.loadError'))) .finally(() => setSessionsLoading(false)) adminApi.mcpTokens() .then(d => setTokens(d.tokens || [])) - .catch(() => toast.error(t('admin.mcpTokens.loadError'))) + .catch(() => latest.current.toast.error(latest.current.t('admin.mcpTokens.loadError'))) .finally(() => setTokensLoading(false)) }, []) diff --git a/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx b/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx index 6abc10eba..9655bde49 100644 --- a/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx +++ b/client/src/mobile/screens/admin/MAdminNotificationsSection.tsx @@ -119,7 +119,6 @@ export default function MAdminNotificationsSection({ admin, t }: MAdminNotificat const testAdminWebhook = async () => { const url = smtpValues.admin_webhook_url === '••••••••' ? undefined : smtpValues.admin_webhook_url - if (!url && smtpValues.admin_webhook_url !== '••••••••') return try { if (url) await authApi.updateAppSettings({ admin_webhook_url: url }).catch(() => {}) const result = await notificationsApi.testWebhook(url) diff --git a/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx b/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx index 88cd93df5..091556361 100644 --- a/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx +++ b/client/src/mobile/screens/admin/MAdminNotifyMatrix.tsx @@ -77,16 +77,18 @@ export default function MAdminNotifyMatrix({ t, toast }: { t: TranslationFn; toa key={ch} className="w-[44px] flex-none text-center font-geist text-[0.5625rem] font-bold uppercase tracking-[0.04em] text-m-faint" > - {t(ADMIN_CHANNEL_LABEL_KEYS[ch]) || ch} + {t(ADMIN_CHANNEL_LABEL_KEYS[ch])} ))}
{matrix.event_types.map((eventType) => { const implemented = matrix.implemented_combos[eventType] ?? [] + // Only version_available has a label key so far; anything else keeps its raw id. + const labelKey = ADMIN_EVENT_LABEL_KEYS[eventType] return (
- {t(ADMIN_EVENT_LABEL_KEYS[eventType]) || eventType} + {labelKey ? t(labelKey) : eventType} {visibleChannels.map((ch) => ( diff --git a/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx b/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx index e33c59374..6b352827c 100644 --- a/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx +++ b/client/src/mobile/screens/admin/MAdminPackingTemplateManager.tsx @@ -162,7 +162,7 @@ export default function MAdminPackingTemplateManager() { await adminApi.deleteTemplateCategory(expandedId, catId) setCategories(prev => prev.filter(c => c.id !== catId)) setItems(prev => prev.filter(i => i.category_id !== catId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.toast.deleteError')) } } // Item CRUD @@ -190,7 +190,7 @@ export default function MAdminPackingTemplateManager() { try { await adminApi.deleteTemplateItem(expandedId, itemId) setItems(prev => prev.filter(i => i.id !== itemId)) - } catch { toast.error(t('admin.packingTemplates.deleteError')) } + } catch { toast.error(t('admin.toast.deleteError')) } } return ( diff --git a/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx b/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx index cac92beb1..bee362cb0 100644 --- a/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx +++ b/client/src/mobile/screens/admin/MAdminPluginsPanel.tsx @@ -107,6 +107,8 @@ interface RegistryDetail extends RegistryItem { icon: string | null requiredAddons?: string[] pluginDependencies?: PluginDep[] + /** Display slice of the manifest's capabilities — drives the same chips as an installed row. */ + capabilities?: { widget?: { slot?: string }; tripPage?: { replaces?: string[] } } } | null } @@ -309,13 +311,8 @@ const CHIP_PENDING = 'text-[color:var(--m-st-pending)] border-[color:color-mix(i const PENDING_CARD = 'border-[color:color-mix(in_srgb,var(--m-st-pending)_28%,transparent)] bg-[color:color-mix(in_srgb,var(--m-st-pending)_10%,transparent)]' const ACT_PILL = 'inline-flex flex-none items-center justify-center gap-[5px] whitespace-nowrap rounded-full px-3 py-[7px] text-[0.6875rem] font-bold bg-m-act text-m-actfg disabled:opacity-50' -function ReviewedBadge({ t, compact }: { t: T; compact?: boolean }) { - if (compact) return - return ( - - {t('admin.plugins.reviewed')} - - ) +function ReviewedBadge({ t }: { t: T }) { + return } /** Marks a manually-uploaded (sideloaded) plugin: no registry, unsigned, not reviewed. */ @@ -533,7 +530,12 @@ export default function MAdminPluginsPanel() { if (!runtimeOn || !Array.from(e.dataTransfer.types).includes('Files')) return e.preventDefault(); dragDepth.current++; setDragActive(true) } - const onDragLeave = () => { if (--dragDepth.current <= 0) { dragDepth.current = 0; setDragActive(false) } } + // Mirrors onDragEnter: a leave that had no matching enter (a non-file drag) is ignored + // instead of driving the depth counter negative. + const onDragLeave = () => { + if (dragDepth.current === 0) return + if (--dragDepth.current === 0) setDragActive(false) + } const onDrop = (e: DragEvent) => { e.preventDefault(); dragDepth.current = 0; setDragActive(false) if (!runtimeOn) return @@ -1146,7 +1148,7 @@ function InstalledRow({ p, t, busy, hasUpdate, latestVer, blocked, onToggle, onU
{p.name} {p.version && v{p.version}} - {p.reviewed_at && } + {p.reviewed_at && } {p.source_repo === 'local:upload' && } {p.source_repo === 'local:link' && } {/* Registry plugins only — a sideloaded/dev-linked plugin already says something @@ -1395,7 +1397,7 @@ function PluginDetailSheet({ item, installed, busy, onInstall, onClose, t, local }, [item.id]) const manifest = detail?.manifest ?? null - const caps = manifest ? deriveCaps(manifest.permissions, {}, t) : [] + const caps = manifest ? deriveCaps(manifest.permissions, manifest.capabilities ?? {}, t) : [] const repoUrl = `https://github.com/${item.repo}` const homepage = item.homepage && /^https?:\/\//i.test(item.homepage) && item.homepage !== repoUrl ? item.homepage : null const sizeKb = detail?.size ? Math.max(1, Math.round(detail.size / 1024)) : null @@ -1421,7 +1423,7 @@ function PluginDetailSheet({ item, installed, busy, onInstall, onClose, t, local

{item.name}

- {item.reviewedAt && } + {item.reviewedAt && }

{item.author}{item.latest ? ` · v${item.latest}` : ''}

diff --git a/client/src/mobile/screens/admin/MAdminSheets.tsx b/client/src/mobile/screens/admin/MAdminSheets.tsx index 8bb1bfbd4..e3e743d6d 100644 --- a/client/src/mobile/screens/admin/MAdminSheets.tsx +++ b/client/src/mobile/screens/admin/MAdminSheets.tsx @@ -210,7 +210,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {

{updateInfo?.is_docker === false ? (