diff --git a/server/src/nest/notifications/notification-events.ts b/server/src/nest/notifications/notification-events.ts index edd9784ba..5b542efc4 100644 --- a/server/src/nest/notifications/notification-events.ts +++ b/server/src/nest/notifications/notification-events.ts @@ -39,6 +39,7 @@ export type NotifEventType = | 'collab_message' | 'packing_tagged' | 'version_available' + | 'replica_failure' | 'synology_session_cleared' | 'plugin_notification'; @@ -55,12 +56,13 @@ export const ALL_EVENT_TYPES: NotifEventType[] = [ 'collab_message', 'packing_tagged', 'version_available', + 'replica_failure', 'synology_session_cleared', 'plugin_notification', ]; /** Events that target admins only (shown in admin panel, not in user settings). */ -export const ADMIN_SCOPED_EVENTS = new Set(['version_available']); +export const ADMIN_SCOPED_EVENTS = new Set(['version_available', 'replica_failure']); // Compile-time guard: shared NotificationEventKey and server NotifEventType must // stay in sync. It sits next to the union it guards — it used to live in the diff --git a/server/src/nest/notifications/notifications.module.ts b/server/src/nest/notifications/notifications.module.ts index 275132121..a5aa951f6 100644 --- a/server/src/nest/notifications/notifications.module.ts +++ b/server/src/nest/notifications/notifications.module.ts @@ -4,23 +4,36 @@ import { NotificationsMcp } from './notifications.mcp'; import { NotificationsService } from './notifications.service'; import { NotificationPreferencesService } from './notification-preferences.service'; import { ReminderJobsService } from './reminder-jobs.service'; +import { StorageHealthNotifierService } from './storage-health-notifier.service'; import { NtfyService } from './transports/ntfy.service'; import { WebhookService } from './transports/webhook.service'; import { MailerModule } from './mailer/mailer.module'; import { AuthModule } from '../auth/auth.module'; import { SchedulingModule } from '../scheduling/scheduling.module'; +import { StorageModule } from '../storage/storage.module'; /** Notifications domain (L6 leaf module). Registered in AppModule. * AuthModule feeds NotificationsMcp's demo gate; MailerModule carries SMTP, * which lives outside this module so AuthService can send the password-reset * mail without AuthModule and NotificationsModule importing each other. + * StorageModule feeds StorageHealthNotifierService, which bridges replica + * failures into admin notifications — this direction has no cycle (Storage + * imports only AppConfig+Audit+Scheduling). * NotificationsService and NotificationPreferencesService are exported for * in-container consumers (AdminController's dev test send and preferences tab, * the plugin RPC surface, HostSurfaceRpc). */ @Module({ - imports: [AuthModule, MailerModule, SchedulingModule], + imports: [AuthModule, MailerModule, SchedulingModule, StorageModule], controllers: [NotificationsController, AdminNotificationPreferencesController], - providers: [NotificationsService, NotificationPreferencesService, WebhookService, NtfyService, NotificationsMcp, ReminderJobsService], + providers: [ + NotificationsService, + NotificationPreferencesService, + WebhookService, + NtfyService, + NotificationsMcp, + ReminderJobsService, + StorageHealthNotifierService, + ], exports: [NotificationsService, NotificationPreferencesService], }) export class NotificationsModule {} diff --git a/server/src/nest/notifications/notifications.service.ts b/server/src/nest/notifications/notifications.service.ts index 9f3eec369..e9b734d57 100644 --- a/server/src/nest/notifications/notifications.service.ts +++ b/server/src/nest/notifications/notifications.service.ts @@ -210,6 +210,13 @@ const EVENT_NOTIFICATION_CONFIG: Record = { navigateTextKey: 'notif.action.view_admin', navigateTarget: () => '/admin', }, + replica_failure: { + inAppType: 'navigate', + titleKey: 'notif.replica_failure.title', + textKey: 'notif.replica_failure.text', + navigateTextKey: 'notif.action.view', + navigateTarget: () => '/admin?tab=storage', + }, synology_session_cleared: { inAppType: 'simple', titleKey: 'notifications.synologySessionCleared.title', diff --git a/server/src/nest/notifications/replica-failure-debouncer.ts b/server/src/nest/notifications/replica-failure-debouncer.ts new file mode 100644 index 000000000..5419b86c3 --- /dev/null +++ b/server/src/nest/notifications/replica-failure-debouncer.ts @@ -0,0 +1,28 @@ +/** + * Lazy per-backend debounce (backfill/stats/notifications spec): the first + * failure notifies immediately; later failures accumulate and the next one + * past the window rides out as a summary carrying the suppressed count. No + * timers — the summary is sent on the next failure, which is the only moment + * anyone would care. Process-local, like the health ring itself. + */ +export class ReplicaFailureDebouncer { + private readonly windows = new Map(); + + constructor( + private readonly windowMs: number, + private readonly now: () => number = Date.now, + ) {} + + /** null = suppress; otherwise the suppressed-count to report with this send. */ + admit(backend: string): number | null { + const now = this.now(); + const window = this.windows.get(backend); + if (!window || now - window.windowStart > this.windowMs) { + const suppressed = window?.suppressed ?? 0; + this.windows.set(backend, { windowStart: now, suppressed: 0 }); + return suppressed; + } + window.suppressed += 1; + return null; + } +} diff --git a/server/src/nest/notifications/storage-health-notifier.service.ts b/server/src/nest/notifications/storage-health-notifier.service.ts new file mode 100644 index 000000000..edc8435a6 --- /dev/null +++ b/server/src/nest/notifications/storage-health-notifier.service.ts @@ -0,0 +1,49 @@ +import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { StorageEventsService } from '../storage/storage-events.service'; +import { NotificationsService } from './notifications.service'; +import { ReplicaFailureDebouncer } from './replica-failure-debouncer'; + +const DEBOUNCE_WINDOW_MS = 60 * 60 * 1000; + +/** + * Bridges storage replica failures into the admin notification channels. + * Lives HERE (not in StorageModule) because NotificationsModule → AuthModule + * → StorageModule — the reverse import would cycle. Subscribes at bootstrap; + * the debouncer keeps an outage from becoming a notification storm. + */ +@Injectable() +export class StorageHealthNotifierService implements OnApplicationBootstrap { + private readonly logger = new Logger(StorageHealthNotifierService.name); + private readonly debouncer = new ReplicaFailureDebouncer(DEBOUNCE_WINDOW_MS); + + constructor( + private readonly events: StorageEventsService, + private readonly notifications: NotificationsService, + ) {} + + onApplicationBootstrap(): void { + this.events.onReplicaFailure((failure) => { + const suppressed = this.debouncer.admit(failure.backend); + if (suppressed === null) return; + void this.notifications + .send({ + event: 'replica_failure', + actorId: null, + scope: 'admin', + targetId: 0, + params: { + backend: failure.backend, + key: failure.key, + op: failure.op, + error: failure.error, + suppressed: String(suppressed), + }, + }) + .catch((err: unknown) => { + this.logger.error( + `replica_failure notification failed: ${err instanceof Error ? err.message : String(err)}`, + ); + }); + }); + } +} diff --git a/server/src/nest/notifications/transports/ntfy.service.ts b/server/src/nest/notifications/transports/ntfy.service.ts index 23288f485..95255259b 100644 --- a/server/src/nest/notifications/transports/ntfy.service.ts +++ b/server/src/nest/notifications/transports/ntfy.service.ts @@ -24,6 +24,7 @@ const NTFY_EVENT_META: Partial { + it('DEB-001 first failure per backend is admitted with suppressed=0', () => { + let now = 1_000_000; + const debouncer = new ReplicaFailureDebouncer(HOUR, () => now); + expect(debouncer.admit('s3-bkp')).toBe(0); + }); + + it('DEB-002 failures inside the window are suppressed; the next one after it carries the count', () => { + let now = 1_000_000; + const debouncer = new ReplicaFailureDebouncer(HOUR, () => now); + expect(debouncer.admit('s3-bkp')).toBe(0); + now += 1000; + expect(debouncer.admit('s3-bkp')).toBeNull(); + expect(debouncer.admit('s3-bkp')).toBeNull(); + now += HOUR; + expect(debouncer.admit('s3-bkp')).toBe(2); // the two suppressed ones ride the summary + now += 1000; + expect(debouncer.admit('s3-bkp')).toBeNull(); // window reset + }); + + it('DEB-003 backends debounce independently', () => { + let now = 1_000_000; + const debouncer = new ReplicaFailureDebouncer(HOUR, () => now); + expect(debouncer.admit('a')).toBe(0); + expect(debouncer.admit('b')).toBe(0); + expect(debouncer.admit('a')).toBeNull(); + expect(debouncer.admit('b')).toBeNull(); + }); +}); diff --git a/server/tests/unit/nest/notifications/storage-health-notifier.service.test.ts b/server/tests/unit/nest/notifications/storage-health-notifier.service.test.ts new file mode 100644 index 000000000..6881c966c --- /dev/null +++ b/server/tests/unit/nest/notifications/storage-health-notifier.service.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, vi } from 'vitest'; +import { StorageEventsService } from '../../../../src/nest/storage/storage-events.service'; +import { StorageHealthNotifierService } from '../../../../src/nest/notifications/storage-health-notifier.service'; +import type { NotificationsService } from '../../../../src/nest/notifications/notifications.service'; + +describe('StorageHealthNotifierService', () => { + it('NOTIF-001 subscribes on bootstrap and sends an admin-scoped replica_failure with params', () => { + const events = new StorageEventsService(); + const send = vi.fn().mockResolvedValue(undefined); + const notifier = new StorageHealthNotifierService(events, { send } as unknown as NotificationsService); + notifier.onApplicationBootstrap(); + events.emitReplicaFailure({ backend: 's3-bkp', key: 'backups/db.zip', op: 'put', error: 'timeout', at: 1 }); + expect(send).toHaveBeenCalledWith({ + event: 'replica_failure', + actorId: null, + scope: 'admin', + targetId: 0, + params: { backend: 's3-bkp', key: 'backups/db.zip', op: 'put', error: 'timeout', suppressed: '0' }, + }); + // Second failure inside the window: suppressed, no second send. + events.emitReplicaFailure({ backend: 's3-bkp', key: 'backups/x.zip', op: 'put', error: 'timeout', at: 2 }); + expect(send).toHaveBeenCalledTimes(1); + }); + + it('NOTIF-002 a rejected send never throws into the emitter (write path stays safe)', () => { + const events = new StorageEventsService(); + const send = vi.fn().mockRejectedValue(new Error('smtp down')); + const notifier = new StorageHealthNotifierService(events, { send } as unknown as NotificationsService); + notifier.onApplicationBootstrap(); + expect(() => + events.emitReplicaFailure({ backend: 'b', key: 'k', op: 'delete', error: 'e', at: 3 }), + ).not.toThrow(); + }); +}); diff --git a/shared/src/i18n/ar/externalNotifications.ts b/shared/src/i18n/ar/externalNotifications.ts index 52a22aa4e..4818ec6cb 100644 --- a/shared/src/i18n/ar/externalNotifications.ts +++ b/shared/src/i18n/ar/externalNotifications.ts @@ -52,6 +52,12 @@ const ar: NotificationLocale = { title: 'إصدار TREK جديد متاح', body: `TREK ${p.version} متاح الآن. تفضل بزيارة لوحة الإدارة للتحديث.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'تمت إعادة تعيين جلسة Synology', body: 'تغيّر حسابك أو رابط Synology. تم تسجيل خروجك من Synology Photos.', diff --git a/shared/src/i18n/ar/notif.ts b/shared/src/i18n/ar/notif.ts index eef17db83..b56adcd0a 100644 --- a/shared/src/i18n/ar/notif.ts +++ b/shared/src/i18n/ar/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} عيّنك في {category} في {trip}', 'notif.version_available.title': 'إصدار جديد متاح', 'notif.version_available.text': 'TREK {version} متاح الآن', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'عرض الرحلة', 'notif.action.view_collab': 'عرض الرسائل', 'notif.action.view_packing': 'عرض التعبئة', diff --git a/shared/src/i18n/br/externalNotifications.ts b/shared/src/i18n/br/externalNotifications.ts index 91fd6b4e0..5fe6f1f17 100644 --- a/shared/src/i18n/br/externalNotifications.ts +++ b/shared/src/i18n/br/externalNotifications.ts @@ -52,6 +52,12 @@ const br: NotificationLocale = { title: 'Nova versão do TREK disponível', body: `O TREK ${p.version} está disponível. Acesse o painel de administração para atualizar.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Sessão Synology encerrada', body: 'Sua conta ou URL do Synology foi alterada. Você foi desconectado do Synology Photos.', diff --git a/shared/src/i18n/br/notif.ts b/shared/src/i18n/br/notif.ts index 2f0711f2c..955123f11 100644 --- a/shared/src/i18n/br/notif.ts +++ b/shared/src/i18n/br/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} atribuiu você a {category} em {trip}', 'notif.version_available.title': 'Nova versão disponível', 'notif.version_available.text': 'TREK {version} está disponível', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Ver viagem', 'notif.action.view_collab': 'Ver mensagens', 'notif.action.view_packing': 'Ver bagagem', diff --git a/shared/src/i18n/ca/externalNotifications.ts b/shared/src/i18n/ca/externalNotifications.ts index f33138356..bb04cc03c 100644 --- a/shared/src/i18n/ca/externalNotifications.ts +++ b/shared/src/i18n/ca/externalNotifications.ts @@ -48,6 +48,12 @@ const ca: NotificationLocale = { title: 'Versió nova de TREK disponible', body: `TREK ${p.version} ja està disponible. Visita el panell d'administració per actualitzar.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Sessió de Synology tancada', body: 'El teu compte o URL de Synology ha canviat. Has tancat la sessió a Synology Photos.', diff --git a/shared/src/i18n/ca/notif.ts b/shared/src/i18n/ca/notif.ts index 28fa58849..e2843b7d1 100644 --- a/shared/src/i18n/ca/notif.ts +++ b/shared/src/i18n/ca/notif.ts @@ -25,6 +25,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} et va assignar a {category} a {trip}', 'notif.version_available.title': 'Versió nova disponible', 'notif.version_available.text': 'TREK {version} ja està disponible', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Mostra el viatge', 'notif.action.view_collab': 'Mostra els missatges', 'notif.action.view_packing': "Mostra l'equipatge", diff --git a/shared/src/i18n/cs/externalNotifications.ts b/shared/src/i18n/cs/externalNotifications.ts index 10cbc237c..0f1a8d625 100644 --- a/shared/src/i18n/cs/externalNotifications.ts +++ b/shared/src/i18n/cs/externalNotifications.ts @@ -52,6 +52,12 @@ const cs: NotificationLocale = { title: 'Nová verze TREK dostupná', body: `TREK ${p.version} je nyní dostupný. Navštivte administrátorský panel pro aktualizaci.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Relace Synology byla zrušena', body: 'Váš účet nebo URL Synology se změnil. Byli jste odhlášeni ze Synology Photos.', diff --git a/shared/src/i18n/cs/notif.ts b/shared/src/i18n/cs/notif.ts index f7d8255b3..66588a95a 100644 --- a/shared/src/i18n/cs/notif.ts +++ b/shared/src/i18n/cs/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} vás přiřadil k {category} v {trip}', 'notif.version_available.title': 'Nová verze dostupná', 'notif.version_available.text': 'TREK {version} je nyní dostupný', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Zobrazit výlet', 'notif.action.view_collab': 'Zobrazit zprávy', 'notif.action.view_packing': 'Zobrazit balení', diff --git a/shared/src/i18n/de/externalNotifications.ts b/shared/src/i18n/de/externalNotifications.ts index 5509451f7..d456a1b19 100644 --- a/shared/src/i18n/de/externalNotifications.ts +++ b/shared/src/i18n/de/externalNotifications.ts @@ -52,6 +52,12 @@ const de: NotificationLocale = { title: 'Neue TREK-Version verfügbar', body: `TREK ${p.version} ist jetzt verfügbar. Besuche das Admin-Panel zum Aktualisieren.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology-Sitzung beendet', body: 'Dein Synology-Konto oder die URL hat sich geändert. Du wurdest von Synology Photos abgemeldet.', diff --git a/shared/src/i18n/de/notif.ts b/shared/src/i18n/de/notif.ts index be71805b1..4da749a44 100644 --- a/shared/src/i18n/de/notif.ts +++ b/shared/src/i18n/de/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} hat dich zu {category} in {trip} zugewiesen', 'notif.version_available.title': 'Neue Version verfügbar', 'notif.version_available.text': 'TREK {version} ist jetzt verfügbar', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Reise ansehen', 'notif.action.view_collab': 'Nachrichten ansehen', 'notif.action.view_packing': 'Packliste ansehen', diff --git a/shared/src/i18n/en/externalNotifications.ts b/shared/src/i18n/en/externalNotifications.ts index 53c6c7fbf..846179460 100644 --- a/shared/src/i18n/en/externalNotifications.ts +++ b/shared/src/i18n/en/externalNotifications.ts @@ -52,6 +52,12 @@ const en: NotificationLocale = { title: 'New TREK version available', body: `TREK ${p.version} is now available. Visit the admin panel to update.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology session cleared', body: 'Your Synology account or URL changed. You have been logged out of Synology Photos.', diff --git a/shared/src/i18n/en/notif.ts b/shared/src/i18n/en/notif.ts index ed191640d..178d7136d 100644 --- a/shared/src/i18n/en/notif.ts +++ b/shared/src/i18n/en/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} assigned you to {category} in {trip}', 'notif.version_available.title': 'New Version Available', 'notif.version_available.text': 'TREK {version} is now available', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'View Trip', 'notif.action.view_collab': 'View Messages', 'notif.action.view_packing': 'View Packing', diff --git a/shared/src/i18n/es/externalNotifications.ts b/shared/src/i18n/es/externalNotifications.ts index 853e26290..96ecc1058 100644 --- a/shared/src/i18n/es/externalNotifications.ts +++ b/shared/src/i18n/es/externalNotifications.ts @@ -52,6 +52,12 @@ const es: NotificationLocale = { title: 'Nueva versión de TREK disponible', body: `TREK ${p.version} ya está disponible. Visita el panel de administración para actualizar.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Sesión de Synology cerrada', body: 'Tu cuenta o URL de Synology ha cambiado. Has cerrado sesión en Synology Photos.', diff --git a/shared/src/i18n/es/notif.ts b/shared/src/i18n/es/notif.ts index 3bf92e58b..605225ab7 100644 --- a/shared/src/i18n/es/notif.ts +++ b/shared/src/i18n/es/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} te asignó a {category} en {trip}', 'notif.version_available.title': 'Nueva versión disponible', 'notif.version_available.text': 'TREK {version} ya está disponible', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Ver viaje', 'notif.action.view_collab': 'Ver mensajes', 'notif.action.view_packing': 'Ver equipaje', diff --git a/shared/src/i18n/externalNotifications/types.ts b/shared/src/i18n/externalNotifications/types.ts index cdb12f0db..534a4f58f 100644 --- a/shared/src/i18n/externalNotifications/types.ts +++ b/shared/src/i18n/externalNotifications/types.ts @@ -33,6 +33,7 @@ export type NotificationEventKey = | 'collab_message' | 'packing_tagged' | 'version_available' + | 'replica_failure' | 'synology_session_cleared' | 'plugin_notification'; diff --git a/shared/src/i18n/fr/externalNotifications.ts b/shared/src/i18n/fr/externalNotifications.ts index 9146e7d07..e49d8dd4f 100644 --- a/shared/src/i18n/fr/externalNotifications.ts +++ b/shared/src/i18n/fr/externalNotifications.ts @@ -52,6 +52,12 @@ const fr: NotificationLocale = { title: 'Nouvelle version TREK disponible', body: `TREK ${p.version} est maintenant disponible. Rendez-vous dans le panneau d'administration pour mettre à jour.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Session Synology effacée', body: 'Votre compte ou URL Synology a changé. Vous avez été déconnecté de Synology Photos.', diff --git a/shared/src/i18n/fr/notif.ts b/shared/src/i18n/fr/notif.ts index 267100444..43ab30443 100644 --- a/shared/src/i18n/fr/notif.ts +++ b/shared/src/i18n/fr/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} vous a assigné à {category} dans {trip}', 'notif.version_available.title': 'Nouvelle version disponible', 'notif.version_available.text': 'TREK {version} est maintenant disponible', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Voir le voyage', 'notif.action.view_collab': 'Voir les messages', 'notif.action.view_packing': 'Voir les bagages', diff --git a/shared/src/i18n/gr/externalNotifications.ts b/shared/src/i18n/gr/externalNotifications.ts index 88cc8f89e..28bf22688 100644 --- a/shared/src/i18n/gr/externalNotifications.ts +++ b/shared/src/i18n/gr/externalNotifications.ts @@ -52,6 +52,12 @@ const gr: NotificationLocale = { title: 'Νέα έκδοση TREK διαθέσιμη', body: `Η έκδοση TREK ${p.version} είναι τώρα διαθέσιμη. Επισκεφθείτε τον πίνακα διαχείρισης για να ενημερώσετε.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Η σύνδεση Synology τερματίστηκε', body: 'Ο λογαριασμός σας Synology ή το URL άλλαξε. Έχετε αποσυνδεθεί από το Synology Photos.', diff --git a/shared/src/i18n/gr/notif.ts b/shared/src/i18n/gr/notif.ts index 90f463d50..d8ce8431b 100644 --- a/shared/src/i18n/gr/notif.ts +++ b/shared/src/i18n/gr/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': 'Ο/Η {actor} σας ανέθεσε στο {category} του {trip}', 'notif.version_available.title': 'Νέα Έκδοση Διαθέσιμη', 'notif.version_available.text': 'Το TREK {version} είναι τώρα διαθέσιμο', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Προβολή Ταξιδιού', 'notif.action.view_collab': 'Προβολή Μηνυμάτων', 'notif.action.view_packing': 'Προβολή Αποσκευών', diff --git a/shared/src/i18n/hu/externalNotifications.ts b/shared/src/i18n/hu/externalNotifications.ts index 10b02b5a9..7d7cf8fbc 100644 --- a/shared/src/i18n/hu/externalNotifications.ts +++ b/shared/src/i18n/hu/externalNotifications.ts @@ -52,6 +52,12 @@ const hu: NotificationLocale = { title: 'Új TREK verzió érhető el', body: `A TREK ${p.version} elérhető. Látogasson el az adminisztrációs panelre a frissítéshez.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology munkamenet törölve', body: 'A Synology fiókja vagy URL-je megváltozott. Kijelentkeztek a Synology Photos-ból.', diff --git a/shared/src/i18n/hu/notif.ts b/shared/src/i18n/hu/notif.ts index ee4e8a5be..39959647c 100644 --- a/shared/src/i18n/hu/notif.ts +++ b/shared/src/i18n/hu/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} hozzárendelte Önt a {category} kategóriához a(z) {trip} utazásban', 'notif.version_available.title': 'Új verzió elérhető', 'notif.version_available.text': 'A TREK {version} elérhető', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Utazás megtekintése', 'notif.action.view_collab': 'Üzenetek megtekintése', 'notif.action.view_packing': 'Csomagolás megtekintése', diff --git a/shared/src/i18n/id/externalNotifications.ts b/shared/src/i18n/id/externalNotifications.ts index e40667766..37770e203 100644 --- a/shared/src/i18n/id/externalNotifications.ts +++ b/shared/src/i18n/id/externalNotifications.ts @@ -52,6 +52,12 @@ const id: NotificationLocale = { title: 'Versi TREK baru tersedia', body: `TREK ${p.version} sekarang tersedia. Kunjungi panel admin untuk memperbarui.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Sesi Synology dihapus', body: 'Akun atau URL Synology Anda berubah. Anda telah keluar dari Synology Photos.', diff --git a/shared/src/i18n/id/notif.ts b/shared/src/i18n/id/notif.ts index 841f53948..ac7e6e96b 100644 --- a/shared/src/i18n/id/notif.ts +++ b/shared/src/i18n/id/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} menugaskanmu ke {category} di {trip}', 'notif.version_available.title': 'Versi Baru Tersedia', 'notif.version_available.text': 'TREK {version} kini tersedia', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Lihat Perjalanan', 'notif.action.view_collab': 'Lihat Pesan', 'notif.action.view_packing': 'Lihat Perlengkapan', diff --git a/shared/src/i18n/it/externalNotifications.ts b/shared/src/i18n/it/externalNotifications.ts index 29fc136f4..c634d269d 100644 --- a/shared/src/i18n/it/externalNotifications.ts +++ b/shared/src/i18n/it/externalNotifications.ts @@ -52,6 +52,12 @@ const it: NotificationLocale = { title: 'Nuova versione TREK disponibile', body: `TREK ${p.version} è ora disponibile. Visita il pannello di amministrazione per aggiornare.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Sessione Synology rimossa', body: 'Il tuo account o URL Synology è cambiato. Sei stato disconnesso da Synology Photos.', diff --git a/shared/src/i18n/it/notif.ts b/shared/src/i18n/it/notif.ts index 5278aae7b..c6fa26793 100644 --- a/shared/src/i18n/it/notif.ts +++ b/shared/src/i18n/it/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} ti ha assegnato a {category} in {trip}', 'notif.version_available.title': 'Nuova versione disponibile', 'notif.version_available.text': 'TREK {version} è ora disponibile', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Vedi viaggio', 'notif.action.view_collab': 'Vedi messaggi', 'notif.action.view_packing': 'Vedi bagagli', diff --git a/shared/src/i18n/ja/externalNotifications.ts b/shared/src/i18n/ja/externalNotifications.ts index 7b5a91957..4c2bddfd2 100644 --- a/shared/src/i18n/ja/externalNotifications.ts +++ b/shared/src/i18n/ja/externalNotifications.ts @@ -52,6 +52,12 @@ const ja: NotificationLocale = { title: '新しいTREKバージョンが利用可能', body: `TREK ${p.version}が利用可能になりました。管理パネルからアップデートしてください。`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synologyセッションがクリアされました', body: 'SynologyアカウントまたはURLが変更されました。Synology Photosからログアウトされました。', diff --git a/shared/src/i18n/ja/notif.ts b/shared/src/i18n/ja/notif.ts index 58a72cf73..bbe89e48f 100644 --- a/shared/src/i18n/ja/notif.ts +++ b/shared/src/i18n/ja/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor}が「{trip}」の{category}をあなたに割り当てました', 'notif.version_available.title': '新しいバージョンがあります', 'notif.version_available.text': 'TREK {version}が利用可能です', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': '旅行を見る', 'notif.action.view_collab': 'メッセージを見る', 'notif.action.view_packing': '持ち物を見る', diff --git a/shared/src/i18n/ko/externalNotifications.ts b/shared/src/i18n/ko/externalNotifications.ts index 5209cfc27..e40f4960e 100644 --- a/shared/src/i18n/ko/externalNotifications.ts +++ b/shared/src/i18n/ko/externalNotifications.ts @@ -52,6 +52,12 @@ const ko: NotificationLocale = { title: '새 TREK 버전 사용 가능', body: `TREK ${p.version}을 사용할 수 있습니다. 관리자 패널에서 업데이트하세요.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology 세션이 초기화되었습니다', body: 'Synology 계정 또는 URL이 변경되었습니다. Synology Photos에서 로그아웃되었습니다.', diff --git a/shared/src/i18n/ko/notif.ts b/shared/src/i18n/ko/notif.ts index 339095447..c2a4d3de4 100644 --- a/shared/src/i18n/ko/notif.ts +++ b/shared/src/i18n/ko/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor}이(가) {trip}의 {category}에 배정했습니다', 'notif.version_available.title': '새 버전 사용 가능', 'notif.version_available.text': 'TREK {version}이(가) 사용 가능합니다', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': '여행 보기', 'notif.action.view_collab': '메시지 보기', 'notif.action.view_packing': '짐 목록 보기', diff --git a/shared/src/i18n/nl/externalNotifications.ts b/shared/src/i18n/nl/externalNotifications.ts index 357214326..79e41e566 100644 --- a/shared/src/i18n/nl/externalNotifications.ts +++ b/shared/src/i18n/nl/externalNotifications.ts @@ -52,6 +52,12 @@ const nl: NotificationLocale = { title: 'Nieuwe TREK-versie beschikbaar', body: `TREK ${p.version} is nu beschikbaar. Bezoek het beheerderspaneel om bij te werken.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology-sessie gewist', body: 'Je Synology-account of URL is gewijzigd. Je bent uitgelogd bij Synology Photos.', diff --git a/shared/src/i18n/nl/notif.ts b/shared/src/i18n/nl/notif.ts index 33c15be5e..16c0ee366 100644 --- a/shared/src/i18n/nl/notif.ts +++ b/shared/src/i18n/nl/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} heeft je toegewezen aan {category} in {trip}', 'notif.version_available.title': 'Nieuwe versie beschikbaar', 'notif.version_available.text': 'TREK {version} is nu beschikbaar', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Reis bekijken', 'notif.action.view_collab': 'Berichten bekijken', 'notif.action.view_packing': 'Paklijst bekijken', diff --git a/shared/src/i18n/pl/externalNotifications.ts b/shared/src/i18n/pl/externalNotifications.ts index 84d8fa48f..b4302ea64 100644 --- a/shared/src/i18n/pl/externalNotifications.ts +++ b/shared/src/i18n/pl/externalNotifications.ts @@ -52,6 +52,12 @@ const pl: NotificationLocale = { title: 'Nowa wersja TREK dostępna', body: `TREK ${p.version} jest teraz dostępny. Odwiedź panel administracyjny, aby zaktualizować.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Sesja Synology wyczyszczona', body: 'Twoje konto lub URL Synology uległo zmianie. Zostałeś wylogowany z Synology Photos.', diff --git a/shared/src/i18n/pl/notif.ts b/shared/src/i18n/pl/notif.ts index c6811b4a3..60f5cc13b 100644 --- a/shared/src/i18n/pl/notif.ts +++ b/shared/src/i18n/pl/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} przypisał Cię do {category} w {trip}', 'notif.version_available.title': 'Nowa wersja dostępna', 'notif.version_available.text': 'TREK {version} jest teraz dostępny', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Zobacz podróż', 'notif.action.view_collab': 'Zobacz wiadomości', 'notif.action.view_packing': 'Zobacz pakowanie', diff --git a/shared/src/i18n/ru/externalNotifications.ts b/shared/src/i18n/ru/externalNotifications.ts index 41ab512a2..2ea2abb42 100644 --- a/shared/src/i18n/ru/externalNotifications.ts +++ b/shared/src/i18n/ru/externalNotifications.ts @@ -52,6 +52,12 @@ const ru: NotificationLocale = { title: 'Доступна новая версия TREK', body: `TREK ${p.version} теперь доступен. Перейдите в панель администратора для обновления.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Сессия Synology сброшена', body: 'Ваш аккаунт или URL Synology изменился. Вы вышли из Synology Photos.', diff --git a/shared/src/i18n/ru/notif.ts b/shared/src/i18n/ru/notif.ts index ee67d0b21..916ec5ff0 100644 --- a/shared/src/i18n/ru/notif.ts +++ b/shared/src/i18n/ru/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} назначил вас в {category} в {trip}', 'notif.version_available.title': 'Доступна новая версия', 'notif.version_available.text': 'TREK {version} теперь доступен', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Открыть поездку', 'notif.action.view_collab': 'Открыть сообщения', 'notif.action.view_packing': 'Открыть упаковку', diff --git a/shared/src/i18n/sv/externalNotifications.ts b/shared/src/i18n/sv/externalNotifications.ts index c1c5d1605..fac796016 100644 --- a/shared/src/i18n/sv/externalNotifications.ts +++ b/shared/src/i18n/sv/externalNotifications.ts @@ -52,6 +52,12 @@ const en: NotificationLocale = { title: 'Ny TREK version tillgänglig', body: `TREK ${p.version} är nu tillgänglig. Gå till adminpanelen för att uppdatera.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology session rensad', body: 'Ditt Synology-konto eller din webbadress har ändrats. Du har loggats ut från Synology Photos.', diff --git a/shared/src/i18n/sv/notif.ts b/shared/src/i18n/sv/notif.ts index 65506fd73..fe8901664 100644 --- a/shared/src/i18n/sv/notif.ts +++ b/shared/src/i18n/sv/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} har tilldelat dig {category} i {trip}', 'notif.version_available.title': 'Ny version tillgänglig', 'notif.version_available.text': 'TREK {version} finns nu tillgängligt', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Visa resan', 'notif.action.view_collab': 'Visa meddelanden', 'notif.action.view_packing': 'Visa förpackning', diff --git a/shared/src/i18n/tr/externalNotifications.ts b/shared/src/i18n/tr/externalNotifications.ts index 282d4a791..037f7eaca 100644 --- a/shared/src/i18n/tr/externalNotifications.ts +++ b/shared/src/i18n/tr/externalNotifications.ts @@ -52,6 +52,12 @@ const tr: NotificationLocale = { title: 'Yeni TREK sürümü mevcut', body: `TREK ${p.version} artık mevcut. Güncellemek için yönetici panelini ziyaret edin.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology oturumu temizlendi', body: 'Synology hesabınız veya URL değişti. Synology Photos oturumunuz kapatıldı.', diff --git a/shared/src/i18n/tr/notif.ts b/shared/src/i18n/tr/notif.ts index 4ec97aef3..ade756a28 100644 --- a/shared/src/i18n/tr/notif.ts +++ b/shared/src/i18n/tr/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} sizi {trip} içinde {category} kategorisine atadı', 'notif.version_available.title': 'Yeni Sürüm Mevcut', 'notif.version_available.text': 'TREK {version} artık kullanılabilir', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Seyahati Görüntüle', 'notif.action.view_collab': 'Mesajları Görüntüle', 'notif.action.view_packing': 'Paket listesini görüntüle', diff --git a/shared/src/i18n/uk/externalNotifications.ts b/shared/src/i18n/uk/externalNotifications.ts index de83ed167..6ab90b7b8 100644 --- a/shared/src/i18n/uk/externalNotifications.ts +++ b/shared/src/i18n/uk/externalNotifications.ts @@ -52,6 +52,12 @@ const uk: NotificationLocale = { title: 'Доступна нова версія TREK', body: `TREK ${p.version} тепер доступний. Перейдіть до панелі адміністратора для оновлення.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Сеанс Synology скинуто', body: 'Ваш обліковий запис або URL Synology змінився. Ви вийшли з Synology Photos.', diff --git a/shared/src/i18n/uk/notif.ts b/shared/src/i18n/uk/notif.ts index 0ae172271..565e6af52 100644 --- a/shared/src/i18n/uk/notif.ts +++ b/shared/src/i18n/uk/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} призначив вас у {category} у {trip}', 'notif.version_available.title': 'Доступна нова версія', 'notif.version_available.text': 'TREK {version} тепер доступний', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Відкрити поїздку', 'notif.action.view_collab': 'Відкрити повідомлення', 'notif.action.view_packing': 'Відкрити пакування', diff --git a/shared/src/i18n/vi/externalNotifications.ts b/shared/src/i18n/vi/externalNotifications.ts index ca4ab2830..c5101dfc3 100644 --- a/shared/src/i18n/vi/externalNotifications.ts +++ b/shared/src/i18n/vi/externalNotifications.ts @@ -52,6 +52,12 @@ const en: NotificationLocale = { title: 'Đã có phiên bản TREK mới', body: `TREK ${p.version} có bản mới. Vui lòng truy cập bảng điều khiển quản trị để cập nhật.`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Đã xóa phiên Synology', body: 'Tài khoản Synology của bạn hoặc URL đã thay đổi. Bạn đã đăng xuất khỏi Synology Photos.', diff --git a/shared/src/i18n/vi/notif.ts b/shared/src/i18n/vi/notif.ts index a2dcb9dfc..980cf2fbf 100644 --- a/shared/src/i18n/vi/notif.ts +++ b/shared/src/i18n/vi/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} đã giao cho bạn {category} trong {trip}', 'notif.version_available.title': 'Phiên bản mới có sẵn', 'notif.version_available.text': 'TREK {version} hiện có sẵn', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': 'Xem chuyến đi', 'notif.action.view_collab': 'Xem tin nhắn', 'notif.action.view_packing': 'Xem đóng gói', diff --git a/shared/src/i18n/zh-TW/externalNotifications.ts b/shared/src/i18n/zh-TW/externalNotifications.ts index 416066206..6888cbd3c 100644 --- a/shared/src/i18n/zh-TW/externalNotifications.ts +++ b/shared/src/i18n/zh-TW/externalNotifications.ts @@ -52,6 +52,12 @@ const zhTW: NotificationLocale = { title: '新版 TREK 可用', body: `TREK ${p.version} 現已可用。請前往管理面板進行更新。`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology 工作階段已清除', body: '您的 Synology 帳戶或 URL 已變更,您已登出 Synology Photos。', diff --git a/shared/src/i18n/zh-TW/notif.ts b/shared/src/i18n/zh-TW/notif.ts index ade8e2911..f2e6a0299 100644 --- a/shared/src/i18n/zh-TW/notif.ts +++ b/shared/src/i18n/zh-TW/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} 在 {trip} 中將 {category} 指派給你', 'notif.version_available.title': '有新版本可用', 'notif.version_available.text': 'TREK {version} 現已推出', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': '查看旅行', 'notif.action.view_collab': '查看訊息', 'notif.action.view_packing': '查看打包', diff --git a/shared/src/i18n/zh/externalNotifications.ts b/shared/src/i18n/zh/externalNotifications.ts index 6fdec2485..75311b740 100644 --- a/shared/src/i18n/zh/externalNotifications.ts +++ b/shared/src/i18n/zh/externalNotifications.ts @@ -52,6 +52,12 @@ const zh: NotificationLocale = { title: '新版 TREK 可用', body: `TREK ${p.version} 现已可用。请前往管理面板进行更新。`, }), + replica_failure: (p) => ({ + title: 'Storage replica failure', + body: + `Replica write failed on '${p.backend}': ${p.op} of ${p.key} — ${p.error}.` + + (p.suppressed !== '0' ? ` ${p.suppressed} more failures were suppressed since the last notification.` : ''), + }), synology_session_cleared: () => ({ title: 'Synology 会话已清除', body: '您的 Synology 账户或 URL 已更改,您已退出 Synology Photos。', diff --git a/shared/src/i18n/zh/notif.ts b/shared/src/i18n/zh/notif.ts index 7f2cd5659..ad2b639b5 100644 --- a/shared/src/i18n/zh/notif.ts +++ b/shared/src/i18n/zh/notif.ts @@ -27,6 +27,8 @@ const notif: TranslationStrings = { 'notif.packing_tagged.text': '{actor} 将您分配到 {trip} 中的 {category}', 'notif.version_available.title': '新版本可用', 'notif.version_available.text': 'TREK {version} 现已可用', + 'notif.replica_failure.title': 'Storage replica failure', + 'notif.replica_failure.text': "Replica write failed on '{backend}': {op} of {key} — {error}", 'notif.action.view_trip': '查看旅行', 'notif.action.view_collab': '查看消息', 'notif.action.view_packing': '查看行李',