From 6c83cd7d36c0579dfa683c9286cfc56d1ce5cf28 Mon Sep 17 00:00:00 2001 From: elpatron Date: Fri, 5 Jun 2026 19:52:33 +0200 Subject: [PATCH] feat: differentiate weather fetch errors by cause --- client/src/components/LiveLogView.tsx | 28 ++++++++++---- client/src/components/LogEntryEditor.tsx | 28 ++++++++++---- client/src/i18n/locales/da.json | 3 ++ client/src/i18n/locales/de.json | 3 ++ client/src/i18n/locales/en.json | 3 ++ client/src/i18n/locales/nb.json | 3 ++ client/src/i18n/locales/sv.json | 3 ++ client/src/services/weather.test.ts | 47 ++++++++++++++++++++++++ client/src/services/weather.ts | 18 +++++++-- 9 files changed, 119 insertions(+), 17 deletions(-) diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index 9fc1b93..7544dbe 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -713,13 +713,27 @@ export default function LiveLogView({ { analyticsSource: 'live_log' } ) } catch (err) { - if (err instanceof WeatherApiError && err.code === 'OFFLINE') { - void showAlert(t('logs.weather_offline'), t('logs.live_weather_owm_btn')) - return - } - if (err instanceof WeatherApiError && err.code === 'NO_KEY') { - void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn')) - return + if (err instanceof WeatherApiError) { + if (err.code === 'OFFLINE') { + void showAlert(t('logs.weather_offline'), t('logs.live_weather_owm_btn')) + return + } + if (err.code === 'NO_KEY') { + void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn')) + return + } + if (err.code === 'UNAUTHORIZED') { + void showAlert(t('settings.weather_unauthorized'), t('logs.live_weather_owm_btn')) + return + } + if (err.code === 'NOT_FOUND') { + void showAlert(t('settings.weather_not_found'), t('logs.live_weather_owm_btn')) + return + } + if (err.code === 'BAD_REQUEST') { + void showAlert(t('settings.weather_bad_request'), t('logs.live_weather_owm_btn')) + return + } } console.error('Live log OWM weather failed:', err) void showAlert(t('settings.weather_error'), t('logs.live_weather_owm_btn')) diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index 7733869..4c9be29 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -1178,13 +1178,27 @@ export default function LogEntryEditor({ showAlert(t('settings.weather_success')) } catch (err) { - if (err instanceof WeatherApiError && err.code === 'OFFLINE') { - showAlert(t('logs.weather_offline')) - return - } - if (err instanceof WeatherApiError && err.code === 'NO_KEY') { - showAlert(t('settings.no_key')) - return + if (err instanceof WeatherApiError) { + if (err.code === 'OFFLINE') { + showAlert(t('logs.weather_offline')) + return + } + if (err.code === 'NO_KEY') { + showAlert(t('settings.no_key')) + return + } + if (err.code === 'UNAUTHORIZED') { + showAlert(t('settings.weather_unauthorized')) + return + } + if (err.code === 'NOT_FOUND') { + showAlert(t('settings.weather_not_found')) + return + } + if (err.code === 'BAD_REQUEST') { + showAlert(t('settings.weather_bad_request')) + return + } } console.error('Weather prefilling failed:', err) showAlert(t('settings.weather_error')) diff --git a/client/src/i18n/locales/da.json b/client/src/i18n/locales/da.json index 289d016..2bc837f 100644 --- a/client/src/i18n/locales/da.json +++ b/client/src/i18n/locales/da.json @@ -790,6 +790,9 @@ "no_key": "Ingen OpenWeatherMap API-nøgle tilgængelig. Gem din egen nøgle i brugerprofilen, eller kontakt operatøren.", "weather_success": "Vejrdata hentet med succes!", "weather_error": "Hentning af vejrdata mislykkedes. Tjek API-nøglen og forbindelsen.", + "weather_unauthorized": "Hentning af vejrdata mislykkedes. API-nøglen er ugyldig eller ikke autoriseret.", + "weather_not_found": "Hentning af vejrdata mislykkedes. Den angivne placering eller koordinater blev ikke fundet.", + "weather_bad_request": "Hentning af vejrdata mislykkedes. Ingen placering eller GPS-position blev angivet.", "weather_date_mismatch": "Vejrdata kan kun hentes for i dag ({{today}}). Dette logbogsindlæg er dateret {{date}}.", "gps_error": "Indtast en placering, eller find GPS-koordinaterne.", "share_title": "Del logbog (skrivebeskyttet)", diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index 5c6ae2a..b938cc4 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -790,6 +790,9 @@ "no_key": "Kein OpenWeatherMap-API-Schlüssel verfügbar. Hinterlege einen eigenen Schlüssel im Benutzerprofil oder kontaktiere den Betreiber.", "weather_success": "Wetterdaten erfolgreich abgerufen!", "weather_error": "Wetterdatenabruf fehlgeschlagen. Überprüfe den API-Schlüssel und die Verbindung.", + "weather_unauthorized": "Wetterdatenabruf fehlgeschlagen. Der API-Schlüssel ist ungültig oder nicht autorisiert.", + "weather_not_found": "Wetterdatenabruf fehlgeschlagen. Der angegebene Ort oder die Koordinaten wurden nicht gefunden.", + "weather_bad_request": "Wetterdatenabruf fehlgeschlagen. Es wurde kein Ort und keine GPS-Position angegeben.", "weather_date_mismatch": "Wetterdaten können nur für den heutigen Tag ({{today}}) abgerufen werden. Dieser Logbucheintrag ist auf den {{date}} datiert.", "gps_error": "Bitte gib einen Ort an oder ermittle die GPS-Koordinaten.", "share_title": "Logbuch teilen (Schreibgeschützt)", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index 2a32916..1340d29 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -790,6 +790,9 @@ "no_key": "No OpenWeatherMap API key available. Add your own key in your user profile or contact the operator.", "weather_success": "Weather details fetched successfully!", "weather_error": "Failed to fetch weather. Check your API key and connection.", + "weather_unauthorized": "Failed to fetch weather. The API key is invalid or unauthorized.", + "weather_not_found": "Failed to fetch weather. The specified location or coordinates were not found.", + "weather_bad_request": "Failed to fetch weather. No location or GPS position was specified.", "weather_date_mismatch": "Weather data can only be fetched for today ({{today}}). This logbook entry is dated {{date}}.", "gps_error": "Please enter a location or fetch GPS coordinates first.", "share_title": "Share Logbook (Read-Only)", diff --git a/client/src/i18n/locales/nb.json b/client/src/i18n/locales/nb.json index b01f7ff..669768f 100644 --- a/client/src/i18n/locales/nb.json +++ b/client/src/i18n/locales/nb.json @@ -790,6 +790,9 @@ "no_key": "Ingen OpenWeatherMap API-nøkkel tilgjengelig. Lagre din egen nøkkel i brukerprofilen, eller kontakt operatøren.", "weather_success": "Værdata vellykket hentet!", "weather_error": "Henting av værdata mislyktes. Kontroller API-nøkkelen og tilkoblingen.", + "weather_unauthorized": "Henting av værdata mislyktes. API-nøkkelen er ugyldig eller ikke autorisert.", + "weather_not_found": "Henting av værdata mislyktes. Den angitte posisjonen eller koordinatene ble ikke funnet.", + "weather_bad_request": "Henting av værdata mislyktes. Ingen posisjon eller GPS-koordinater ble angitt.", "weather_date_mismatch": "Værdata kan bare hentes ut for i dag ({{today}}). Denne loggbokoppføringen er datert {{date}}.", "gps_error": "Vennligst skriv inn en posisjon eller finn GPS-koordinatene.", "share_title": "Del loggbok (skrivebeskyttet)", diff --git a/client/src/i18n/locales/sv.json b/client/src/i18n/locales/sv.json index 7f7641c..179fbbf 100644 --- a/client/src/i18n/locales/sv.json +++ b/client/src/i18n/locales/sv.json @@ -790,6 +790,9 @@ "no_key": "Ingen OpenWeatherMap API-nyckel tillgänglig. Spara din egen nyckel i användarprofilen eller kontakta operatören.", "weather_success": "Väderdata har hämtats framgångsrikt!", "weather_error": "Hämtning av väderdata misslyckades. Kontrollera API-nyckeln och anslutningen.", + "weather_unauthorized": "Hämtning av väderdata misslyckades. API-nyckeln är ogiltig eller inte auktoriserad.", + "weather_not_found": "Hämtning av väderdata misslyckades. Den angivna platsen eller koordinaterna hittades inte.", + "weather_bad_request": "Hämtning av väderdata misslyckades. Ingen plats eller GPS-position angavs.", "weather_date_mismatch": "Väderdata kan endast hämtas för idag ({{today}}). Denna loggbokspost är daterad {{date}}.", "gps_error": "Ange en plats eller bestäm GPS-koordinaterna.", "share_title": "Aktieloggbok (skrivskyddad)", diff --git a/client/src/services/weather.test.ts b/client/src/services/weather.test.ts index a7b6685..f792209 100644 --- a/client/src/services/weather.test.ts +++ b/client/src/services/weather.test.ts @@ -69,4 +69,51 @@ describe('fetchOpenWeatherCurrent', () => { expect(trackPlausibleEvent).not.toHaveBeenCalled() }) + + it('throws UNAUTHORIZED when status is 401', async () => { + apiFetch.mockResolvedValue({ + ok: false, + status: 401, + json: async () => ({ error: 'Unauthorized' }) + }) + + const { fetchOpenWeatherCurrent, WeatherApiError } = await import('./weather.js') + const err = await fetchOpenWeatherCurrent({ lat: '54', lon: '10' }).catch((e) => e) + expect(err).toBeInstanceOf(WeatherApiError) + expect((err as any).code).toBe('UNAUTHORIZED') + }) + + it('throws NOT_FOUND when status is 404', async () => { + apiFetch.mockResolvedValue({ + ok: false, + status: 404, + json: async () => ({ error: 'Not Found' }) + }) + + const { fetchOpenWeatherCurrent, WeatherApiError } = await import('./weather.js') + const err = await fetchOpenWeatherCurrent({ lat: '54', lon: '10' }).catch((e) => e) + expect(err).toBeInstanceOf(WeatherApiError) + expect((err as any).code).toBe('NOT_FOUND') + }) + + it('throws BAD_REQUEST when status is 400', async () => { + apiFetch.mockResolvedValue({ + ok: false, + status: 400, + json: async () => ({ error: 'Bad Request' }) + }) + + const { fetchOpenWeatherCurrent, WeatherApiError } = await import('./weather.js') + const err = await fetchOpenWeatherCurrent({ lat: '54', lon: '10' }).catch((e) => e) + expect(err).toBeInstanceOf(WeatherApiError) + expect((err as any).code).toBe('BAD_REQUEST') + }) + + it('throws BAD_REQUEST when coordinates or query are missing', async () => { + const { fetchOpenWeatherCurrent, WeatherApiError } = await import('./weather.js') + const err = await fetchOpenWeatherCurrent({}).catch((e) => e) + expect(err).toBeInstanceOf(WeatherApiError) + expect((err as any).code).toBe('BAD_REQUEST') + expect(apiFetch).not.toHaveBeenCalled() + }) }) diff --git a/client/src/services/weather.ts b/client/src/services/weather.ts index 9b3e55c..c16fcbb 100644 --- a/client/src/services/weather.ts +++ b/client/src/services/weather.ts @@ -7,9 +7,12 @@ import { } from './analytics.js' export class WeatherApiError extends Error { - code: 'NO_KEY' | 'OFFLINE' | 'REQUEST_FAILED' + code: 'NO_KEY' | 'OFFLINE' | 'REQUEST_FAILED' | 'UNAUTHORIZED' | 'NOT_FOUND' | 'BAD_REQUEST' - constructor(message: string, code: 'NO_KEY' | 'OFFLINE' | 'REQUEST_FAILED' = 'REQUEST_FAILED') { + constructor( + message: string, + code: 'NO_KEY' | 'OFFLINE' | 'REQUEST_FAILED' | 'UNAUTHORIZED' | 'NOT_FOUND' | 'BAD_REQUEST' = 'REQUEST_FAILED' + ) { super(message) this.name = 'WeatherApiError' this.code = code @@ -38,7 +41,7 @@ export async function fetchOpenWeatherCurrent( } else if (params.q?.trim()) { searchParams.set('q', params.q.trim()) } else { - throw new WeatherApiError('lat/lon or location query required') + throw new WeatherApiError('lat/lon or location query required', 'BAD_REQUEST') } const userKey = getOwmApiKeyForActiveUser().trim() @@ -65,6 +68,15 @@ export async function fetchOpenWeatherCurrent( if (res.status === 503) { throw new WeatherApiError('No OpenWeatherMap API key configured', 'NO_KEY') } + if (res.status === 401) { + throw new WeatherApiError('Invalid OpenWeatherMap API key', 'UNAUTHORIZED') + } + if (res.status === 404) { + throw new WeatherApiError('Location or coordinates not found', 'NOT_FOUND') + } + if (res.status === 400) { + throw new WeatherApiError('Invalid or missing location parameters', 'BAD_REQUEST') + } const data = await res.json() if (!res.ok) {