fix: timer loading — zero duration, start failure, and keepScreenOn sync#10
Open
fix: timer loading — zero duration, start failure, and keepScreenOn sync#10
Conversation
**TTS not playing (audio.js + TTSBridge.kt)**
- audio.js was checking window.NativePhp?.tts?.speak which never existed
in the Android codebase — no Kotlin bridge was ever installed for it.
- Browser speechSynthesis fallback also silently failed because
getVoices() returns [] synchronously; code never waited for the
voiceschanged event, so no voice was selected.
- Fix JS side: check window.AndroidTTS?.speak (native Kotlin bridge,
see PR description for Kotlin patch) and fix the browser fallback to
wait for voiceschanged before calling speak().
**Screen wake lock never acquired (app.js + TimerScreen.php + Settings.php)**
- keepScreenOn setting was saved to DB (default true) but never consumed:
TimerScreen.php did not expose it as a Livewire property, Settings.php
did not include it in the settingsLoaded event, and app.js had no
wake lock code whatsoever.
- Fix: expose keepScreenOn on TimerScreen, pass it through settingsLoaded,
and use navigator.wakeLock.request('screen') in timerAudio Alpine
component tied to timer state transitions (acquire on active states,
release on idle/complete). Re-acquires automatically if OS releases it.
**TTS caching (TTSBridge.kt)**
- Once the native Kotlin bridge is applied, TTSBridge pre-synthesizes all
known phrases (Done, Go, Next, Get ready, 3/2/1 …) to WAV files in the
app cache directory on first launch and reloads them via MediaPlayer.
- Cache TTL is 3 days; stale/missing files are re-synthesized automatically.
- Known phrases play instantly with zero TTS engine startup latency.
Dynamic labels (e.g. custom countdown text) fall back to live TTS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**programTotalDuration = 0 / Start does nothing** When a program has no phases, Program::totalDuration() returns 0 and TimerRunner::start() throws "Program has no phases." — an exception that silently rolls back the Livewire response, leaving the UI unchanged. - TimerScreen::start() now returns early when phases are empty instead of throwing, so the component stays consistent. - timer-screen.blade.php shows an amber "No phases" notice in place of the Start button when idle and phases is empty, directing the user to edit the program. **ProgramEditor::totalDuration() showed wrong total** The editor's preview summed the last phase's cooldown, but the timer skips that cooldown (goes straight to Complete after the last rep). Fixed totalDuration() to subtract the last phase's cooldown, matching Program::totalDuration() so the displayed preview equals the actual run. **keepScreenOn not forwarded in settingsLoaded events** The TTS/wake-lock commit exposed keepScreenOn in TimerScreen and added a settingsLoaded JS handler, but loadProgram() and requestSettings() still dispatched the event without keepScreenOn. The wake lock could get stuck in the wrong state if keepScreenOn was false. Both dispatches now include keepScreenOn: $this->keepScreenOn. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Android WebView, speechSynthesis.voiceschanged may never fire, which caused the speak() fallback path to register a listener that never ran -- effectively silencing all TTS when window.AndroidTTS is not yet available (i.e. before the APK is rebuilt with TTSBridge.kt). Fix: speak immediately using whatever voices are already loaded; the default voice is used if no en-* voice is found, which is correct behaviour on Android where voice selection is handled by the TTS engine. Also replace smart-quote/em-dash characters in comments with plain ASCII to prevent future string-match failures in automated tooling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**Demo HIIT program (seeder)** - composer.json: add Database\\Seeders\\ to production autoload.psr-4 so the seeder is available in the embedded NativePHP runtime. - database/seeders/DatabaseSeeder.php: new seeder that creates the demo HIIT program (Warmup 10s / Sprint 8sx3 / Stretch 8s) when the programs table is empty. Idempotent via Program::exists() guard. Calls Setting::current() first so the settings row exists before Program construction touches it. - AppServiceProvider::boot(): call DatabaseSeeder wrapped in try/catch so the inevitable "table does not exist" error during `artisan migrate` itself is swallowed gracefully and succeeds on the next boot. **TTS diagnostic logging** JS (app.js + audio.js): - timerAudio init(): logs soundMode, keepScreenOn, and whether window.AndroidTTS is already present at page-load time. - playBeep listener: logs voiceText() output before calling speak() so an empty countdownLabel is visible before the call. - speak(): logs which path is taken (AndroidTTS bridge / speechSynthesis / neither), voice count + selection, and attaches utt.onerror so SpeechSynthesisUtterance failures surface in the console instead of silently disappearing. Kotlin (TTSBridge.kt + WebViewManager.kt -- gitignored): - WebViewManager: log after AndroidTTS bridge registration. - TTSBridge.speak(): entry log with text always fires (was missing). - TTSBridge.speak(): explicit cache-miss log before speakLive(). - TTSBridge.speakLive(): tts null-check with error log. - TTSBridge.prebuildCache(): log phrase count at start. - All Kotlin log messages prefixed [TTS] for easy Logcat filtering: adb logcat -s PHPMonitor:D PHPMonitor-Console:D TTSBridge:D Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a path-repository package that wraps Android TTS (and a Web Speech API fallback) into a NativePHP-style bridge (Kotlin + PHP + JS). Registers it via a new NativeServiceProvider and integrates it into Settings with an updateAndTest() action for in-app voice testing. Also bumps nativephp/mobile to 3.2.3 and pins axios to 1.15.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bundle - Import audioTTS package module directly instead of relying on window.AndroidTTS bridge - Add 'database' to NativePHP bundled paths so SQLite DB is included on device - Fix countdown label off-by-one (show remaining - 1 during countdown) - Allow cooldown input on last phase (was incorrectly disabled); improve helper text - Add "Demo with no phases" seed program for edge-case testing - Clean up debug logging, debugger statements, and unused imports in Settings/audio - Gitignore AI tooling files (.claude, CLAUDE.md, .agents, etc.)
Owner
Author
|
@copilot resolve the merge conflicts in this pull request |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
When a program has no phases:
Program::totalDuration()returns 0 →programTotalDuration = 0TimerRunner::start()throwsRuntimeException("Program has no phases.")— this exception silently rolls back the Livewire response, so the UI never updates and the Start button appears to do nothingThis can happen when a user creates a program and navigates to the timer without adding any phases.
Changes
TimerScreen::start()— guard against empty phasesInstead of propagating an exception from
TimerRunner::start(), the action now returns early when phases are empty. State is left untouched and the blade renders the "no phases" notice.timer-screen.blade.php— no-phases noticeWhen the timer is idle and
$phasesis empty, the Start button is replaced with an amber warning: "No phases — edit this program to add at least one phase before starting." The Edit button in the secondary row is still shown so the user can fix the program immediately.ProgramEditor::totalDuration()— match timer behaviourThe editor's preview summed every phase's cooldown, including the last phase. But the timer skips the last phase's cooldown (goes straight to
COMPLETEDafter the final rep). This made the preview total higher than the actual run duration for any program with a last-phase cooldown. Fixed to subtract the last phase's cooldown, matchingProgram::totalDuration().TimerScreen::loadProgram()/requestSettings()— forwardkeepScreenOnBoth methods dispatch
settingsLoadedto sync JS audio state, but neither includedkeepScreenOn. Thefix/tts-wakelock-cachingcommit added asettingsLoadedJS handler that readskeepScreenOnto gate wake-lock acquisition. Without it, the wake lock could be stuck in the wrong state whenever a program was loaded or settings were refreshed. Both dispatches now includekeepScreenOn: $this->keepScreenOn.Test plan
https://claude.ai/code/session_018ofANSCtBHs5P8rvvJ9Mh3