From fe55e6481a201063ffa63e8d294a18eb383c8b2c Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Fri, 27 Mar 2026 14:38:08 -0400 Subject: [PATCH] v0.12.32 bug fixes --- backend/package.json | 2 +- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/SettingsModal.jsx | 139 --------------- frontend/src/components/UserFooter.jsx | 204 +++++++++++++++++++++- frontend/src/pages/Chat.jsx | 1 + 6 files changed, 204 insertions(+), 146 deletions(-) diff --git a/backend/package.json b/backend/package.json index e942422..b3bb960 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.31", + "version": "0.12.32", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index 13b39f2..3f23a33 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.31}" +VERSION="${1:-0.12.32}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index f0a90e0..6833a27 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.31", + "version": "0.12.32", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index 4970387..9461f94 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -193,143 +193,6 @@ function RegistrationTab({ onFeaturesChanged }) { ); } -// ── Push Debug Tab ──────────────────────────────────────────────────────────── -function DebugRow({ label, value, ok, bad }) { - const color = ok ? 'var(--success)' : bad ? 'var(--error)' : 'var(--text-secondary)'; - return ( -
- {label} - {value} -
- ); -} - -function PushDebugTab() { - const toast = useToast(); - const [debugData, setDebugData] = useState(null); - const [loading, setLoading] = useState(true); - const [testing, setTesting] = useState(false); - - const permission = (typeof Notification !== 'undefined') ? Notification.permission : 'unsupported'; - const [cachedToken, setCachedToken] = useState(localStorage.getItem('rc_fcm_token')); - const [lastError, setLastError] = useState(localStorage.getItem('rc_fcm_error')); - - const load = async () => { - setLoading(true); - try { - const data = await api.pushDebug(); - setDebugData(data); - } catch (e) { - toast(e.message || 'Failed to load debug data', 'error'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { load(); }, []); - - const doTest = async (mode) => { - setTesting(true); - try { - const result = await api.testPush(mode); - const sent = result.results?.find(r => r.status === 'sent'); - const failed = result.results?.find(r => r.status === 'failed'); - if (sent) toast(`Test sent (mode=${mode}) — check device for notification`, 'success'); - else if (failed) toast(`Test failed: ${failed.error}`, 'error'); - else toast('No subscription found — grant permission and reload', 'error'); - } catch (e) { - toast(e.message || 'Test failed', 'error'); - } finally { - setTesting(false); - } - }; - - const clearToken = () => { - localStorage.removeItem('rc_fcm_token'); - localStorage.removeItem('rc_fcm_error'); - setCachedToken(null); - setLastError(null); - toast('Cached token cleared — reload to re-register with server', 'info'); - }; - - const reregister = () => { - localStorage.removeItem('rc_fcm_token'); - localStorage.removeItem('rc_fcm_error'); - setCachedToken(null); - setLastError(null); - window.dispatchEvent(new CustomEvent('rosterchirp:push-init')); - toast('Re-registering push subscription…', 'info'); - }; - - const box = { background: 'var(--surface-variant)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '12px 14px', marginBottom: 14 }; - const sectionLabel = { fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 8 }; - - return ( -
-
Push Notification Debug
- - {/* This device */} -
-
This Device
-
- - - {debugData && } - {debugData && } - {lastError && } -
-
- - -
-
- - {/* Test push */} -
-
Send Test Notification to This Device
-

- notification — same path as real messages (SW onBackgroundMessage)
- browser — Chrome shows it directly, bypasses the SW (confirm delivery works) -

-
- - -
-
- - {/* Registered devices */} -
-
Registered Devices
- -
- - {loading ? ( -

Loading…

- ) : !debugData?.subscriptions?.length ? ( -

No FCM tokens registered.

- ) : ( -
- {debugData.subscriptions.map(sub => ( -
-
- {sub.name || sub.email} - {sub.device} -
- - {sub.fcm_token} - -
- ))} -
- )} -
- ); -} - // ── Main modal ──────────────────────────────────────────────────────────────── export default function SettingsModal({ onClose, onFeaturesChanged }) { const [tab, setTab] = useState('registration'); @@ -349,7 +212,6 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) { const tabs = [ isTeam && { id: 'team', label: 'Team Management' }, { id: 'registration', label: 'Registration' }, - { id: 'pushdebug', label: 'Push Debug' }, ].filter(Boolean); return ( @@ -373,7 +235,6 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) { {tab === 'team' && } {tab === 'registration' && } - {tab === 'pushdebug' && } ); diff --git a/frontend/src/components/UserFooter.jsx b/frontend/src/components/UserFooter.jsx index 0eb6221..2d9e947 100644 --- a/frontend/src/components/UserFooter.jsx +++ b/frontend/src/components/UserFooter.jsx @@ -1,5 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext.jsx'; +import { useToast } from '../contexts/ToastContext.jsx'; +import { api } from '../utils/api.js'; import Avatar from './Avatar.jsx'; function useTheme() { @@ -41,6 +43,175 @@ function usePushToggle() { return { permitted, enabled, toggle }; } +// ── Debug helpers ───────────────────────────────────────────────────────────── +function DebugRow({ label, value, ok, bad }) { + const color = ok ? 'var(--success)' : bad ? 'var(--error)' : 'var(--text-secondary)'; + return ( +
+ {label} + {value} +
+ ); +} + +// ── Test Notifications Modal ────────────────────────────────────────────────── +function TestNotificationsModal({ onClose }) { + const toast = useToast(); + const [debugData, setDebugData] = useState(null); + const [loading, setLoading] = useState(true); + const [testing, setTesting] = useState(false); + + const permission = (typeof Notification !== 'undefined') ? Notification.permission : 'unsupported'; + const [cachedToken, setCachedToken] = useState(localStorage.getItem('rc_fcm_token')); + const [lastError, setLastError] = useState(localStorage.getItem('rc_fcm_error')); + + const load = async () => { + setLoading(true); + try { + const data = await api.pushDebug(); + setDebugData(data); + } catch (e) { + toast(e.message || 'Failed to load debug data', 'error'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { load(); }, []); + + const doTest = async (mode) => { + setTesting(true); + try { + const result = await api.testPush(mode); + const sent = result.results?.find(r => r.status === 'sent'); + const failed = result.results?.find(r => r.status === 'failed'); + if (sent) toast(`Test sent (mode=${mode}) — check device for notification`, 'success'); + else if (failed) toast(`Test failed: ${failed.error}`, 'error'); + else toast('No subscription found — grant permission and reload', 'error'); + } catch (e) { + toast(e.message || 'Test failed', 'error'); + } finally { + setTesting(false); + } + }; + + const clearToken = () => { + localStorage.removeItem('rc_fcm_token'); + localStorage.removeItem('rc_fcm_error'); + setCachedToken(null); + setLastError(null); + toast('Cached token cleared — reload to re-register with server', 'info'); + }; + + const reregister = () => { + localStorage.removeItem('rc_fcm_token'); + localStorage.removeItem('rc_fcm_error'); + setCachedToken(null); + setLastError(null); + window.dispatchEvent(new CustomEvent('rosterchirp:push-init')); + toast('Re-registering push subscription…', 'info'); + }; + + const box = { background: 'var(--surface-variant)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '12px 14px', marginBottom: 14 }; + const sectionLabel = { fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 8 }; + + return ( +
e.target === e.currentTarget && onClose()}> +
+
+

Test Notifications

+ +
+ + {/* This device */} +
+
This Device
+
+ + + {debugData && } + {debugData && } + {lastError && } +
+
+ + +
+
+ + {/* Test push */} +
+
Send Test Notification to This Device
+

+ notification — same path as real messages (SW onBackgroundMessage)
+ browser — Chrome shows it directly, bypasses the SW (confirm delivery works) +

+
+ + +
+
+ + {/* Registered devices */} +
+
Registered Devices
+ +
+ + {loading ? ( +

Loading…

+ ) : !debugData?.subscriptions?.length ? ( +

No FCM tokens registered.

+ ) : ( +
+ {debugData.subscriptions.map(sub => ( +
+
+ {sub.name || sub.email} + {sub.device} +
+ + {sub.fcm_token} + +
+ ))} +
+ )} +
+
+ ); +} + +// ── Confirm Modal ───────────────────────────────────────────────────────────── +function ConfirmToggleModal({ enabling, onConfirm, onCancel }) { + return ( +
e.target === e.currentTarget && onCancel()}> +
+

+ {enabling ? 'Enable Notifications' : 'Disable Notifications'} +

+

+ {enabling + ? 'Turn on push notifications for this device?' + : 'Turn off push notifications? You will no longer receive alerts on this device.'} +

+
+ + +
+
+
+ ); +} + export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=false }) { const { user, logout } = useAuth(); const [showMenu, setShowMenu] = useState(false); @@ -48,6 +219,8 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f const { permitted: showPushToggle, enabled: pushEnabled, toggle: togglePush } = usePushToggle(); const menuRef = useRef(null); const btnRef = useRef(null); + const [showConfirm, setShowConfirm] = useState(false); + const [showTestNotif, setShowTestNotif] = useState(false); useEffect(() => { if (!showMenu) return; @@ -63,6 +236,12 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f const handleLogout = async () => { await logout(); }; + const handleToggleConfirm = () => { + togglePush(); + setShowConfirm(false); + setShowMenu(false); + }; + if (mobileCompact) return (
))} {showPushToggle && ( - + )} + {showPushToggle && pushEnabled && ( + )}
@@ -87,6 +272,8 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f
)} + {showConfirm && setShowConfirm(false)} />} + {showTestNotif && setShowTestNotif(false)} />} ); @@ -135,14 +322,20 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f About {showPushToggle && ( - + )} + {showPushToggle && pushEnabled && ( + )}
@@ -152,6 +345,9 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f )} + + {showConfirm && setShowConfirm(false)} />} + {showTestNotif && setShowTestNotif(false)} />} ); } diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index 298cc2e..f38572c 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -17,6 +17,7 @@ import AboutModal from '../components/AboutModal.jsx'; import HelpModal from '../components/HelpModal.jsx'; import NavDrawer from '../components/NavDrawer.jsx'; import SchedulePage from '../components/SchedulePage.jsx'; +import MobileGroupManager from '../components/MobileGroupManager.jsx'; import './Chat.css'; function urlBase64ToUint8Array(base64String) {