diff --git a/src/components/dev/NotificationDebugPanel.vue b/src/components/dev/NotificationDebugPanel.vue index 57d96542..72355647 100644 --- a/src/components/dev/NotificationDebugPanel.vue +++ b/src/components/dev/NotificationDebugPanel.vue @@ -62,11 +62,35 @@ :class="{ 'opacity-50 cursor-not-allowed': busy }" @click="onSimulateWakeupRefresh" > - Simulate WAKEUP_PING + Simulate WAKEUP_PING (Local)

Local simulation only — calls the refresh API directly (no FCM push).

+ +

+ {{ realWakeupStatus.message }} +

+

+ Full pipeline — backend `/debug/send-wakeup` → FCM → WAKEUP_PING + handler. +

@@ -306,6 +330,7 @@ const testModeEnabled = ref(NotificationDebugService.isTestModeEnabled()); const fcmToken = ref(NotificationDebugService.getFcmToken()); const activeBackendUrl = ref(NotificationDebugService.getActiveBackendUrl()); +const realWakeupStatus = ref<{ ok: boolean; message: string } | null>(null); const truncatedFcmToken = computed(() => { const t = fcmToken.value?.trim() ?? ""; @@ -420,6 +445,43 @@ async function onSimulateWakeupRefresh(): Promise { }); } +function formatRealWakeupStatusMessage( + result: Awaited< + ReturnType + >, +): string { + if (result.ok) { + const body = + typeof result.responseBody === "object" && result.responseBody !== null + ? (result.responseBody as Record) + : null; + const parts = ["Real WAKEUP_PING sent via backend."]; + if (typeof body?.message === "string" && body.message.trim()) { + parts.push(body.message.trim()); + } + if (typeof body?.tokenSuffix === "string" && body.tokenSuffix.trim()) { + parts.push(`token …${body.tokenSuffix.trim()}`); + } + return parts.join(" "); + } + const parts = [`Real WAKEUP_PING failed: ${result.errorMessage}`]; + if (result.status != null) { + parts.push(`(HTTP ${result.status})`); + } + return parts.join(" "); +} + +async function onSendRealWakeupPing(): Promise { + realWakeupStatus.value = null; + await withBusy(async () => { + const result = await NotificationDebugService.sendRealWakeupPing(); + realWakeupStatus.value = { + ok: result.ok, + message: formatRealWakeupStatusMessage(result), + }; + }); +} + async function onCopyFcmToken(): Promise { const token = fcmToken.value?.trim(); if (!token) { diff --git a/src/services/notifications/NotificationDebugService.ts b/src/services/notifications/NotificationDebugService.ts index 36ebbe17..b66deb67 100644 --- a/src/services/notifications/NotificationDebugService.ts +++ b/src/services/notifications/NotificationDebugService.ts @@ -10,6 +10,7 @@ import { Capacitor } from "@capacitor/core"; import type { PushNotificationSchema } from "@capacitor/push-notifications"; import { logger } from "@/utils/logger"; +import { getOrCreateDeviceId } from "./deviceId"; import { clearNotificationDebugLogs, logNotification, @@ -26,12 +27,17 @@ import { getLastKnownFcmToken, reregisterFcmTokenNow, } from "./firebaseMessagingClient"; +import { + getNotificationApiHeaders, + httpAuthErrorMessage, +} from "./notificationApiAuth"; import { applyNotificationRefreshPayload, handleCapacitorPushNotificationReceived, refreshNotificationsWithDiagnostics, type NotificationRefreshPayload, } from "./NativeNotificationService"; +import { truncateFcmTokenForLog } from "./notificationLog"; import { DailyNotification } from "@/plugins/DailyNotificationPlugin"; import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin"; @@ -49,6 +55,52 @@ export type PendingNotificationsResult = { inspectorUnavailableMessage?: string; }; +export type SendRealWakeupPingResult = + | { ok: true; responseBody?: unknown } + | { + ok: false; + errorMessage: string; + status?: number; + responseBody?: unknown; + }; + +function wakeupPingResponseDetail(body: unknown): Record { + if (typeof body !== "object" || body === null) { + return {}; + } + const record = body as Record; + const detail: Record = {}; + for (const key of [ + "success", + "message", + "reason", + "error", + "tokenSuffix", + "deviceId", + ] as const) { + if (record[key] !== undefined) { + detail[key] = record[key]; + } + } + return detail; +} + +function wakeupPingFailureMessage(status: number, body: unknown): string { + if (typeof body === "object" && body !== null) { + const record = body as Record; + for (const key of ["message", "reason", "error"] as const) { + const value = record[key]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + } + if (status === 401 || status === 403) { + return httpAuthErrorMessage(status); + } + return `HTTP ${status}`; +} + function isUnimplementedError(e: unknown): boolean { return ( typeof e === "object" && @@ -112,6 +164,75 @@ export const NotificationDebugService = { }); }, + /** Full pipeline: backend `/debug/send-wakeup` → FCM → native WAKEUP_PING handler. */ + async sendRealWakeupPing(): Promise { + logNotification("Real WAKEUP_PING requested"); + + const fcmToken = getLastKnownFcmToken()?.trim() ?? ""; + if (!fcmToken) { + const errorMessage = "no FCM token (register first)"; + logNotification(`Real WAKEUP_PING failed: ${errorMessage}`); + return { ok: false, errorMessage }; + } + + try { + const auth = await getNotificationApiHeaders(); + if (!auth.ok) { + logNotification(`Real WAKEUP_PING failed: ${auth.message}`); + return { ok: false, errorMessage: auth.message }; + } + + const deviceId = await getOrCreateDeviceId(); + const baseUrl = getNotificationApiBaseUrl(); + const res = await fetch(`${baseUrl}/debug/send-wakeup`, { + method: "POST", + headers: auth.headers, + body: JSON.stringify({ + deviceId, + fcmToken, + platform: Capacitor.getPlatform(), + testMode: getTestMode(), + }), + }); + + let responseBody: unknown; + try { + responseBody = await res.json(); + } catch { + responseBody = undefined; + } + + if (!res.ok) { + const errorMessage = wakeupPingFailureMessage(res.status, responseBody); + logNotification(`Real WAKEUP_PING failed: ${errorMessage}`, { + status: res.status, + token: truncateFcmTokenForLog(fcmToken), + ...wakeupPingResponseDetail(responseBody), + }); + return { + ok: false, + errorMessage, + status: res.status, + responseBody, + }; + } + + logNotification("Real WAKEUP_PING success", { + token: truncateFcmTokenForLog(fcmToken), + deviceId, + ...wakeupPingResponseDetail(responseBody), + }); + return { ok: true, responseBody }; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + logNotification(`Real WAKEUP_PING failed: ${errorMessage}`, { + token: truncateFcmTokenForLog(fcmToken), + }); + logger.warn(`${LOG} sendRealWakeupPing failed`, err); + return { ok: false, errorMessage }; + } + }, + generateMockNotifications( intervalMs: number = 60_000, ): NotificationRefreshPayload {