diff --git a/backend/package.json b/backend/package.json
index a9e3c62..ea57a11 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-backend",
- "version": "0.12.35",
+ "version": "0.12.36",
"description": "RosterChirp backend server",
"main": "src/index.js",
"scripts": {
diff --git a/build.sh b/build.sh
index 25c89a7..8775b7c 100644
--- a/build.sh
+++ b/build.sh
@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
-VERSION="${1:-0.12.35}"
+VERSION="${1:-0.12.36}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="rosterchirp"
diff --git a/frontend/package.json b/frontend/package.json
index b29a0a2..06ba45c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-frontend",
- "version": "0.12.35",
+ "version": "0.12.36",
"private": true,
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/UserFooter.jsx b/frontend/src/components/UserFooter.jsx
index a6c5322..7339cc3 100644
--- a/frontend/src/components/UserFooter.jsx
+++ b/frontend/src/components/UserFooter.jsx
@@ -63,15 +63,19 @@ const isMobileDevice = isIOS || isAndroid;
function TestNotificationsModal({ onClose }) {
const toast = useToast();
+ const { user } = useAuth();
+ const isAdmin = user?.role === 'admin';
const [debugData, setDebugData] = useState(null);
- const [loading, setLoading] = useState(true);
+ const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false);
-
- const permission = (typeof Notification !== 'undefined') ? Notification.permission : 'unsupported';
+ const [permission, setPermission] = useState(
+ (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 () => {
+ if (!isAdmin) return; // debug endpoint is admin-only
setLoading(true);
try {
const data = await api.pushDebug();
@@ -83,7 +87,22 @@ function TestNotificationsModal({ onClose }) {
}
};
- useEffect(() => { load(); }, []);
+ useEffect(() => { load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const handleGrantPermission = async () => {
+ if (typeof Notification === 'undefined') {
+ toast('Notifications not supported on this device/browser', 'error');
+ return;
+ }
+ const result = await Notification.requestPermission();
+ setPermission(result);
+ if (result === 'granted') {
+ window.dispatchEvent(new CustomEvent('rosterchirp:push-init'));
+ toast('Permission granted — registering…', 'success');
+ } else {
+ toast('Permission denied', 'error');
+ }
+ };
const doTest = async (mode) => {
setTesting(true);
@@ -112,6 +131,7 @@ function TestNotificationsModal({ onClose }) {
const reregister = () => {
localStorage.removeItem('rc_fcm_token');
localStorage.removeItem('rc_fcm_error');
+ localStorage.removeItem('rc_webpush_endpoint'); // clear iOS webpush cache too
setCachedToken(null);
setLastError(null);
window.dispatchEvent(new CustomEvent('rosterchirp:push-init'));
@@ -147,6 +167,11 @@ function TestNotificationsModal({ onClose }) {
{!isIOS && debugData &&