From 14c80f436ad89c54d0fc54c836deb90e6e0ecb3a Mon Sep 17 00:00:00 2001
From: Ricky Stretch
Date: Mon, 23 Mar 2026 08:14:02 -0400
Subject: [PATCH] FCM test
---
backend/src/routes/push.js | 46 +++++++++++++-----
frontend/public/sw.js | 15 +++++-
frontend/src/components/ProfileModal.jsx | 62 +++++++++++++++++-------
frontend/src/utils/api.js | 2 +-
4 files changed, 92 insertions(+), 33 deletions(-)
diff --git a/backend/src/routes/push.js b/backend/src/routes/push.js
index 61521ff..9139786 100644
--- a/backend/src/routes/push.js
+++ b/backend/src/routes/push.js
@@ -112,7 +112,10 @@ router.post('/unsubscribe', authMiddleware, async (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); }
});
-// Send a test push to the requesting user's own device β for diagnosing FCM setup
+// Send a test push to the requesting user's own device β for diagnosing FCM setup.
+// mode=data (default): data-only message handled by the service worker onBackgroundMessage.
+// mode=browser: webpush.notification message handled by Chrome directly (bypasses SW).
+// Use mode=browser to check if FCM delivery itself works when the SW is not involved.
router.post('/test', authMiddleware, async (req, res) => {
try {
const subs = await query(req.schema,
@@ -130,25 +133,42 @@ router.post('/test', authMiddleware, async (req, res) => {
return res.status(503).json({ error: 'Firebase Admin not initialised on server β check FIREBASE_SERVICE_ACCOUNT in .env' });
}
+ const mode = req.query.mode === 'browser' ? 'browser' : 'data';
+
const results = [];
for (const sub of subs) {
try {
- await messaging.send({
+ const message = {
token: sub.fcm_token,
- data: {
- title: 'RosterChirp Test',
- body: 'Push notifications are working! π',
- url: '/',
- groupId: '',
- },
android: { priority: 'high' },
webpush: { headers: { Urgency: 'high' } },
- });
- results.push({ device: sub.device, status: 'sent' });
- console.log(`[Push] Test notification sent to user ${req.user.id} device=${sub.device}`);
+ };
+
+ if (mode === 'browser') {
+ // Chrome displays the notification directly β onBackgroundMessage does NOT fire.
+ // Use this to verify FCM delivery works independently of the service worker.
+ message.webpush.notification = {
+ title: 'RosterChirp Test (browser)',
+ body: 'FCM delivery confirmed β Chrome handled this directly.',
+ icon: '/icons/icon-192.png',
+ };
+ message.webpush.fcm_options = { link: '/' };
+ } else {
+ // data-only β service worker onBackgroundMessage must show the notification.
+ message.data = {
+ title: 'RosterChirp Test',
+ body: 'Push notifications are working!',
+ url: '/',
+ groupId: '',
+ };
+ }
+
+ await messaging.send(message);
+ results.push({ device: sub.device, mode, status: 'sent' });
+ console.log(`[Push] Test (${mode}) sent to user ${req.user.id} device=${sub.device}`);
} catch (err) {
- results.push({ device: sub.device, status: 'failed', error: err.message, code: err.code });
- console.error(`[Push] Test notification failed for user ${req.user.id} device=${sub.device}:`, err.message);
+ results.push({ device: sub.device, mode, status: 'failed', error: err.message, code: err.code });
+ console.error(`[Push] Test (${mode}) failed for user ${req.user.id} device=${sub.device}:`, err.message);
}
}
res.json({ results });
diff --git a/frontend/public/sw.js b/frontend/public/sw.js
index e0ba63a..1300512 100644
--- a/frontend/public/sw.js
+++ b/frontend/public/sw.js
@@ -54,6 +54,7 @@ self.addEventListener('fetch', (event) => {
let badgeCount = 0;
function showRosterChirpNotification(data) {
+ console.log('[SW] showRosterChirpNotification:', JSON.stringify(data));
badgeCount++;
if (self.navigator?.setAppBadge) self.navigator.setAppBadge(badgeCount).catch(() => {});
@@ -64,17 +65,29 @@ function showRosterChirpNotification(data) {
data: { url: data.url || '/' },
tag: data.groupId ? `rosterchirp-group-${data.groupId}` : 'rosterchirp-message',
renotify: true,
- vibrate: [200, 100, 200], // haptic pattern β also activates the OS sound channel
+ vibrate: [200, 100, 200],
});
}
// ββ FCM background messages βββββββββββββββββββββββββββββββββββββββββββββββββββ
if (messaging) {
messaging.onBackgroundMessage((payload) => {
+ console.log('[SW] onBackgroundMessage received, data:', JSON.stringify(payload.data));
return showRosterChirpNotification(payload.data || {});
});
+} else {
+ console.warn('[SW] Firebase messaging not initialised β push notifications disabled');
}
+// ββ Raw push event (fallback diagnostic) βββββββββββββββββββββββββββββββββββββ
+// Fires for every push event BEFORE the Firebase SDK handles it.
+// Log it so chrome://inspect shows whether the SW is even waking up.
+self.addEventListener('push', (event) => {
+ console.log('[SW] push event received, hasData:', !!event.data, 'text:', event.data?.text?.()?.slice(0, 120));
+ // Note: Firebase compat SDK registers its own push listener and handles display.
+ // This listener is diagnostic only β do not call showNotification() here.
+});
+
// ββ Notification click ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
self.addEventListener('notificationclick', (event) => {
event.notification.close();
diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx
index a3618f4..cf76e7e 100644
--- a/frontend/src/components/ProfileModal.jsx
+++ b/frontend/src/components/ProfileModal.jsx
@@ -180,24 +180,50 @@ export default function ProfileModal({ onClose }) {
β’ App is backgrounded when the test fires
-
+
+
+
+
+
+ Test (via SW) β normal production path, service worker shows notification.
+ Test (via Browser) β bypasses service worker; Chrome displays directly.
+