From 41c1e48874698c196297467ce23462243f9dafc4 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:19:10 -0400 Subject: [PATCH] Wake lock persistence, button reposition, docs, dev LAN support Wake lock (Prevent Sleep) toggles now persist across reloads via localStorage in both AdminView and DisplayView. Buttons repositioned inline in AdminView campaigns header bar (was floating overlay causing overlap on tablets). DisplayView buttons persist localStorage too. Wake lock acquire failure now shows toast with fix hint (HTTPS or Chrome flag). Fullscreenchange listener re-acquires wake lock (Android discards on screen off). dev-start.sh: auto-detects LAN IP (en0/en1), frontend binds 0.0.0.0, backend URL inlined as LAN IP so phones reach backend. DANGEROUSLY_DISABLE_HOST_CHECK for LAN access. Outputs LAN URL + wake lock flag instructions. Docs: README 'Prevent Sleep (Wake Lock)' section covering secure context requirement, Android Chrome flag workaround for LAN testing, iOS Safari standalone PWA bug. DEVELOPMENT.md LAN access + wake lock note. --- README.md | 20 ++++++++++ TODO.md | 2 +- docs/DEVELOPMENT.md | 8 ++++ scripts/dev-start.sh | 16 ++++++-- src/App.js | 90 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 130 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4382857..fb24170 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,26 @@ ttrpg-initiative-tracker/ * `docs/GLOSSARY.md` — domain terms (turn vs. round, etc.) * `TODO.md` — known bugs and backlog +## Prevent Sleep (Wake Lock) + +DM and Player views have a Coffee/Moon toggle to keep screen awake during combat. + +**Requires secure context** (HTTPS or localhost). The Screen Wake Lock API refuses +on plain HTTP. + +- **Prod (HTTPS):** works automatically on all devices. +- **Local dev (localhost):** works automatically — localhost is a trusted origin. +- **LAN IP (phones, plain HTTP):** wake lock fails silently. Two options: + 1. **Android Chrome flag (dev testing):** `chrome://flags/#unsafely-treat-insecure-origin-as-secure` → add full URL with port, e.g. `http://10.0.0.5:3999`. Enable flag, relaunch. + 2. **mkcert local CA:** generate trusted cert for LAN IP. Heavier setup. + +**iOS Safari:** Wake Lock API broken in standalone PWA mode (iOS 16.4–18.4, WebKit bug 254545). Fixed in 18.4+. Works in browser tab (not installed PWA). + +**Troubleshooting:** if toggle shows active but screen still sleeps, check: +- Battery saver / power save mode (browser refuses) +- Chrome devtools console for `WakeLockError` +- Secure context: `window.isSecureContext` must be `true` + ## Contributing If you want to contribute, send me a message here: [https://discourse.draft13.com/c/ttrpg-initiative-tracker/16](https://discourse.draft13.com/c/ttrpg-initiative-tracker/16), and I can add to this Gitea instance and you can feel free to fork the repository and submit pull requests. For major changes, please pose a topic to the Discourse instance above linked above first to discuss what you would like to change. diff --git a/TODO.md b/TODO.md index cbd8a64..ee69e02 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md. ## Open -fullscreen and dont lock on main app dm view and the no-game-player view +x fullscreen and dont lock on main app dm view and the no-game-player view ...and doesnt actually prevent lock on android also better vert tab layout - labelt friendly diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b26deef..4c9ad6f 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -63,6 +63,14 @@ git config core.hooksPath .githooks # enable pre-push test gate Idempotent: if port busy, leaves existing proc as-is. +**LAN access (phones):** frontend binds 0.0.0.0, LAN IP auto-detected. +Other devices hit `http://:3999`. + +**Wake Lock (Prevent Sleep) on LAN IP:** requires secure context. +Plain HTTP on LAN IP = wake lock silently fails. Android Chrome flag workaround: +`chrome://flags/#unsafely-treat-insecure-origin-as-secure` → add +`http://:3999` (with port), enable, relaunch. See README for full details. + Smoke check: ```bash curl http://127.0.0.1:4001/health # -> {"ok":true} diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index e87855b..7e1a47c 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -27,11 +27,15 @@ fi # frontend: server storage, :3999 if ! lsof -ti :3999 >/dev/null 2>&1; then - echo "starting frontend :3999..." + # detect LAN ip so other devices (phones) can reach backend + LAN_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo 127.0.0.1) + echo "starting frontend :3999 (lan ip: $LAN_IP)..." NODE_ENV=development REACT_APP_DEV_TOOLS=1 \ REACT_APP_STORAGE=server \ - REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \ - REACT_APP_BACKEND_REALTIME_URL=ws://127.0.0.1:4001/ws \ + REACT_APP_BACKEND_URL=http://${LAN_IP}:4001 \ + REACT_APP_BACKEND_REALTIME_URL=ws://${LAN_IP}:4001/ws \ + DANGEROUSLY_DISABLE_HOST_CHECK=true \ + HOST=0.0.0.0 \ BROWSER=none PORT=3999 \ nohup npm start > tmp/fe.log 2>&1 & echo $! > tmp/fe.pid @@ -51,5 +55,11 @@ done echo "" echo "backend : http://127.0.0.1:4001 (curl http://127.0.0.1:4001/health)" echo "frontend : http://127.0.0.1:3999 (admin / player /display)" +echo "lan url : http://${LAN_IP}:3999 (phones/other devices on network)" echo "logs : tmp/server.log tmp/fe.log" echo "stop : ./scripts/dev-stop.sh" +echo "" +echo "NOTE: Prevent Sleep (wake lock) requires secure context (HTTPS/localhost)." +echo " LAN IP (http) won't work unless Android Chrome flag set:" +echo " chrome://flags/#unsafely-treat-insecure-origin-as-secure" +echo " Add: http://${LAN_IP}:3999 (include port)" diff --git a/src/App.js b/src/App.js index 7d10402..844689e 100644 --- a/src/App.js +++ b/src/App.js @@ -2489,6 +2489,53 @@ function AdminView({ userId }) { ); const { data: initialActiveInfo } = useFirestoreDocument(getPath.activeDisplay()); + const [isFullscreen, setIsFullscreen] = useState(false); + const [wakeLockEnabled, setWakeLockEnabled] = useState(() => { + try { return localStorage.getItem('ttrpg.wakeLock') === 'true'; } + catch { return false; } + }); + const wakeLockRef = useRef(null); + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + } else { + document.exitFullscreen(); + } + }; + + useEffect(() => { + const onFsChange = () => setIsFullscreen(!!document.fullscreenElement); + document.addEventListener('fullscreenchange', onFsChange); + return () => document.removeEventListener('fullscreenchange', onFsChange); + }, []); + + useEffect(() => { + if (!wakeLockEnabled) { + wakeLockRef.current?.release(); + wakeLockRef.current = null; + return; + } + const acquire = async () => { + try { wakeLockRef.current = await navigator.wakeLock.request('screen'); } + catch (e) { + console.error('Wake lock failed:', e); + showToast('Prevent Sleep failed. Requires HTTPS or Chrome flag: chrome://flags/#unsafely-treat-insecure-origin-as-secure'); + } + }; + acquire(); + const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); }; + const onFsChange = () => { if (document.fullscreenElement) acquire(); }; + document.addEventListener('visibilitychange', onVisChange); + document.addEventListener('fullscreenchange', onFsChange); + return () => { + document.removeEventListener('visibilitychange', onVisChange); + document.removeEventListener('fullscreenchange', onFsChange); + wakeLockRef.current?.release(); + wakeLockRef.current = null; + }; + }, [wakeLockEnabled, showToast]); + const [campaignsWithDetails, setCampaignsWithDetails] = useState([]); const [draggedCampaignId, setDraggedCampaignId] = useState(null); const [selectedCampaignId, setSelectedCampaignId] = useState(null); @@ -2735,6 +2782,22 @@ function AdminView({ userId }) { > Create Campaign +
+ + +
{!campaignsCollapsed && ( @@ -2963,7 +3026,10 @@ function DisplayView() { const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState(''); const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); - const [wakeLockEnabled, setWakeLockEnabled] = useState(false); + const [wakeLockEnabled, setWakeLockEnabled] = useState(() => { + try { return localStorage.getItem('ttrpg.wakeLock') === 'true'; } + catch { return false; } + }); const [displayParticipants, setDisplayParticipants] = useState([]); const wakeLockRef = useRef(null); const currentParticipantRef = useRef(null); @@ -3029,8 +3095,12 @@ function DisplayView() { // Re-acquire after tab becomes visible again (browser auto-releases on hide) const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); }; document.addEventListener('visibilitychange', onVisChange); + // Re-acquire on fullscreen change (Android discards wakeLock on screen off) + const onFsChange = () => { if (document.fullscreenElement) acquire(); }; + document.addEventListener('fullscreenchange', onFsChange); return () => { document.removeEventListener('visibilitychange', onVisChange); + document.removeEventListener('fullscreenchange', onFsChange); wakeLockRef.current?.release(); wakeLockRef.current = null; }; @@ -3121,6 +3191,22 @@ function DisplayView() { if (!isPlayerDisplayActive || !activeEncounterData) { return (
+
+ + +

Game Session Paused

The Dungeon Master has not activated an encounter for display.

@@ -3153,7 +3239,7 @@ function DisplayView() { >