Compare commits

...

5 Commits

12 changed files with 153 additions and 24 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.1.21
0.1.1.23
+32 -5
View File
@@ -45,11 +45,38 @@ export default function CreatorAvatar({
let photo: string | null = null
let role = ''
if (creatorId && crewSnapshotsById && crewSnapshotsById[creatorId]) {
const snap = crewSnapshotsById[creatorId]
name = snap.name || ''
photo = snap.photo || null
role = snap.role || ''
if (creatorId && crewSnapshotsById) {
let snap: PersonSnapshot | undefined = crewSnapshotsById[creatorId]
// Fallback: If not found directly by key, search by role or name or active user
if (!snap) {
if (creatorId === 'skipper') {
snap = Object.values(crewSnapshotsById).find((s) => s.role === 'skipper')
} else {
// Try to match name case-insensitively
snap = Object.values(crewSnapshotsById).find(
(s) => (s.name || '').trim().toLowerCase() === creatorId.trim().toLowerCase()
)
// Try to match active username/userid to the skipper snapshot
if (!snap) {
const activeUsername = localStorage.getItem('active_username')
const activeUserId = localStorage.getItem('active_userid')
if (
(activeUsername && creatorId.toLowerCase() === activeUsername.toLowerCase()) ||
(activeUserId && creatorId === activeUserId)
) {
snap = Object.values(crewSnapshotsById).find((s) => s.role === 'skipper')
}
}
}
}
if (snap) {
name = snap.name || ''
photo = snap.photo || null
role = snap.role || ''
}
}
// Fallback to active username if owner or no crew pool matches
+21 -7
View File
@@ -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'))
+21 -7
View File
@@ -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'))
+3
View File
@@ -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)",
+3
View File
@@ -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)",
+3
View File
@@ -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)",
+3
View File
@@ -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)",
+3
View File
@@ -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)",
+47
View File
@@ -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()
})
})
+15 -3
View File
@@ -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) {
+1 -1
View File
@@ -291,7 +291,7 @@ MAX_WAIT="$4"
APP_URL="$5"
APP_VERSION="$6"
DEST="$7"
DEPLOY_BRANCH="$8"
DEPLOY_BRANCH="${8:-}"
cd "$REMOTE_DIR" || { echo "Error: Remote directory '$REMOTE_DIR' not found."; exit 1; }