fix(client): the audit round over the 4.3.1 release

Ride minutes and late marks follow an edited timetable instead of the routing
that fetched them; a landing day's join band reads as the ride it is rather
than a nine hour drive of no length; the refuel offer is there on the road
out of a terminal or a pick-up desk; the phone's stage map draws the ride;
the share page numbers its stops the way the planner does; the PDF lists a
day the way the day plan lists it, from both shells; a poster-less clip draws
its play badge everywhere, not only in the editor; the hero title has room
for its descenders and the list card keeps its badge clear; the release
notice keeps its close control in reach when it stacks; a link chip's actions
are thumb-sized and its delete asks first; the Google retry line and the
Google-only switch appear only where Google answers.
This commit is contained in:
Maurice
2026-09-22 12:10:21 +02:00
parent cb85ba915c
commit 85b5756e89
78 changed files with 2476 additions and 486 deletions
+33 -2
View File
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import { offersGoogleRetry, sourceLabelFor } from './placeSource'
import { googleHoldsSlot, offersGoogleRetry, placesGoogleOnlyHint, selectGoogleHoldsSlot, sourceLabelFor } from './placeSource'
describe('offersGoogleRetry', () => {
it('FE-PLACESOURCE-001: offers Google for a list the index or OpenStreetMap answered, only where a key exists', () => {
it('FE-PLACESOURCE-001: offers Google for a list the index or OpenStreetMap answered, only where Google answers', () => {
expect(offersGoogleRetry('trek-places+openstreetmap', true)).toBe(true)
expect(offersGoogleRetry('openstreetmap', true)).toBe(true)
expect(offersGoogleRetry('trek-places+plugin', true)).toBe(true)
@@ -17,6 +17,37 @@ describe('offersGoogleRetry', () => {
})
})
describe('googleHoldsSlot', () => {
it('FE-PLACESOURCE-004: a key holds the slot under auto and google, not under Amap or OpenStreetMap', () => {
expect(googleHoldsSlot(true, 'auto')).toBe(true)
expect(googleHoldsSlot(true, 'google')).toBe(true)
// A stored key still serves photos and details here, but the server never
// walks the Google chain for a search, so a link that offered it would be dead.
expect(googleHoldsSlot(true, 'amap')).toBe(false)
expect(googleHoldsSlot(true, 'openstreetmap')).toBe(false)
expect(googleHoldsSlot(false, 'google')).toBe(false)
// Unrecognised reads as auto, as the server reads it.
expect(googleHoldsSlot(true, '')).toBe(true)
})
it('FE-PLACESOURCE-005: the store selector asks the same question of the auth state', () => {
expect(selectGoogleHoldsSlot({ hasMapsKey: true, placesProvider: 'auto' })).toBe(true)
expect(selectGoogleHoldsSlot({ hasMapsKey: true, placesProvider: 'openstreetmap' })).toBe(false)
expect(selectGoogleHoldsSlot({ hasMapsKey: false, placesProvider: 'auto' })).toBe(false)
})
})
describe('placesGoogleOnlyHint', () => {
it('FE-PLACESOURCE-006: names what the admin has to do before the switch does anything', () => {
expect(placesGoogleOnlyHint(false, 'auto')).toBe('admin.placesGoogleOnly.missingKey')
expect(placesGoogleOnlyHint(false, 'openstreetmap')).toBe('admin.placesGoogleOnly.missingKey')
expect(placesGoogleOnlyHint(true, 'amap')).toBe('admin.placesGoogleOnly.otherProvider')
expect(placesGoogleOnlyHint(true, 'openstreetmap')).toBe('admin.placesGoogleOnly.otherProvider')
expect(placesGoogleOnlyHint(true, 'auto')).toBe('admin.placesGoogleOnly.subtitle')
expect(placesGoogleOnlyHint(true, 'google')).toBe('admin.placesGoogleOnly.subtitle')
})
})
describe('sourceLabelFor', () => {
it('FE-PLACESOURCE-003: a row names its own index, else the list it came from', () => {
const t = (k: string) => k
+46 -15
View File
@@ -32,6 +32,52 @@ const SOURCE_KEYS: Record<string, string> = {
amap: 'places.source.amap',
}
/**
* Whether a search can reach Google on this install at all.
*
* A stored key is not enough. The server walks the Google key chain only while
* the admin's provider choice leaves Google the keyed slot, which is `auto` or
* `google`; under Amap or OpenStreetMap the key still serves photos and
* details, but a search sent to Google is answered by the index as before.
* Anything unrecognised reads as `auto`, the way the server reads it.
*/
export function googleHoldsSlot(hasMapsKey: boolean, placesProvider: string): boolean {
return hasMapsKey && placesProvider !== 'amap' && placesProvider !== 'openstreetmap'
}
/** The same question asked of the auth store, for `useAuthStore(selectGoogleHoldsSlot)`. */
export const selectGoogleHoldsSlot = (s: { hasMapsKey: boolean; placesProvider: string }): boolean =>
googleHoldsSlot(s.hasMapsKey, s.placesProvider)
/**
* Whether a result list can be sent to Google instead.
*
* The index and OpenStreetMap answer first, and Google is only asked when they
* find nothing, so a list that has the wrong place on it never reaches Google
* on its own. The link that sends it there is offered when Google holds the
* keyed slot (googleHoldsSlot above) and this list did not already come from
* Google: a list Google produced, alone or as the empty-case fallback, has
* nowhere further to go, and the offline cache is not a search at all.
*/
export function offersGoogleRetry(listSource: string, googleAnswers: boolean): boolean {
if (!googleAnswers || !listSource) return false
return !listSource.split('+').includes('google') && listSource !== 'offline-cache'
}
/**
* The line under the admin's "Search with Google only" switch.
*
* Two ways the switch can be a promise the search cannot keep, and they ask
* different things of the admin: paste a key, or hand the keyed slot back to
* Google. The subtitle that says what the switch does is only true of the
* third state.
*/
export function placesGoogleOnlyHint(hasMapsKey: boolean, placesProvider: string): string {
if (!hasMapsKey) return 'admin.placesGoogleOnly.missingKey'
if (!googleHoldsSlot(hasMapsKey, placesProvider)) return 'admin.placesGoogleOnly.otherProvider'
return 'admin.placesGoogleOnly.subtitle'
}
/**
* The label for one row.
*
@@ -40,21 +86,6 @@ const SOURCE_KEYS: Record<string, string> = {
* answered the call: Google never marks its places, and a merged list marks only
* the index side, so an unmarked row in one is OpenStreetMap by elimination.
*/
/**
* Whether a result list can be sent to Google instead.
*
* The index and OpenStreetMap answer first, and Google is only asked when they
* find nothing, so a list that has the wrong place on it never reaches Google
* on its own. The link that sends it there is offered when the instance has a
* Google key and this list did not already come from Google: a list Google
* produced, alone or as the empty-case fallback, has nowhere further to go, and
* the offline cache is not a search at all.
*/
export function offersGoogleRetry(listSource: string, hasMapsKey: boolean): boolean {
if (!hasMapsKey || !listSource) return false
return !listSource.split('+').includes('google') && listSource !== 'offline-cache'
}
export function sourceLabelFor(place: unknown, listSource: string, t: TranslationFn): string | null {
const own = (place as { source?: unknown } | null)?.source
const source = typeof own === 'string' && own