fix(share): only call a share link expired when the server says so (#2505)

The public share page turned every failed GET /api/shared/:token into
"Link expired or invalid". A 5xx during a restart, a proxy 502, a rate
limit, a timeout or a viewer who is briefly offline all told the viewer
the link was dead and sent them back to the owner for a new one, while
the same link loaded fine on the next try.

The endpoint answers 404 with its JSON { error } body for an unknown,
expired or revoked token, so that is now the only answer that shows the
expired screen. A 404 without that body comes from a proxy in front of
TREK (Traefik answers "404 page not found" while the container is
stopped or still starting) and counts as a failed load, as does a 200
that is not the share payload, such as an auth wall's login page, which
used to crash the page. Any failed load shows "This trip could not be
loaded" with a retry button that refetches in place and stays busy
while it runs. The load also ignores a response that lands after the
page moved on to another token.

Both screens use the theme tokens instead of hex literals. New keys are
translated in all 23 locales.
This commit is contained in:
Maurice
2026-09-25 22:00:19 +02:00
parent bea3af4b45
commit 0a3a993dfc
30 changed files with 472 additions and 26 deletions
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'الرابط منتهي أو غير صالح',
'shared.expiredHint': 'رابط الرحلة المشترك لم يعد نشطًا.',
'shared.loadFailed': 'تعذّر تحميل هذه الرحلة',
'shared.loadFailedHint': 'هذا لا يعني أن الرابط قد انتهت صلاحيته. تحقّق من اتصالك وحاول مرة أخرى بعد قليل.',
'shared.retry': 'إعادة المحاولة',
'shared.readOnly': 'عرض للقراءة فقط',
'shared.tabPlan': 'الخطة',
'shared.tabBookings': 'الحجوزات',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link expirado ou inválido',
'shared.expiredHint': 'Este link de viagem compartilhado não está mais ativo.',
'shared.loadFailed': 'Não foi possível carregar esta viagem',
'shared.loadFailedHint':
'Isso não significa que o link expirou. Verifique sua conexão e tente novamente em instantes.',
'shared.retry': 'Tentar novamente',
'shared.readOnly': 'Visualização somente leitura',
'shared.tabPlan': 'Plano',
'shared.tabBookings': 'Reservas',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Enllaç caducat o invàlid',
'shared.expiredHint': 'Aquest enllaç de viatge compartit ja no està actiu.',
'shared.loadFailed': "No s'ha pogut carregar aquest viatge",
'shared.loadFailedHint':
"Això no vol dir que l'enllaç hagi caducat. Comprova la connexió i torna-ho a provar d'aquí a una estona.",
'shared.retry': 'Torna-ho a provar',
'shared.readOnly': 'Vista de només lectura',
'shared.tabPlan': 'Pla',
'shared.tabBookings': 'Reserves',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Odkaz vypršel nebo je neplatný',
'shared.expiredHint': 'Tento sdílený odkaz na cestu již není aktivní.',
'shared.loadFailed': 'Cestu se nepodařilo načíst',
'shared.loadFailedHint': 'Neznamená to, že odkaz vypršel. Zkontrolujte připojení a za chvíli to zkuste znovu.',
'shared.retry': 'Zkusit znovu',
'shared.readOnly': 'Sdílené zobrazení – pouze pro čtení',
'shared.tabPlan': 'Plán',
'shared.tabBookings': 'Rezervace',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link abgelaufen oder ungültig',
'shared.expiredHint': 'Dieser geteilte Reise-Link ist nicht mehr aktiv.',
'shared.loadFailed': 'Die Reise konnte nicht geladen werden',
'shared.loadFailedHint':
'Das heißt nicht, dass der Link abgelaufen ist. Prüfe deine Verbindung und versuche es gleich noch einmal.',
'shared.retry': 'Erneut versuchen',
'shared.readOnly': 'Nur-Lesen Ansicht',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Buchungen',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link expired or invalid',
'shared.expiredHint': 'This shared trip link is no longer active.',
'shared.loadFailed': 'This trip could not be loaded',
'shared.loadFailedHint': 'That does not mean the link has expired. Check your connection and try again in a moment.',
'shared.retry': 'Try again',
'shared.readOnly': 'Read-only shared view',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Bookings',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Enlace expirado o inválido',
'shared.expiredHint': 'Este enlace de viaje compartido ya no está activo.',
'shared.loadFailed': 'No se pudo cargar este viaje',
'shared.loadFailedHint':
'Eso no significa que el enlace haya caducado. Comprueba tu conexión y vuelve a intentarlo en un momento.',
'shared.retry': 'Reintentar',
'shared.readOnly': 'Vista de solo lectura',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Reservas',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Lien expiré ou invalide',
'shared.expiredHint': "Ce lien de partage n'est plus actif.",
'shared.loadFailed': 'Impossible de charger ce voyage',
'shared.loadFailedHint':
'Cela ne veut pas dire que le lien a expiré. Vérifiez votre connexion et réessayez dans un instant.',
'shared.retry': 'Réessayer',
'shared.readOnly': 'Vue en lecture seule',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Réservations',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Ο σύνδεσμος έληξε ή δεν είναι έγκυρος',
'shared.expiredHint': 'Αυτός ο κοινόχρηστος σύνδεσμος ταξιδιού δεν είναι πλέον ενεργός.',
'shared.loadFailed': 'Δεν ήταν δυνατή η φόρτωση αυτού του ταξιδιού',
'shared.loadFailedHint':
'Αυτό δεν σημαίνει ότι ο σύνδεσμος έχει λήξει. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά σε λίγο.',
'shared.retry': 'Δοκιμάστε ξανά',
'shared.readOnly': 'Κοινόχρηστη προβολή μόνο για ανάγνωση',
'shared.tabPlan': 'Σχέδιο',
'shared.tabBookings': 'Κρατήσεις',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link lejárt vagy érvénytelen',
'shared.expiredHint': 'Ez a megosztott utazási link már nem aktív.',
'shared.loadFailed': 'Az utazást nem sikerült betölteni',
'shared.loadFailedHint':
'Ez nem jelenti azt, hogy a link lejárt. Ellenőrizd a kapcsolatot, és próbáld újra egy kicsit később.',
'shared.retry': 'Újrapróbálás',
'shared.readOnly': 'Csak olvasható megosztott nézet',
'shared.tabPlan': 'Terv',
'shared.tabBookings': 'Foglalások',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Tautan kedaluwarsa atau tidak valid',
'shared.expiredHint': 'Tautan perjalanan bersama ini tidak lagi aktif.',
'shared.loadFailed': 'Perjalanan ini tidak dapat dimuat',
'shared.loadFailedHint':
'Ini tidak berarti tautannya sudah kedaluwarsa. Periksa koneksi Anda dan coba lagi sebentar lagi.',
'shared.retry': 'Coba lagi',
'shared.readOnly': 'Tampilan bersama — hanya baca',
'shared.tabPlan': 'Rencana',
'shared.tabBookings': 'Pemesanan',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link scaduto o non valido',
'shared.expiredHint': 'Questo link di viaggio condiviso non è più attivo.',
'shared.loadFailed': 'Impossibile caricare questo viaggio',
'shared.loadFailedHint': 'Non significa che il link sia scaduto. Controlla la connessione e riprova tra un momento.',
'shared.retry': 'Riprova',
'shared.readOnly': 'Vista in sola lettura',
'shared.tabPlan': 'Programma',
'shared.tabBookings': 'Prenotazioni',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'リンクが無効',
'shared.expiredHint': 'この共有リンクは無効です。',
'shared.loadFailed': 'この旅行を読み込めませんでした',
'shared.loadFailedHint':
'リンクの有効期限が切れたわけではありません。接続を確認して、しばらくしてからもう一度お試しください。',
'shared.retry': '再試行',
'shared.readOnly': '読み取り専用',
'shared.tabPlan': 'プラン',
'shared.tabBookings': '予約',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': '링크가 만료되었거나 유효하지 않습니다',
'shared.expiredHint': '이 공유 여행 링크는 더 이상 유효하지 않습니다.',
'shared.loadFailed': '이 여행을 불러올 수 없습니다',
'shared.loadFailedHint': '링크가 만료되었다는 뜻은 아닙니다. 연결 상태를 확인한 뒤 잠시 후 다시 시도해 주세요.',
'shared.retry': '다시 시도',
'shared.readOnly': '읽기 전용 공유 보기',
'shared.tabPlan': '계획',
'shared.tabBookings': '예약',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link verlopen of ongeldig',
'shared.expiredHint': 'Deze gedeelde reislink is niet meer actief.',
'shared.loadFailed': 'Deze reis kon niet worden geladen',
'shared.loadFailedHint':
'Dat betekent niet dat de link is verlopen. Controleer je verbinding en probeer het zo nog eens.',
'shared.retry': 'Opnieuw proberen',
'shared.readOnly': 'Alleen-lezen weergave',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Boekingen',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Link wygasł lub jest nieprawidłowy',
'shared.expiredHint': 'Ten link do podróży jest już nieaktywny.',
'shared.loadFailed': 'Nie udało się wczytać tej podróży',
'shared.loadFailedHint': 'Nie oznacza to, że link wygasł. Sprawdź połączenie i spróbuj ponownie za chwilę.',
'shared.retry': 'Spróbuj ponownie',
'shared.readOnly': 'Widok udostępniony tylko do odczytu',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Rezerwacje',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Ссылка устарела или недействительна',
'shared.expiredHint': 'Эта ссылка на поездку больше не активна.',
'shared.loadFailed': 'Не удалось загрузить поездку',
'shared.loadFailedHint':
'Это не значит, что срок действия ссылки истёк. Проверьте подключение и повторите попытку чуть позже.',
'shared.retry': 'Повторить',
'shared.readOnly': 'Режим только для чтения',
'shared.tabPlan': 'План',
'shared.tabBookings': 'Бронирования',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Länken har gått ut eller är ogiltig',
'shared.expiredHint': 'Den här länken till den delade resan är inte längre aktiv.',
'shared.loadFailed': 'Resan kunde inte laddas',
'shared.loadFailedHint':
'Det betyder inte att länken har gått ut. Kontrollera din anslutning och försök igen om en liten stund.',
'shared.retry': 'Försök igen',
'shared.readOnly': 'Delad vy i skrivskyddat läge',
'shared.tabPlan': 'Plan',
'shared.tabBookings': 'Bokningar',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Bağlantının süresi dolmuş veya geçersiz',
'shared.expiredHint': 'Bu paylaşılan gezi bağlantısı artık etkin değil.',
'shared.loadFailed': 'Bu gezi yüklenemedi',
'shared.loadFailedHint':
'Bu, paylaşım bağlantısının süresinin dolduğu anlamına gelmez. İnternet bağlantınızı kontrol edip biraz sonra tekrar deneyin.',
'shared.retry': 'Yeniden dene',
'shared.readOnly': 'Salt okunur paylaşılan görünüm',
'shared.tabPlan': 'Planı',
'shared.tabBookings': 'Rezervasyonlar',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Посилання прострочено або недійсне',
'shared.expiredHint': 'Це посилання на поїздку більше не активне.',
'shared.loadFailed': 'Не вдалося завантажити поїздку',
'shared.loadFailedHint':
'Це не означає, що термін дії посилання минув. Перевірте з’єднання та спробуйте ще раз трохи згодом.',
'shared.retry': 'Спробувати ще раз',
'shared.readOnly': 'Тільки для читання',
'shared.tabPlan': 'План',
'shared.tabBookings': 'Бронювання',
+4
View File
@@ -3,6 +3,10 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': 'Liên kết đã hết hạn hoặc không hợp lệ',
'shared.expiredHint': 'Liên kết chuyến đi được chia sẻ này không còn hoạt động.',
'shared.loadFailed': 'Không thể tải chuyến đi này',
'shared.loadFailedHint':
'Điều này không có nghĩa là liên kết đã hết hạn. Hãy kiểm tra kết nối và thử lại sau ít phút.',
'shared.retry': 'Thử lại',
'shared.readOnly': 'Chế độ xem chia sẻ chỉ đọc',
'shared.tabPlan': 'Kế hoạch',
'shared.tabBookings': 'Đặt chỗ',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': '連結已過期或無效',
'shared.expiredHint': '此共享旅行連結已失效。',
'shared.loadFailed': '無法載入此旅行',
'shared.loadFailedHint': '這並不代表連結已過期。請檢查網路連線,稍後再試。',
'shared.retry': '重試',
'shared.readOnly': '只讀共享檢視',
'shared.tabPlan': '計劃',
'shared.tabBookings': '預訂',
+3
View File
@@ -3,6 +3,9 @@ import type { TranslationStrings } from '../types';
const shared: TranslationStrings = {
'shared.expired': '链接已过期或无效',
'shared.expiredHint': '此共享旅行链接已失效。',
'shared.loadFailed': '无法加载此旅行',
'shared.loadFailedHint': '这并不代表链接已过期。请检查网络连接,稍后再试。',
'shared.retry': '重试',
'shared.readOnly': '只读共享视图',
'shared.tabPlan': '计划',
'shared.tabBookings': '预订',