Biometric sign-in has never worked. auth.js drove
window.Capacitor.Plugins.NativeBiometric — the API of
capacitor-native-biometric, which is not a dependency of this project. The
installed plugin is @aparajita/capacitor-biometric-auth, registered as
BiometricAuthNative with an entirely different API and no credential
storage at all. bioPlugin() therefore always returned null, bioAvailable()
always resolved {ok:false}, and the button was never revealed.
Rewritten against what is actually installed, with no new dependency:
BiometricAuthNative (checkBiometry/authenticate) presents the prompt, and
the already-working SecureStoragePlugin — via the window.SecureStorage
wrapper — holds the credentials. Credentials are only read after
authenticate() resolves, so the OS still gates access. biometryType is a
numeric enum in this plugin, so the old FACE_ID/TOUCH_ID string maps are
replaced with a single lookup exposed as typeName.
Secure storage itself was fine and is unchanged: SecureStoragePlugin
matches the name the wrapper looks up and is registered in
capacitor.settings.gradle.
Recording across screen lock: window.nativeKeepAwake() called Capacitor's
KeepAwake plugin, which is also not installed here, so it silently did
nothing and the device slept mid-encounter — taking the WebView's
MediaRecorder with it. keepAwake() is now a method on the existing
NativeRecording JavascriptInterface, which sets FLAG_KEEP_SCREEN_ON, and
is bound to the WebView so it survives the launcher's navigation to the
remote origin. The Capacitor plugin remains a fallback.
If the screen is locked anyway (power button, incoming call), the activity
pauses and Chromium throttles timers for hidden WebViews, starving
MediaRecorder's chunk delivery. MainActivity now calls resumeTimers() on
pause while recording. The foreground service was already correct — it
holds a partial wake lock and declares FOREGROUND_SERVICE_TYPE_MICROPHONE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--push ran `git push origin HEAD`, but this repo's only remote is named
forgejo — so the flag failed after the script had already committed and
tagged, leaving the release half-done. Resolve the remote instead:
prefer forgejo, fall back to origin, else use the only remote present,
and fail with a clear message if there is none.
Also corrects the header comment, which claimed the script does not build
the APK and pointed at --gh / gh CLI. Forgejo CI builds the APK on every
branch push and attaches it to a Forgejo release on a v* tag push, which
is what Obtainium tracks. --gh is a leftover from the GitHub era.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Voice lists were a single flat set from LITELLM_TTS_VOICES, so picking a
model could leave an incompatible voice selected and the request would
fail at the gateway. Voices are now resolved per model family (Kokoro,
Kitten, Supertonic, Groq Orpheus EN/AR), with a compatibility check that
falls back through user → admin → env → first valid voice. Groq Orpheus
requests also pin response_format to wav.
Also refreshes the cardiac/respiratory auscultation samples, extends the
well-visit component, and fixes the Android launch theme background
(@null → colorPrimary) so the splash does not flash through.
NOTE: this is in-progress work that was already sitting uncommitted in
the working tree; it is committed here as-is so the tree was clean for
the release bump.
The Turnstile challenge failed reliably inside the Capacitor WebView,
which blocked login and registration from the Android app.
Three separate causes:
1. Android WebView blocks third-party cookies by default. Turnstile runs
in a cross-origin iframe from challenges.cloudflare.com and needs its
own storage, so the widget never emitted a token. MainActivity now
calls setAcceptThirdPartyCookies on the app's own WebView.
2. The register handler read the Turnstile response with an unscoped
document.querySelector, which matched the *login* widget's input (it
comes first in the DOM). Registration therefore submitted the login
widget's token — single-use with a 5 minute expiry, so any prior login
attempt or slow signup made it fail server-side.
3. The register and forgot-password widgets auto-rendered inside forms
that start at display:none, where Turnstile does not reliably complete
a challenge, and nothing re-rendered them when the form was shown.
Widgets are now rendered explicitly when their form first becomes
visible, and tokens are captured from the render callback instead of
being read back out of the injected input — which makes the unscoped
lookup in (2) structurally impossible. Added error/expired/timeout
callbacks so a widget failure surfaces the Cloudflare error code instead
of failing silently behind a generic toast.
Login is no longer gated at all. It is the path mobile users hit
constantly, and it is already covered by a 10-per-15-min per-IP rate
limit, a constant-time credential check, and TOTP 2FA. Registration and
password reset — the endpoints that actually attract bots — stay gated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>