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 {