# Notification Debug Panel **Created:** 2026-07-07 **Updated:** 2026-09-24 **Audience:** Developers testing notification registration, AlertSearch authorization upload, and FCM delivery diagnostics on native (iOS/Android) dev builds. The **Notification Debug Panel** is a dev-only UI for FCM token registration, AlertSearch authorization upload, FCM wakeup delivery diagnostics (`/debug/send-wakeup`), and local schedule inspection. It does not schedule notifications from `WAKEUP_PING`. The production `WAKEUP_PING` → `/notifications/refresh` → `api_*` path was retired; leftover `api_*` schedules are cleared once at startup (Phase 4). --- ## Notification API base URL Notification HTTP calls (`/notifications/register`, `/notifications/alert-authorization`, `/debug/send-wakeup`, etc.) do **not** use `APP_SERVER`. They use a dedicated Notification API host, resolved at runtime by `getNotificationApiBaseUrl()` in `NotificationDebugConfig.ts`. The app does not call `/notifications/refresh`. ### Configuration constants | Symbol | Location | Purpose | |--------|----------|---------| | `VITE_DEFAULT_NOTIFY_API_SERVER` | `.env.development` / `.env.test` / `.env.production` | Build-time default Notification API URL for that Vite mode (same pattern as other `VITE_DEFAULT_*` backends) | | `DEFAULT_NOTIFY_API_SERVER` | `src/constants/app.ts` | Runtime constant: `import.meta.env.VITE_DEFAULT_NOTIFY_API_SERVER \|\| AppString.PROD_NOTIFY_API_SERVER` | | `AppString.PROD_NOTIFY_API_SERVER` | `src/constants/app.ts` | Hardcoded production fallback: `https://notify-api.timesafari.app` | | `AppString.TEST_NOTIFY_API_SERVER` | `src/constants/app.ts` | Hardcoded test host: `https://test-notify-api.timesafari.app` (for explicit UI/debug use; not the automatic fallback) | Production, test, and development builds get different Notification API URLs from their respective `.env.*` files. Runtime request code always goes through `DEFAULT_NOTIFY_API_SERVER` (via `getNotificationApiBaseUrl()`), not by reading the env var directly at each call site. Typical values today: | Build / env file | `VITE_DEFAULT_NOTIFY_API_SERVER` | |------------------|----------------------------------| | `.env.production` | `https://notify-api.timesafari.app` | | `.env.test` | `https://test-notify-api.timesafari.app` | | `.env.development` | `https://test-notify-api.timesafari.app` | ### URL resolution order `getNotificationApiBaseUrl()` selects the base URL in this order: 1. **Debug Panel backend override** — `localStorage` key `notificationDebug.backendBaseUrl` (set via **Save Backend URL** or `setBackendBaseUrl()`) 2. **`VITE_DEFAULT_NOTIFY_API_SERVER`** — baked into the build as part of `DEFAULT_NOTIFY_API_SERVER` 3. **`AppString.PROD_NOTIFY_API_SERVER`** — hardcoded fallback when the env var is unset (`https://notify-api.timesafari.app`) Clearing the Debug Panel override (empty field + Save) returns the app to step 2 / 3 (`DEFAULT_NOTIFY_API_SERVER`). The override never changes auth behavior by itself. `APP_SERVER` / `VITE_APP_SERVER` remain for deep links and the main app web host only — not for notification API traffic. --- ## Access 1. Use a **non-production** build (for example `build:android:dev`, `build:ios:dev`, or `vite dev` with a non-`production` mode). 2. Open **Account** → enable **Show All General Advanced Functions**. 3. Open **Notification Debug Panel** (route `/dev/notifications`). On native platforms, grant notification permission when prompted so FCM token registration and the debug actions work. --- ## Configuration (Backend Testing) Settings persist in `localStorage` via `NotificationDebugConfig.ts`: | Key | Default | Purpose | |-----|---------|---------| | `notificationDebug.backendBaseUrl` | *(unset — use `DEFAULT_NOTIFY_API_SERVER`)* | Override which notification server receives API calls | | `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) | | `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls | All notification API requests (`/notifications/register`, `/notifications/alert-authorization`, `/debug/send-wakeup`, etc.) obtain headers through `getNotificationApiHeaders()` in `notificationApiAuth.ts`. ### Notification Backend URL Paste a base URL (no trailing slash) and tap **Save Backend URL**. This changes **only** which server the app calls (`getNotificationApiBaseUrl()`). It does **not** disable JWT authentication. Leave empty to use the configured build default (`DEFAULT_NOTIFY_API_SERVER`, from `VITE_DEFAULT_NOTIFY_API_SERVER` or `AppString.PROD_NOTIFY_API_SERVER`). The Debug Panel override still wins whenever a non-empty URL is saved. ### Test Mode When enabled (default if never saved), register and send-wakeup requests include `"testMode": true` in the JSON body. The backend can use this to route test traffic separately from production. Test Mode is **independent of authentication**. It does not control whether `Authorization` headers are sent. ### Skip JWT Authentication (Local Development Only) When **off** (default), the app resolves the active DID and sends `Authorization: Bearer …` on notification API calls. When **on**, requests include only `Content-Type: application/json` — for local servers (localhost or ngrok) that intentionally accept unauthenticated notification requests during development. Enable this **only** for local development backends that do not require JWT. Hosted shared test servers that require normal app authentication should leave this **off**. The panel **Backend Status** section shows the active URL, `testMode`, and `bypassAuth` values. --- ## Recommended settings ### Hosted test server Example: `https://test-notify-api.timesafari.app` On development and test builds, this host is already the default via `VITE_DEFAULT_NOTIFY_API_SERVER`. You can leave **Notification Backend URL** empty, or paste the same URL explicitly. | Setting | Value | |---------|-------| | **Notification Backend URL** | Empty (use default) or `https://test-notify-api.timesafari.app` | | **Test Mode** | **ON** (if the server expects `testMode: true`) | | **Skip JWT Authentication** | **OFF** | Ensure the app has an **active identity (DID)** with a valid endorser session so JWT headers can be built. ### Local localhost / ngrok development Example: `https://abc123.ngrok-free.app` or `http://127.0.0.1:3000` | Setting | Value | |---------|-------| | **Notification Backend URL** | Your local or ngrok URL | | **Test Mode** | **ON** or **OFF** — match what your local **notification-wakeup-service** expects | | **Skip JWT Authentication** | **ON** only if the local server accepts unauthenticated requests; **OFF** if it validates JWT like production | --- ## Backend Testing actions | Action | What it does | |--------|----------------| | **Register Token Now** | `POST {backend}/notifications/register` with current FCM token, `deviceId`, `platform`, and `testMode`. Forces re-registration (bypasses duplicate-token skip). | | **Upload AlertSearch Authorization** | Mints and uploads delegated AlertSearch JWTs (`POST /notifications/alert-authorization`). Requires an active `did:ethr` identity; Test Mode is not used. | | **Send Real WAKEUP_PING** | `POST {backend}/debug/send-wakeup`; server sends a real FCM data message with `data.type = "WAKEUP_PING"`. FCM delivery diagnostic only — the app logs `push handler ignored type=…` and does **not** call `/notifications/refresh` or schedule `api_*` notifications. Background the app before expecting delivery. | **Current FCM Token** displays the last token from Capacitor/Firebase registration. **Event Log** shows the last 100 `[Notifications]` messages (also visible in logcat / Xcode console on native). --- ## Other panel sections | Section | Purpose | |---------|---------| | **Pending Notification Inspector** | Lists locally scheduled notifications (Daily Reminder, New Activity / dual, and any leftover `api_*` until Phase 4 cleanup). | | **Clear Notifications** | Clears/cancels plugin-scheduled notifications on native. Does not replace the one-time `api_*` startup cleanup. | --- ## Programmatic override (optional) From a WebView dev console (`chrome://inspect` on Android, Safari Web Inspector on iOS): ```javascript import { setBackendBaseUrl, setTestMode, setBypassAuth, getNotificationApiBaseUrl, } from "@/services/notifications"; setBackendBaseUrl("https://abc123.ngrok-free.app"); setTestMode(true); setBypassAuth(true); // local dev only getNotificationApiBaseUrl(); ``` --- ## Troubleshooting ### 401 Unauthorized (`registerToken failed: unauthorized`) **Likely causes:** JWT required but **Skip JWT Authentication** is off and the session is missing or expired; or JWT sent but the server rejected it. **Checks:** 1. Panel **Backend Status** → `bypassAuth: false` for hosted servers. 2. App has an active DID and endorser login. 3. Event Log: look for `Using authenticated notification request` vs `Using debug unauthenticated notification request`. 4. For hosted test server: keep **Skip JWT Authentication** **OFF**. **Fixes:** Sign in / restore identity; refresh endorser session; for local ngrok without JWT support, enable **Skip JWT Authentication**. ### `registerToken auth unavailable` / `Waiting for auth before registration` The app deferred registration because JWT could not be built (no active DID or empty token) and **Skip JWT Authentication** is **off**. **Fixes:** Complete identity setup in the app, or enable **Skip JWT Authentication** only for an intentionally unauthenticated local backend. ### Failed to fetch / network error **Likely causes:** Backend down, wrong URL, stale ngrok tunnel, device offline, or TLS/certificate issues. **Checks:** Panel **Backend Status** URL; `curl -sS "$URL/health"` from your machine; ngrok inspect UI for incoming requests. **Fixes:** Restart backend and ngrok; **Save Backend URL** with the current HTTPS forwarding URL (no trailing slash). ### Backend unreachable / no requests in ngrok Same as above. Confirm the **Active** URL in the panel matches your running tunnel or local server port. ### Register succeeds but Send Real WAKEUP_PING does not show delivery **Real WAKEUP_PING success** only means the backend accepted the wakeup request and attempted FCM delivery. It does **not** schedule local `api_*` notifications. Delivery is confirmed when logcat / Event Log shows `push handler ignored type=WAKEUP_PING` (the retired refresh chain is gone). **Checks:** App backgrounded (not force-stopped); FCM token matches registration; Firebase / Play services available. See platform-specific guides for extended ngrok and FCM workflows: - [local-android-testing-ngrok.md](./local-android-testing-ngrok.md) - [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) --- ## Key source files | File | Purpose | |------|---------| | `src/constants/app.ts` | `DEFAULT_NOTIFY_API_SERVER`, `PROD_NOTIFY_API_SERVER`, `TEST_NOTIFY_API_SERVER` | | `src/components/dev/NotificationDebugPanel.vue` | Dev UI | | `src/services/notifications/NotificationDebugConfig.ts` | Base URL resolution, testMode, bypassAuth persistence | | `src/services/notifications/notificationApiAuth.ts` | JWT vs unauthenticated headers | | `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate | | `src/services/notifications/NotificationDebugService.ts` | Panel action handlers | | `src/services/notifications/NotificationService.ts` | `POST /notifications/register` | | `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) | --- ## Related docs - [notification-system-overview.md](./notification-system-overview.md) - [notification-from-api-call.md](./notification-from-api-call.md) - [local-android-testing-ngrok.md](./local-android-testing-ngrok.md) - [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md)