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 && (
-
)}
+ {showConfirm && setShowConfirm(false)} />}
+ {showTestNotif && setShowTestNotif(false)} />}
);
@@ -135,14 +322,20 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f
About
{showPushToggle && (
- { togglePush(); setShowMenu(false); }}>
+ { setShowMenu(false); setShowConfirm(true); }}>
{pushEnabled ? (
) : (
)}
Notifications
- {pushEnabled ? 'ON' : 'OFF'}
+ {pushEnabled ? 'ON' : 'OFF'}
+
+ )}
+ {showPushToggle && pushEnabled && (
+ { setShowMenu(false); setShowTestNotif(true); }}>
+
+ Test Notifications
)}
@@ -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) {