Changelog: Android phone
Every release of the Android app, newest first. It has had its own version number since hub 0.28; earlier releases are on the hub page, when every component shared one version. Phones are offered each release by the app's update banner.
1.29
1.29.0
Upgrade notes
- A phone already signed in keeps the hub address it registered with. To move it to a hub's new name, unregister it in Settings and sign in again with the new address: it keeps its device id, so it continues as the same satellite.
Fixes
- The sign-in screen offered a fixed address, https://birdnet.owndesign.net, wrong for every other hub and for this one since it moved to otavi.app; it offers the address of the hub the APK was built for, and
https://in a build made for no hub in particular.
Chores
build-apk.shpasses the hub's address (OTAVI_MOBILE_HUB_URL, derived fromOTAVI_APP_FQDN) into the build asMOBILE_DEFAULT_HUB_URL.
1.28
1.28.0
Features
- The settings screen can turn identification on this phone on or off (On-satellite inference), within the team's and the hub's locks; while the model is not ready, chunks still go to the hub.
- A Privacy link in the settings opens the hub's privacy page.
Chores
- React Router updated to 7.18.
1.27
1.27.0
Features
- The phone's own settings screen checks each value against the hub's limits before sending and names any that is out of range, since the hub refuses those.
- The phone says "this phone" or "satellite" where it said "device", in English and French.
Fixes
- The phone starts from the hub's defaults before its first configuration (from the shared settings registry): the upload queue at 5000 MB, 8000 MB and 720 h (it was 1000, 2000 and 168), the range filter at 0.01 (0.03) and the five pre-upload drop categories on (none).
- The settings screen's drop rows show their real state instead of always On.
1.26
1.26.0
Upgrade notes
- Picking the phone this one replaces at registration needs hub 0.63.0.
Features
- Registering asks whether this phone replaces one registered before: pick the old phone after a reinstall and it carries on with its settings and history.
- An update that interrupts a recording resumes it when the app reopens, from the next update on.
Chores
- The APK is built from pinned Android SDK and Alpine images.
1.25
1.25.0
Fixes
- The back button closes the full-screen photo, then the species sheet, before it leaves the app; it used to leave the app with the sheet still open.
1.24
1.24.0
Features
- Species photos show whole, at their own proportions.
- Tap a species photo to see it full screen: pinch or double-tap to zoom where your fingers are, drag to look around, swipe to the next photo and down to close.
- Zooming downloads the full-resolution copy when it weighs 8 MB or less (the size comes from hub 0.62.4).
1.23
1.23.0
Features
- Swipe through a species' photos in its sheet: each loads as a small square only when you reach it, and the credit follows the photo on screen.
- Species names and texts now follow the phone's language rather than the team's default, with English where the hub has no text in that language.
1.22
1.22.0
Features
- Tap the latest detection or a species in today's list to open its sheet: the photo with its credit, the times this phone heard it today, its traits and the Wikipedia summary in your team's language, with their sources.
1.21
1.21.0
Features
- On Wi-Fi while charging the phone downloads and verifies a pending release by itself, so the banner offers "Install now" and only the Android install sheet is left; nothing is fetched on mobile data until you tap.
- A release marked force update shows a blocking dialog instead of a dismissible banner.
1.20
1.20.0
Features
- The YAMNet gate drops a chunk for an excluded category (voices, dogs, machines...) only when the bird score is weak; a clear call uploads whatever surrounds it.
- The bird score includes the specific call classes.
- Whistling, breathing and domestic animals no longer count against birds.
- The silence floor defaults to off.
Chores
- The gate follows the same rule as the Pi, in shared code.
1.19
1.19.1
Performance
- The detection feed loads 128 px thumbnails framed on the bird, a few KB each. It used to download the 800 px photo for every avatar, which was slow on mobile data.
1.19.0
Upgrade notes
- Listing the teams that welcome newcomers needs a hub that lets teams open up hub 0.52.0.
Features
- The registration screen lists the teams that welcome newcomers: join an open one at once, or ask and wait for an admin to accept.
1.18
1.18.0
Upgrade notes
- Explaining why a team is unavailable at registration needs hub 0.51.0.
Features
- The registration screen now says why a team is unavailable instead of failing at submit: you are a viewer there, or the team's admins add the devices.
1.17
1.17.1
Fixes
- The recording screen showed the app name twice (the frame's title row above the header). One header now, with the device name and the app version under the wordmark.
1.17.0
Upgrade notes
- The Otavi app keeps the same Android package and signing key, so this installs as a normal one-tap update and keeps your sign-in and outbox.
- Letting team members register their phone themselves needs hub 0.50.0.
Features
- The app is now Otavi: new launcher icon (a feather on a trunk slice), a feather in the status bar while recording, new splash, and the wordmark in the header and on the sign-in and registration screens.
- Team members can now register their phone themselves; previously only team admins could.
1.16
1.16.3
Features
- The registration screen can create a team when the account has none yet (if the hub allows team creation; the creator becomes its owner).
- Vocabulary aligned on "team" / "équipe".
Fixes
- Signing in with an account that has no team no longer fails.
1.16.2
Features
- The release check now runs from the sign-in screen too (using the hub address in the URL field, default or typed), so a fresh install or a logged-out phone sees the update banner without first logging in and registering.
1.16.1
Features
- Sign-in is a real form with username / current-password hints, and the TOTP prompt is a named totp field carrying the one-time-code hint (backup codes opt out), so Android password managers pair the OTP prompt with the saved login and offer to fill it.
1.16.0
Upgrade notes
- The signing identity changed, so installing 1.16.0 over 1.15.x or earlier needs a one-time uninstall and reinstall; re-registering the phone re-attaches it to its existing satellite record.
Security
- Release APKs are signed with a dedicated offline keystore (the debug key that was committed to the repo is removed and gitignored) and built with assembleRelease, so the shipped app is no longer debuggable.
- allowBackup=false with data-extraction rules keep the stored hub key and MQTT password out of device backups.
- An explicit network-security-config forbids cleartext.
- The in-app updater downloads only over https and follows no redirects before verifying the APK's SHA-256.
1.15
1.15.1
Features
- One-tap self-update: in-app download with progress, SHA-256 verification against the release record, then straight into the system installer (opens the sideload-permission toggle on first use).
- The update banner appears within seconds of an APK upload via the MQTT update channel; re-check at launch and every 6 h as fallback.
- "Later" now skips that version until the next release; forced releases override the skip.
1.14
1.14.5
Features
- On-device range filter: device-mode classification now drops geographically implausible species using the bundled BirdNET meta model (
[lat, lon, week_48]→ per-species occurrence, same label order as the classifier; a species is kept when its score is ≥ the threshold), matching the hub. The bundled meta model adds ~15 MB to the APK. - Device results honor the tenant's
min_confidence(was a hard-coded 0.1) andrange_filter_threshold, both read from the hub config on launch and live over MQTT. - Full design-system pass (BirdNET Satellite handoff): the app gets its own theme (emerald primary, caramel secondary, the surface/text ramps, a 3-stop confidence scale), distinct from the hub web UI.
- A sticky header with the equalizer logo and a dark/light theme toggle.
- A canvas "Rings" listening pulse (animates only while recording).
- A scheme-adaptive spectrogram.
- An interactive Leaflet site map in Settings (zoom + drag-pin, placing it manually disables GPS).
- The design pass covers the Monitor chunk-audit (bars + legend + dB list) and stat sub-captions.
- The Settings sections, Event-log card, and danger-zone inline confirm are restyled to the handoff.
Chores
- Silences a benign "Directory exists" startup log (stat-first before mkdir).
1.12
1.12.3
Upgrade notes
- On-device range filtering is not applied in v1 of the phone's device mode.
Features
- On-device BirdNET inference (Phase 3) on Android, mirroring the Pi device mode: when the hub puts a phone in
deviceinferenceMode, chunks that pass the YAMNet gate are classified locally by a bundled BirdNET v2.4 FP16 model (BirdNetInferPlugin, TensorFlow Lite: raw 144000-sample waveform → 6522 logits → flat sigmoid, matching birdnetlib). - In device mode only hits upload (audio + detections together on the
inference-resultchannel), no-detection chunks drop on-device, and an unavailable model falls back to a hub-mode audio upload. - Device-mode detections ride the same store-and-forward outbox as hub-mode audio (new
detectionscolumn), so a hit survives an offline hub. - The bundled labels carry no eBird code, so the scientific name is the species key (same as the Pi).
- Inference visibility on the Live screen: an "On-device AI" / "Hub AI" badge (with a "model loading" state) and a "Hub-managed" chip when config is locked.
- A Merlin-style "Hearing a bird…" → "Heard:
" pulse on the Live screen's spectrogram (resolved locally in device mode, or on the hub's detection / a grace timeout in hub mode). - The Settings tab gains an "Identification → Runs on" row.
- Inference mode is read from the hub config on launch (REST) as well as live over MQTT, so device mode engages even when the phone connected after the mode was set.
- Device-mode detections honor the tenant's
min_confidence(previously a hard-coded 0.1), which comes from the hub satellite config. - Bird names render in the tenant's primary language via
/api/species/translate(hero card + today's list), matching the hub web UI; the language comes from the hub satellite config.
1.11
1.11.0
Features
- Live spectrogram on the recording screen: the Live tab now shows a scrolling spectrogram of the current recording under the listening pulse. The native capture thread runs a 2048-point Hann-windowed FFT per window and emits 64 frequency columns (0–12 kHz) on a
spectrumevent, and the canvas scrolls one column per window (~23/s) with an on-brand green ramp. - Spectrogram streaming is gated by native
startSpectrum/stopSpectrumand the page-visibility API, so it runs only while the card is visible and the screen is on; the capture thread keeps recording unattended without the FFT burning battery off-screen.
1.10
1.10.9
Fixes
- Fixed the tail stall (queue stuck on the last <10 chunks), a last-writer race in the outbox: a chunk is marked
sentin the callback of its QoS-1 publish (on PUBACK) andackedwhen the hub's ack arrives, and although the expected order is PUBACK then ack, near the end of a fast drain the hub's ack can runmarkAcked(status toacked) before the publish callback firesmarkSent, which then overwrote it back tosent. The hub won't re-ack an already-stored chunk, so the chunk sat stuck until the 10-minute requeue re-uploaded it, where the same race could recur, a residue that accumulates exactly at the tail of a burst.markSentnow refuses to downgrade anackedchunk (WHERE status != 'acked'), so the ordering of the two messages no longer matters. - With 1.10.8's reliable ack delivery, the queue now actually reaches zero.
1.10.8
Upgrade notes
- The persistent session pairs with the broker change in this release (
persistent_client_expiration 14dto reap sessions of decommissioned devices, explicitmax_queued_messages 10000).
Fixes
- Hub acks were being dropped, not lost in transit, which is why the last chunks never drained: the phone connected with a clean MQTT session, so every disconnect made the broker discard the phone's
/acksubscription and any queued messages, and an ack the hub publishes a few hundred ms after storing a chunk was dropped whenever the phone had disconnected in that window (constant, during the churn). Such a chunk was safely stored hub-side, but the phone never learned, so it stayedsent, counted as unacked forever and got re-uploaded every 10 minutes; roughly half of a backlog acked, half stuck, which is the pattern observed (2,010 stuck out of ~3,700). The phone now uses a persistent MQTT session (clean: false): the broker keeps its subscription and queues acks while it's briefly offline, delivering them on reconnect, so every uploaded chunk gets confirmed and the queue reaches zero.
1.10.7
Fixes
- Fixed the actual reconnect-churn root cause, confirmed by 1.10.6's instrumentation: mqtt.js's client-side keepalive watchdog, whose 30 s ping timer freezes under WebView throttling and on thaw declares the (healthy) socket dead and closes it cleanly (device logs show
MQTT offline+connection closedin the same millisecond ~90 s after each connect, while the broker-side path handles the identical workload from a server client without a hiccup). The 1.10.6 focus-handler guard lengthened connection lifetimes but could not stop the self-kill. Client pings are now disabled (keepalive: 0, MQTT-legal; the broker applies no idle timeout for such sessions), removing the throttling-sensitive watchdog entirely. - Detecting half-open sockets, the watchdog's one real job, moves to the publish path: two consecutive unconfirmed publishes (PUBACK timeout) on a nominally live connection force a socket rebuild. Liveness signals remain: heartbeats every 30 s, TCP close/error events, and the 20 s stale guard.
- The same logs also confirmed the mic live 928 ms after launch, and the paced drain confirming batches on every connection window.
1.10.6
Fixes
- MQTT reconnect churn fix candidate: the focus handler now only forces a rebuild after 20 s of genuine disconnection and otherwise lets mqtt.js's own auto-reconnect finish. The broker logs showed the phone cleanly closing its own connection every few seconds even foregrounded, while the same sustained 400 KB-publish workload from a server-side client over the identical WSS/Traefik/Mosquitto path runs clean, so the killer is app-side. The only clean-closer in the app is
forceNewConnection()in the focus handler, which fired whenever a focus or resume event found the client momentarily disconnected, including mid-auto-reconnect: each kill scheduled another 3 s reconnect for the next focus event to kill.
Chores
- Instrumentation: every forced rebuild logs how long the connection had been down, so if churn persists the log viewer will say exactly which path drives it.
1.10.5
Fixes
- The Monitor tab refreshes on its own 3 s clock: the stat cards read live values (queued count, last-sent elapsed) at render time but nothing re-rendered while paused, or while the backlog drained inside the MQTT client, so the numbers froze exactly when watching them mattered (observing the 1.10.4 backlog drain). The tick only runs while the Monitor tab is open.
1.10.4
Fixes
- The outbox drain rides the keepalive: the 10 s AlarmManager keepalive that already drives the native-queue drain now also nudges the outbox drain (requeue stale + resume paced upload), making screen-off the normal full-speed path. 1.10.3's pacing stabilized the connection (the 5 s death loop is gone from the broker logs), but the drain itself is triggered by the connect event and a 5-minute sweep, both running on timers Chromium throttles when the screen is off, so the backlog only flowed while the app was visible (observed: 1-6 chunks/min screen-off vs 20/min screen-on).
1.10.3
Fixes
- Outbox drain flow control: the drain now publishes one chunk at a time and waits for the broker's PUBACK (30 s timeout) before the next, so throughput self-paces to the link. Field evidence (2,010 unacked chunks, ~580 MB, not moving while connected; broker logs showing the client dying every ~5 s) showed each reconnect's drain blasting up to 50 chunks (~25 MB of base64) into the WebSocket with no flow control: the socket died, a handful of messages survived per cycle, and
markSentclaimed the rest until the 10-minute requeue flipped them back (connect, flood, die, repeat). sentstatus is only recorded on confirmed delivery, on the backlog path and the live path both, so a dying socket leaves chunkspendinginstead of stranding them in limbo.- An unconfirmed publish stops the drain; the next connect or sweep resumes from where it stopped.
1.10.2
Features
- Logs read newest-first in the in-app viewer (Copy/Download exports stay chronological), as the chunk audit list already did.
Fixes
- Paused no longer blocks the upload pipeline: the 1.10.1 fix held the native-queue drain while paused (to avoid the consumer discarding chunks), which traded data loss for a stalled backlog. The real fix lands instead: the consumer's paused guard is gone, safe because pause stops the capture thread, so anything draining is pre-pause audio that must be filtered, queued, and uploaded like any other chunk. Pausing now releases the mic while the backlog keeps flowing to the hub.
1.10.1
Upgrade notes
- Requires hub 0.37.4, which makes re-ingest idempotent (duplicate chunkId is acked instead of erroring, no double inference).
Fixes
- Paused drain discarded audio (one of three real holes from the chunk-lifecycle audit): the drain kept running while paused, and the consumer drops chunks when paused, after they were already deleted from the native queue, so pausing right after a screen-off backlog, or opening the app paused with a previous session's tail still queued, silently destroyed that audio. The drain now holds while paused; queued chunks wait on disk for resume.
- 'sent' chunks whose ack never arrived were stranded forever: resend only picked 'pending' rows, so a chunk published while the hub was down between receipt and ack sat in 'sent' until purge. Rows stuck in 'sent' for 10 minutes now requeue on every connect and every sweep.
- Crash-window orphans are adopted, not deleted: a crash between the audio file write and the debounced index persist left a file the 1.9.0 sweep would delete. The filename is the chunk id, so the sweep now re-inserts the row (recorded_at from file mtime) and the chunk uploads normally.
Performance
- Time-to-mic, a second, deeper pass: capture now starts before everything except the preference read.
- At launch, the outbox initialises in the background; early chunks wait in a small in-memory buffer and flush when it's ready.
- At launch, the YAMNet probe is backgrounded; the gate fails open until the model reports ready.
- Launches after the first skip the 500 ms permission probe entirely.
Chores
- A
[startup] mic live after Xmslog line lands in the device log for field verification.
1.10.0
Performance
- Instant capture at launch, the mic no longer waits for anything remote: startup previously serialized a high-accuracy GPS fix (5-30 s cold), the MQTT client setup, and two unbounded hub HTTP fetches before initializing capture, with record-on-launch firing last of all. The order is now local-first: preferences, outbox, mic (recording immediately when record-on-launch is on), then UI state.
- The MQTT socket and the hub fetches (config-lock, both now 5 s abort-bounded) fire afterwards in the background; chunks recorded before connectivity land in the outbox and upload when the socket comes up, as designed.
- The GPS fix also moved to the background: recording starts with the last-known coordinates and the live fix refreshes them when it lands.
Chores
- Removed a vestigial second update banner that compared the hub's version against the app's (dead since the per-stream versioning split because its
node:fsimport always threw in the WebView; the reorder would have armed it). The real APK update prompt (/api/mobile/latest) is untouched.
1.9
1.9.0
Upgrade notes
- Pre-1.9
.b64outbox files keep working through their legacy read path until they drain or purge.
Features
- Record on launch (Settings > Behaviour, off by default): the app auto-resumes recording when it opens, so a phone reboot or app restart in an unattended install picks capture back up without a tap.
Fixes
- Mic actually released while paused at startup: since continuous capture (1.5.0), opening the app started the native AudioRecord immediately even though the session begins paused, so the mic stayed open and chunks were recorded then discarded. The session now pauses (and releases the mic) right after capture init, reopening only on resume.
- Orphan file sweep: the 5-minute purge now deletes audio files that lost their index row (crash between file write and DB insert), mirroring the Pi's pass; previously such files were invisible to every size cap and leaked forever.
- The app-lifecycle event listeners are removed on teardown (each retry or re-registration used to stack a new set permanently).
Performance
- Outbox stores binary WAVs: new chunks are written as decoded bytes instead of base64 text, cutting on-device queue disk use by 25%.
1.8
1.8.0
Features
- Offline-first recording: an unreachable hub no longer blocks the session at startup. The MQTT connect resolves once the client is wired (mqtt.js retries every 3 s forever), capture starts regardless, and every chunk lands in the persistent outbox first.
- Chunks upload when connectivity returns; the existing 5-minute three-pass sweep (age, then hard cap, then soft cap acked-only) enforces the configured storage limits locally while offline, so the queue can never outgrow its caps. Hub-side detection feedback resumes on reconnect.
Fixes
- Notification lifecycle: the persistent notification previously appeared as soon as the registered app opened (even paused) and survived closing the app, with wake/wifi locks held and, if recording, the mic feeding a dead-end buffer. The foreground service is now bound to the recording session: it starts when recording starts and stops on pause.
onTaskRemovedreleases the mic and ends the session cleanly when the app is swiped away (START_NOT_STICKY; a resurrected service would have no WebView to upload through). Paused or closed means no notification, no locks, no mic.- The MQTT client now inspects the broker's per-topic SUBACK and logs refused subscriptions loudly instead of claiming success (the silent refusal that hid the live detection feed bug fixed in hub 0.36.3).
1.7
1.7.0
Features
- Monitor and Settings catch up with the Live tab's design language. On Monitor, the six flat stat tiles become four icon stat cards (sent, filtered, last sent, queued), dropping the Hub / GPS / Name duplicates already covered by Live and Settings.
- On Monitor, the schedule card is a pure read-only preview again, and Diagnostics (verbose logging + log viewer) closes the page.
- The recording profile picker moves to Settings, at the top of Recording Configuration, where the rest of the device config lives.
- In Settings, the unregister danger zone collapses to a single red row that expands to the typed confirmation, instead of a permanently expanded alert.
- The whole app speaks French: a minimal i18n module (
src/i18n.ts, device locale picked at startup, inline English fallback) with 177 French keys covering every screen including the Android notification texts. Species names stay hub-localised.
Chores
- The unused DetectionsFeed component (superseded by the Live screen in 1.6.0) is removed.
1.6
1.6.1
Upgrade notes
- Choosing the recording profile from the phone requires hub 0.36.2 for the hub to honor it.
Features
- Tab refinements after the 1.6.0 field test: Settings is now a true tab, so the bottom nav stays visible and the fullscreen close-me window is gone.
- Content reshuffled by intent rather than history: "Keep screen on" joins "Vibrate on detection" under Settings > Behaviour, and verbose logging + the log viewer move to Monitor as a Diagnostics card.
- Monitor loses its tap-to-record status card since Live owns start/pause.
- Device-side profile control: the recording profile is now selectable from the phone (Monitor > Recording Profile) among continuous, dawn chorus, night migration and low power. It is sent over the existing config-request channel, so a hub-side config lock turns the picker back into read-only text.
1.6.0
Features
- Detection-first UI revamp: the app was one long technical scroll where detections sat in a collapsible panel; it's now three bottom-nav tabs with everything previously shown kept intact.
- Live tab (default): an ambient listening pulse whose ring scales with the live RMS of each captured chunk (tap to pause/resume), a hero card for the latest detection (species photo from the hub's image cache, confidence ring, rare / first-of-day badges, slide-in on arrival), and a "Today" list grouped per species with counts, best confidence, last-heard time, and expandable occurrences. A slim hub/GPS dot strip replaces the stat tiles on this tab.
- Monitor tab: the previous technical surface unchanged (tap-to-record status card, stat tiles, chunk audit, schedule, keep-screen-on, outbox storage).
- Settings: the settings panel becomes a tab instead of an overlay.
- Settings gains a "Vibrate on detection" toggle (off by default so unattended field installs stay silent; uses
navigator.vibrate, new VIBRATE manifest permission). - The persistent Android notification now mirrors the latest detection ("Common Chaffinch · 87% · 14:32") instead of only throughput counters, so the phone gives feedback face-down in a field setup.
Performance
- Species thumbnails are fetched with the satellite's API key and cached per species including misses, so data usage stays at one small request per species per session.
1.5
1.5.1
Fixes
- Screen-off processing stall fixed in two parts, diagnosed from device logs after the 1.5.0 field test: chunk processing froze ~20 s after the screen went off even though the AlarmManager keepalives kept firing, while capture itself was continuous as designed (the backlog drained in bursts on screen-on, proving the native thread never stopped).
- The keepalive receiver now calls
WebView.resumeTimers()before each JS poke (idempotent, un-throttles the scheduler). Cause: Chromium throttles a hidden WebView's timer and microtask scheduling, soevaluateJavascriptfrom the keepalive still executed synchronously (its console.logs appeared throughout) but the drain'sawaiton the Capacitor bridge never got its continuation scheduled. - The drain's re-entrancy guard, which then stayed locked until the screen came back, now force-releases after 20 s so a hung bridge call can never lock processing permanently.
1.5.0
Features
- Pause releases the microphone (battery + privacy); queued chunks keep draining.
Fixes
- Continuous native capture: screen-off recording stops losing audio. The foreground service, persistent notification, wake lock, and AlarmManager keepalive all existed, but capture was orchestrated from WebView JS: each 3 s chunk was a JS-to-native
recordChunkround trip, so when Android throttled the WebView with the screen off, the loop stalled between 10 s keepalive pokes and the audio in those gaps was never recorded. - New mode in
AudioRecorderPlugin:startContinuous()runs a native thread holding one AudioRecord open, slicing gapless 3 s WAVs into an on-disk queue (filesDir/capture-queue, capped at 200 chunks ≈ 10 minutes, oldest dropped beyond that). Opening and closing AudioRecord per chunk had also left inter-chunk gaps and required the leading-silence trim hack. - JS drains the capture queue via
drainChunks()whenever it gets CPU: a 3 s timer while awake, and the existing AlarmManager keepalive while throttled. WebView stalls now delay filtering/upload instead of losing audio. - The drain also restarts the native capture thread if it died (mic stolen by a phone call, recorder error).
build-apk.shno longer shell-sources the whole.env: anSMTP_FROMcontaining<...>broke the build with a shell syntax error. The script now extracts only the two vars it needs.
Chores
- The legacy per-chunk loop remains as fallback when the native method is unavailable (web dev mode).
1.4
1.4.1
Upgrade notes
- Signing in with MFA on the phone needs hub 0.32.6 on the hub.
- Passkey support is not included in v1: TOTP + backup codes cover the existing field installs, and passkeys may be added later if needed.
Fixes
- LoginScreen now handles MFA-enabled accounts. Before 1.4.1 the mobile login flow assumed
/api/auth/loginalways returned{ token }, but for MFA-enabled accounts the hub returns{ mfaRequired: true, mfaToken }, so the app readloginData.tokenas undefined, passedBearer undefinedto/api/auth/meand got 401. Every MFA user was locked out of the mobile app. - LoginScreen gains a second step: when the password response carries
mfaRequired, it swaps to a TOTP / backup-code prompt and POSTs the code to/api/auth/login/mfa/{totp,backup}?returnToken=truehub 0.32.6. - Method toggle on the MFA step: switch between "6-digit TOTP code" and "backup code" without re-entering the password.
- "Cancel" on the MFA step returns to the password screen.
- After a successful MFA step the existing
finishLogintail (hit/api/auth/me, populate tenants,onLogin) runs unchanged.
1.4.0
Upgrade notes
- Pairs with hub 0.32.5's satellite-scoped API keys.
- Existing satellites self-migrate the first time they boot on this version (assuming the user has logged in within the last 7 days); fresh installs skip the bridge entirely.
- The user JWT (
hubToken) is kept for one release cycle so existing installs can bridge to the API key.
Fixes
- Mobile satellites no longer hold a user JWT as their long-term auth:
MobileConfig.hubApiKeyis now the preferred Bearer token for every authenticated HTTP call, and new registrations populatehubApiKeydirectly from thePOST /api/satellitesresponse. - New
hubAuthToken(config)helper resolves the right token (api key beats JWT); all three authenticated call sites (/satellites/:id/config,/satellites/:idPATCH,/satellites/:id/schedule, plus the new detections fetch) go through it. - Auto-exchange on launch (one-shot useEffect): if
hubApiKeyis missing buthubTokenis still valid, the app POSTs to/api/satellites/:id/rotate-api-key, stores the returned key in Preferences and stops using the JWT going forward. It is a silent no-op if the hub is older (no rotate endpoint, 404), the JWT has lapsed (401), or the key is already present.
Chores
- The DetectionsFeed prop
hubTokenis renamedauthTokenfor clarity.
1.3
1.3.1
Fixes
- Outbox persist no longer blows the JS stack once the sqlite blob gets large, a stability bug flushed out by phone 1.3.0 in the field:
persistIfDirtydidbtoa(String.fromCharCode(...data))wheredatais the entire sqlite export, so once the buffer grew past the WebView engine's arg-count limit (~100 KB on Android) the spread blew the stack, the persist threw and the unhandled-rejection log spammed every 5 s. It now uses aFileReader.readAsDataURLround-trip, which handles any size cleanly. The bug was pre-existing and unrelated to the detections feed, but the feed pushing new rows likely tipped the blob past the threshold. - DetectionsFeed surfaces an actionable message when the user's session JWT has expired, the second stability fix flushed out by phone 1.3.0 in the field: mobile auth currently piggybacks on the user's 7-day session JWT, and once it lapses every authenticated mobile HTTP call 401s (
config,schedule, the new detections fetch, all of it). The feed now shows "Session expirée" with the hint "reconnectez-vous depuis Paramètres" instead of the rawHTTP 401. The real fix (a satellite-scoped long-lived API key so the satellite isn't bound to a user's session lifetime) is a follow-up.
1.3.0
Upgrade notes
- Pairs with hub 0.32.4's new
/detectionMQTT topic.
Features
- New collapsible
DetectionsFeedcard on the recording screen, below the existing chunk audit: see what this phone is hearing, live. A compact header shows the last 16 status dots (success / info / warning by rare + first-of-day flags); expand it for the full list with species name, scientific name, confidence % and badges. - REST backfill on mount:
GET /api/detections?tenantId=…&satelliteId=<self>&limit=50so a cold start shows recent history, authenticated with the same Bearer-token flow as the rest of the mobile HTTP calls. - Live via MQTT:
mqtt-client.onDetection(cb)subscribes to the new/detectiontopic and prepends each event into the feed, deduplicated by server-side detection UUID and capped at 50. - Confidence shown is raw model output (matching what the MQTT payload carries). Calibration is intentionally not refetched per detection: that would need an authed roundtrip to the hub per event, and the display is already useful without it.
- No spectrogram / audio playback in v1: the intent is "what's my phone hearing right now", and verification + audio review can follow once the field-use ergonomics are known.
1.2
1.2.1
Fixes
- Override Settings: fractional inputs now actually accept decimals (RMS gate, YAMNet bird threshold and the three outbox-MB caps). The number inputs used
type="number"with noinputModehint, which on Android maps to the integer-only keypad (no "." key), so users couldn't enter0.003for RMS or0.05for YAMNet bird-prob. - Fractional fields (
step < 1) now usetype="text"+inputMode="decimal"+pattern="[0-9]*[.,]?[0-9]*"so the OS shows the decimal-capable keypad. - Comma-decimal input (
0,003from fr/de keyboards) is normalised to dot at submit time. - Mid-typing states like
"0."are preserved: the previous eagerNumber()coercion was collapsing them back to"0"on each render and stripping the decimal point as the user typed it.
1.2.0
Features
- Persistent chunk-history panel: the Chunks audit panel introduced in 1.1.0 was in-memory only, so every app restart wiped the history. It is now persisted to the existing outbox sql.js DB via a new
chunk_eventstable (capped at 200 rows by an after-insert trim), with write-through on every push. - On connect the in-memory ring is hydrated from the persisted rows, so the panel reopens populated with the last hours of activity: useful when you leave the phone recording overnight and want to see what happened in the morning without scrolling Android logcat.
1.1
1.1.0
Features
- New collapsible Chunks panel (per-chunk audit) on the recording screen surfaces the last 100 chunk events as a newest-first list; previously the recording surface only showed two opaque counters (sent / filtered).
- Chunk events have five color-coded terminal states: sent (passed RMS + YAMNet, published to the hub), live (also published to an active live-audio session, parallel path), silence (RMS below
filterMinRms), low_bird (YAMNet birdProb belowyamnetMinBirdProb) and category (an excluded YAMNet category dominates: anthropogenic / human_voice / amphibian / insect / other_animal; the dominating category name is shown on the row). - Each row carries timestamp, status badge, RMS, birdProb (when YAMNet ran) and the dominating category when applicable.
- The closed header shows a row of 16 colored dots representing recent activity, so heavy filtering stretches read at a glance without expanding.
- The panel is in-memory only (capped at 100 entries) and resets on app restart: the goal is "what's happening right now", not long-term forensics, which would need persistence and is intentionally deferred.
1.0
1.0.1
Fixes
- Mobile inherits the v0.29 web-only light-mode Alert palette fix: alerts now read as soft pastel banners with dark readable text in light mode instead of saturated shade-7 stripes. Dark mode is unchanged.
Chores
- Theme refactor, parity with web: the Mantine theme moved to a shared
@birdnet-ng/ui-themeworkspace package consumed by both web and mobile (previously a stop-gap copy ofpackages/web/src/theme.tssince the v0.27 mobile parity migration). - Mobile and web now ship the same
themeobject: palette, component vars and Alert + Badge fallbacks land on both surfaces simultaneously. - No other functional changes.
1.0.0
Chores
- Initial standalone release, functionally identical to the mobile portion of
vhub-0.28.0(Mantine UI parity, persistent outbox matching the Pi). - Future APK-side changes bump this stream independently.