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() { >