Roadmap
What's been released and what's coming next.
Released
Full web i18n sweep + docs refresh (web)
Swept every page for straggler hardcoded strings (Verification was largely un-internationalized); EN/FR dictionaries now 1567 keys at parity, every code-referenced key present. t() gained {placeholder} substitution.
Outbox lifecycle test suites (vhub / vsat / vapk)
Vitest coverage for the chunk-lifecycle code rewritten while chasing the upload bugs: status transitions, the markSent/markAcked tail-stall race, stale-sent requeue, the capacity-bounded purge passes (unsent data protected), and orphan sweep/adoption. Real sql.js in both Pi and mobile; the hub suite adds tenant-isolation and security-hardening matrices over an ephemeral Postgres.
Mobile upload reliability + instant capture (vapk-1.5.0 → 1.10.9)
The phone satellite hardened end to end:
- Continuous native capture that survives screen-off WebView throttling; mic live <1 s after launch, fully decoupled from connectivity.
- Offline-first: chunks buffer in a persistent outbox and upload when the hub returns; the queue now drains to zero. Fixes along the way: paced PUBACK-gated drain (no socket-flooding), a persistent MQTT session so hub acks survive reconnects, disabled client pings (the throttled keepalive watchdog was self-killing sockets), and the markSent/markAcked race that stranded the last chunks.
- Detection-first UI (Live / Monitor / Settings tabs), notification mirroring, record-on-launch, clean foreground-service lifecycle.
Quick wins (vhub-0.37.6)
- Satellite-offline alerts fire once per outage (
on_state_change) instead of hourly. - Verification page: playback cursor tracks the audio over the spectrogram (removed a duplicate native player).
Webhook secret encryption + satellite-key enforcement (vhub-0.37.1, 0.37.2)
Webhook signing secrets sealed at rest (AES-256-GCM); satellite-scoped API keys enforced so a key only touches its own satellite; the dead legacy tenant-webhooks table and routes removed in favour of alert channels.
Tenant-isolation hardening (vhub-0.36.3 → 0.37.5)
Every UUID-addressed route re-anchors authorization on the resource's own tenant (404 on mismatch, no UUID probing); create routes and vote/annotation identity hardened. Plus: idempotent re-ingest, recording-time detected_at, the live-detection-feed broker ACL fix, and bounded BullMQ job retention (Redis was at 680 MB from un-trimmed jobs).
Deep health checks + inference-worker hardening (vhub-0.37.0)
/api/health/ready probes every dependency; Docker healthchecks wired for all hub-stream containers. Worker gets a Postgres connection pool, retry backoff, cache eviction, download size guard, persistent model cache, and resource limits.
Account-event emails (vhub-0.34.x)
Welcome-on-register, account-locked, and password-reset emails over the existing SMTP plumbing; mailer fails open when SMTP is unset.
Inference worker self-healing (vhub-0.32.8, vhub-0.32.9)
Two fixes for silent inference outages.
- vhub-0.32.8: status reporter counts consecutive Redis failures, exits after 5 (75 s) so Docker restarts the container. Caught the case where DNS to
redis:6379failed and BullMQ wedged in an internal reconnect loop, no process exit. - vhub-0.32.9: status reporter also tracks the processed counter, exits if it stalls for 5 minutes against a non-empty queue. Covers the case where Redis is reachable but BullMQ is wedged for some other reason.
Mobile APK download links in UI (vhub-0.32.7)
GET /api/mobile/download/:id was already public, but nothing linked to it. Added a per-version Download button on PlatformSettings → Mobile Releases (platform admin) and a "Get mobile app" button on the Satellites page header (any tenant member) that fetches /api/mobile/latest and anchors to the result. Lets tenant admins onboard new phones without platform-admin access.
MFA on mobile (vhub-0.32.6 + vapk-1.4.1)
MFA-enabled accounts were locked out of the mobile app: /api/auth/login returned {mfaRequired, mfaToken} and the mobile code read loginData.token as undefined, passing Bearer undefined to /api/auth/me. Added a TOTP / backup-code prompt after the password step. The hub-side change extends /api/auth/login/mfa/{totp,backup} with ?returnToken=true so non-cookie clients get the post-MFA session JWT in the body.
Satellite-scoped API keys (vhub-0.32.5 + vapk-1.4.0, migration 072)
Pre-072 the mobile satellite stored the registering user's session JWT as its long-term auth. JWTs expire after 7 days, so every authenticated mobile HTTP call silently 401-ed at the same time. The fix mints a non-expiring per-satellite key at registration.
api_keys.satellite_idUUID with FK tosatellites.id, cascade on delete (migration 072). Old tenant-scoped keys keepsatellite_id = NULL.POST /api/satellites(all three registration paths) now returns{ ..., apiKey }. Raw key is shown once; SHA-256 hash in storage.POST /api/satellites/:id/rotate-api-key(member+) for explicit rotation. Used by the mobile auto-exchange path.- Mobile
MobileConfig.hubApiKeyis preferred overhubTokeneverywhere via ahubAuthToken(config)helper. Auto-exchange on launch: ifhubApiKeyis missing buthubTokenis still valid, mobile POSTs torotate-api-keyand stores the result. - Auth resolver populates
request.auth.satelliteIdfrom the key row (not enforced yet; tenant scope still gates routes).
Live detection feed on mobile (vhub-0.32.4 + vapk-1.3.0/1.3.1)
The mobile recording screen gains a "Détections" card showing what this phone is hearing in real time. Pairs with a new MQTT topic.
- New
/detectionMQTT topic. The hub'sDetectionPublisherservice (dispatcher / full mode) subscribes to the in-processeventBusand republishes eachDetectionEventtobirdnet/{tenant}/{satellite}/detectionQoS 1. Confidence is raw (the watcher emits before the calibration curve is bulk-loaded). - Mobile component
DetectionsFeed: cold-start REST backfill via/api/detections?satelliteId=<self>&limit=50, then live MQTT prepend. Dedup by server-side UUID, cap 50. - vapk-1.3.1 fixed two field bugs the v1.3.0 feed exposed: the outbox persist
btoa(String.fromCharCode(...data))blew the WebView stack once the sqlite blob crossed ~100 KB (replaced withFileReader.readAsDataURL), and 401 errors now show "Session expirée, reconnectez-vous depuis Paramètres" instead of "HTTP 401".
Outbox usage card UI fix + reporting truth (vhub-0.32.3 + vsat-1.0.2)
The Satellite detail page outbox card had two problems on the same screen.
- At 14 MB used vs a 5 GB hard cap the colored fill was sub-pixel and only the soft-cap marker was visible, which users naturally read as "current usage". The bar gains an always-visible 2 px current-usage tick coloured by threshold.
- "Used" only counted tracked outbox bytes. A Pi holding 3.5 GB of orphan WAVs still showed "14 MB" because
outboxStats.totalBytessumssize_bytesfrom outbox rows only. vsat-1.0.2 addsoutboxStats.diskBytes(real${dataDir}/audio/byte count), and the hub UI now showsmax(diskBytes, totalBytes)plus a separate "Untracked (pending orphan sweep)" segment whenever the delta is nonzero.
Pi orphan sweep + persistent filter counters (vsat-1.0.1, vsat-1.0.3)
The outbox purge was deciding everything from SELECT SUM(size_bytes) FROM outbox. WAVs on disk that no row referenced (filter-time unlinkSync failures, crash between WAV write and enqueue) were invisible to it, so a Pi could run past outbox_hard_size_mb indefinitely. Two follow-ups:
- vsat-1.0.1: added an orphan pass at the head of the four-pass sweep that deletes WAVs in
${dataDir}/audio/not referenced by any outbox row, with a 5-minute min-age guard to avoid racing with mid-pipeline writes. - vsat-1.0.3: filter pipeline counters (
totalProcessed,totalSent,rejected*) now persist to${dataDir}/filter-stats.jsonvia a 5 s debounced flush, with a clean-shutdown drain on SIGINT / SIGTERM. Pre-1.0.3 they lived only in process memory, so the hub UI's "Filtre audio" card snapped back to zero on every Pi reboot.
System alert rules seeded on tenant creation (vhub-0.32.2)
The 4 system rules (rare_species, first_of_day, first_of_season, satellite_offline) were seeded by migration 038 once at hub startup. Tenants created after that migration ran got nothing, so their Alerts → Rules tab was empty and no system-driven alerts fired. Extracted seedSystemAlertRules(db, tenantId) to services/alert-rules-engine.ts and wired it into POST /api/tenants. Idempotent; the migration uses the same shape.
v0.32.1 — Wilson 95% CI on precision dashboard + retroactive recalibrate script (hub stream)
- Per-species precision dashboard (Compare Models page) now shows the Wilson 95% confidence interval next to each percentage —
78% (62–88)instead of78%*. Wilson is the right choice for binomial proportions at small n; the previous binary<10 verifiedasterisk overstated the discreteness of the threshold. Rows with a ≥40-percentage-point CI band grey out, with a footer note explaining. - New
recalibrate-historical.tsone-shot script (pnpm --filter @birdnet-ng/hub recalibrate:historical [--days=N]) re-walks every historical detection and recomputestrust_scoreagainst the calibration curve. Trust score for detections older than 10 minutes pre-v0.31 was computed from raw confidence; the v0.31 watcher only handles the last-10-minutes window. This script makes the v0.31 behavior retroactive. Idempotent.
v0.32 — Effort-aware rarity flag (hub stream)
Inspired by Johnston 2021 (Diversity & Distributions 27:1265). The eBird-driven rarity flag previously treated absence-from-recent-obs as binary evidence; per the paper this is much weaker at low-effort locations than at hotspots — a remote rural site sees few checklists, so "no nearby observations" might just mean "nobody looked".
- New
detections.rarity_confidencecolumn (migration 071) ∈ {high,medium,low,null} carries the strength of the flag based on nearby checklist count: high ≥50 in past 30d, medium 10–49, low 3–9, no flag at all below 3. EBirdService.getEffortSummary(lat, lng)computes the count from the same/data/obs/geo/recentresponse we already fetch — distinctsubId(checklists) +locId(locations) — so no extra API hits.- Rare badges in Verification, SpeciesProfile, and SharedDetection render with confidence-adjusted styling: high = filled, medium = light, low = outline / "?" suffix. Tooltips on every surface explain the underlying checklist count.
- Watchlist / first-sighting / few-sighting flags stay at high confidence — those are grounded in our own data, no sampling-effort dimension.
v0.31 — v0.30 follow-ups + trust score on calibrated confidence (hub stream)
- Trust score now uses calibrated confidence. Both the inline at-creation path and the periodic 5-min rescore look up the per-(tenant, model) calibration curve and apply it before feeding
compute_trust_score. Falls back to raw when no curve has been fit. A "0.85 raw" call from a known over-confident model (calibrated to e.g. 0.55) now contributes proportionally less to the trust logit instead of compounding raw bias. The periodic rescore bulk-loads curves for the batch's distinct (tenant, model) pairs. - Annotation overlays on spectrograms. The standalone
SpectrogramandSpectrogramPlayeraccept anannotationsprop and render colored rectangles per annotation (green confirm / red reject / amber unsure). Verification page now fetches annotations for the currently-shown detection and overlays them so the voter sees where prior reviewers pinpointed before voting. - Calibration curve viewer. Coverage rows on the Models-tab calibration card are click-to-expand; expanded rows render an inline SVG step plot of the fitted bucket curve vs a dashed y=x reference line. Bucket dots are sized by sample count (more verified samples = bigger dot). Lets admins eyeball whether the calibration is sensible at a glance.
- Cost hint when
species_extra_images_count > 10. A yellow warning under the input projects "≈ N MB per species" using the live image cache average bytes/file as the cost estimate, plus the existing cache size as a sanity reference.
v0.30 — Confidence calibration + time-boxed verification annotations (hub stream)
Two Cornell-inspired pipeline upgrades.
- Per-(tenant, model) confidence calibration via isotonic regression. BirdNET's raw confidence is a softmax-style score, not a real probability — Cornell's Johnston 2021 paper documents the same pattern for any classifier and proposes monotonic re-alignment with observed frequencies. We do the equivalent with Pool Adjacent Violators (the textbook isotonic regression), fit on accumulated confirmed/rejected verification votes. Per-tenant because acoustic environments differ; per-model because different BirdNET versions have different score distributions. Stored in
confidence_calibration(migration 069); the detection list bulk-loads the curve and decorates each row withcalibrated_confidence. UI showsConf 78% (62%)inline. New "Confidence calibration" card on the Models tab triggers refreshes; coverage atGET /api/calibration/coverage. Trust score still uses raw confidence — promoting it onto calibrated is a deferred behavior change. - Time-boxed verification annotations, inspired by Cornell's Merlin training-data methodology. The Verification page gains a "Pinpoint the call on the spectrogram" button — drag a rectangle on the SpectrogramPlayer canvas marking exactly when + at what frequency you hear the species, then cast Yes/No/Unsure with optional notes in one shot. Stored in
detection_annotations(migration 070) withtime_start_ms / time_end_ms / freq_low_hz? / freq_high_hz? / vote / notes; mirrors averification_votesrow so the existing consensus-promotion logic still fires. Builds a labeled bounding-box dataset opportunistically as users verify, no extra workflow. - 2 new migrations (069, 070).
v0.29 — iNaturalist as second image source, Platform Settings tabs, cross-tenant admin views, retention correctness pass (hub stream)
- iNaturalist opt-in fallback for gallery extras (
inaturalist_enabledplatform setting). Wikipedia is always the primary source; iNat fills only what Wikipedia is missing. License-gated (default = commercial-safecc0/cc-by/cc-by-sa); admin can widen to includecc-by-ncvariants. Per-provider exhaustion markers (extras_exhausted_atfor Wikipedia,inaturalist_exhausted_atfor iNat — migration 068). - License code captured + displayed:
species_image_files.license_codeandspecies_images.license_codepopulated from extmetadata; lightbox attribution pill shows the license as a small uppercase badge.pnpm --filter @birdnet-ng/hub backfill:licensesre-queries Wikimedia for the ~1700 pre-068 rows that came in license-blank. - Platform Settings → 5 hash-deep-linked tabs: Access · Images · Storage · Models · Mobile. Active tab persists in
location.hash. Spotlight gains one entry per tab withkeywords[]so searches like "Wikimedia" / "Retention" / "APK" / "iNaturalist" jump straight to the right tab. - Storage tab regrouped into 3 cards: Storage overview (total bar + soft/hard cap inputs + state hint), Audio (protected/unprotected split bar + 4 chunk stats + retention rules + run-now), Other caches (Images / Models / APKs proportional bar + gallery extras + APK keep-N). Each card has its own scoped Save action.
- Protected audio guarantee tightened: hard-cap purge pass now also excludes
_retention_protected(keep-best + pinned). Both passes are eviction-by-age over unprotected only — soft / hard differ only in target threshold. If protected alone exceeds the hard cap, capacity stays exceeded; admins must lower keep-best per species or unpin to reclaim. - APK bytes now factor into the cap math (previously the dashboard counted them but the purge logic didn't, so stale APKs could push the bar over without triggering a sweep).
- Spotlight species index: every detected species in the active tenant becomes a Spotlight result (capped at 1000, sorted by detection count). Multilingual matching — each species action's
keywords[]carries EN common name + scientific name + species_code + every translation already loaded for the user's primary + secondary languages. - Cross-tenant platform-admin views: Members and Alerts inbox accept an optional
tenantIdfrom platform admins (route 400s for tenant admins as before). When platform admin is on "Toutes les organisations", Alerts inbox + Members tables span every tenant with an "Organisation" column; Members invite form gains a tenant Select picker. - Two cross-tenant bug fixes: detections list 500'd on platform-admin "all tenants" because pg-pool reads
paramslazily andparams.push(limit, offset)mutated the bind for the count query; alerts inbox showed 0 while the bell badge showed 50 because the inbox fell back totenants[0]instead ofundefined(matching the bell's behavior). - System page → Image provider rate limits panel: Wikimedia (10 000 req/h with auth, 500 req/h without) + iNaturalist (self-throttled 80 req/min) shown side-by-side with usage bars and paused-until indicators. Wikimedia counter is now Redis-backed (sorted set) so it survives dispatcher restarts; previously the in-memory ring zeroed on every redeploy.
recordRequest()no longer counts CDN fetches (upload.wikimedia.org) or iNat photo downloads — only Action API hits, which is what the 10K/hr budget actually applies to. - Image cache top counter sums all cached images (primary + extras across both providers) instead of only primary species rows. New "By source" panel shows Wikimedia / iNaturalist primary + extras side by side.
- Theme: light-mode Alert palette fix at the theme level — Alerts now read as soft pastel banners on light pages instead of saturated dark stripes (matching the Badge pattern that was already there). Cache stat cards: échouées as warning amber when > 0, introuvables as danger red when > 0.
- i18n big sweep: 176 missing user-facing strings across 10 prefixes pulled from inline fallbacks into
en.json+fr.json(theme, language, search, nav, alerts, common, detections, models_compare, platform_settings, satellites). FR-only stale keys pruned; en + fr now symmetric. - 20-cap removed from
species_extra_images_count— admins choose how many gallery extras per species to keep. - 1 new migration (068):
license_code+inaturalist_exhausted_at+sourcecolumns.
v0.28 — Per-stream versioning, mobile UI parity, mobile outbox parity (hub stream)
- Three independent SemVer streams:
hub/satellite/mobile. A hub patch no longer flags every satellite as outdated; each stream tags + bumps separately (vhub-X.Y.Z/vsat-X.Y.Z/vapk-X.Y.Z). pnpm version:bump --scope <hub|satellite|mobile>replaces the all-in-one bump; refuses to bump scopes with no changes since the last scoped tag.- New
GET /api/system/versionsreturns{hub, satellite, mobile}— Satellites + SatelliteDetail pages compare per-stream. - Heartbeat carries a
streamfield; the hub stores it insatellites.stream(migration 067) and uses it to pick the right comparison target. - Pi
birdnet-update.shchecks out the latestvsat-*tag instead oforigin/main; hub-only bumps no longer trigger Pi rebuilds. - Mobile
versionCodederives deterministically from the SemVer (major*1_000_000 + minor*1_000 + patch); the+b<timestamp>suffix on uploaded APK versions is retired. - Capacitor mobile app fully migrated to Mantine v9 — same theme, palette, components, light/dark scheme as the hub web UI.
- Mobile gains a persistent on-device outbox (sql.js + Capacitor Filesystem) with three-pass purge that mirrors the Pi's
OutboxQueue./ackMQTT messages now flip chunks to acked status; soft pass evicts those first.
v0.27 — MFA, Mantine UI, Image Originals
- Multi-factor authentication: TOTP (RFC 6238) + WebAuthn passkeys + 10 single-use backup codes
- AES-256-GCM at-rest encryption of TOTP secrets via
MFA_ENCRYPTION_KEY - Mandatory MFA enrollment for platform admins (forced full-page gate before AppShell)
- "Remember this device for 30 days" cookie (sha256-hashed at rest, listed/revocable on /account)
- Per-mfaToken attempt cap (5 wrong → token burned) + global 5-strike user lockout
- Platform-admin reset action on PlatformUsers + CLI emergency reset (
pnpm --filter @birdnet-ng/hub exec mfa:reset) - Audit log coverage for every MFA action
- Full Mantine v9 UI migration: palette tokens (leaves / wood / surface / success / warning / danger / info / rare / neutral / gray), light mode auto-flip, PageShell + SectionTitle primitives
- Confidence colour CSS vars (
--conf-high/mid/low) cascading through Dashboard, Detections, Verification, Species, SpeciesProfile - Account page split into 2x2 layout with per-card save buttons; design palette + primitives reference page at /design
- Wikimedia images now downloaded as both 800px display thumbs AND full-resolution originals in a single pass (primary + each extra)
?original=1flag on image proxy serves the high-res copy- Unified lightbox component with click-to-zoom + drag-pan + swipe nav (was three separate lightboxes)
extras_exhausted_atcooldown (migration 066) so the worker rotates across species instead of looping on a partial gallery- Image bytes (both sizes) accounted in storage cap + PlatformSettings storage bar
withCount=falseopt-out on/api/detectionsskips the multi-second COUNT(*) when caller doesn't paginate; Dashboard recent-detections is now configurable (10–100 rows, persisted)/api/stats/activity?period=todayfilters to current server day (the Dashboard "Activity today" card was showing lifetime hour-of-day counts)- Image worker
require()ESM regression fixed; backfill loop ordering now rotates instead of looping - 4 new migrations (063–066): image originals, MFA tables (users + user_passkeys), remembered devices, extras_exhausted_at
v0.13 — Mobile & On-Device
- Mobile always-on background mode: foreground service, wake lock, WiFi lock, AlarmManager keepalive
- Custom native RecordingService with persistent notification (bird icon, recording stats, pause state)
- Battery optimization exemption prompt on startup
- Fresh MQTT connection on foreground return (handles Android WebSocket suspension)
- Remote satellite log retrieval via MQTT (hub requests, satellite responds with logs)
- Get Logs button on satellite detail page (Pi: journalctl, mobile: in-app logs)
- Mobile app OTA updates: self-hosted APK distribution via hub API
- In-app update banner with download prompt (version check on startup)
- Platform admin: upload APK, manage releases, force update toggle
- Build script auto-upload with API key auth
- Nginx 200MB upload limit for APK files
- Detections overview: species grouped by count with time range tabs (today/week/month/year), server-side sorting
- Species profile revamp: hero image + stats layout, full paginated detection list with filters/sort
- First-of-day/season changed from per-satellite to per-tenant
- Remote satellite log retrieval via MQTT (Get Logs button)
- ConfidenceRing shared component (used on detections overview and analytics)
- Lightbox click no longer triggers parent navigation
v0.18 — Unified model registry UI + precision dashboard
- Catalog and Installed lists merged into one unified, single list per kind with two-line rows. State badge per row (Actif · Installé · Installé désactivé · Disponible · Disponible masqué); quantization badge (FP32/FP16/INT8) auto-derived from name. Sort: active first, then enabled installed, then disabled, then available, then hidden
- Active models are no longer deletable — the user must promote another first. Lifecycle hint at the top of each section: "install from catalog (or upload your own) → set as active"
- 13 catalog entries: BirdNET v2.4 / v2.3 / v2.2 / v2.1 each in FP32 / FP16 / INT8, plus Perch v2 (embedding). All from stable URLs (raw.githubusercontent.com pinned commits + HuggingFace mirror). All flagged with their license (BirdNET CC-BY-NC-SA, Perch Apache 2.0)
- Catalog dismiss: × hides an entry, ↺ restores. Stored as a
platform_settings.dismissed_catalog_idsarray. "Voir les écartés" toggle to view hidden entries. Per-platform, not per-user - Builtin BirdNET row removed from the registry — it was confusing noise. Migration 050 deletes
source = 'builtin'rows and auto-promotes the most recent custom classifier as the new default. Worker still has the bundled birdnetlib model loaded as a silent fallback when no active classifier is configured - New
birdnet_models.kindcolumn ('classifier' / 'embedding'); the previouskindcolumn was renamed tosource('builtin' / 'custom'). Default is per-kind so a classifier and an embedding model can both be active simultaneously - Per-model precision dashboard on the Compare Models page: each model shows total detections, verified count, confirmed, rejected, and precision % (= confirmed / verified). Drill-down shows per-species precision sorted by sample size; species with <10 verified detections get a small-sample warning. Recall intentionally not measured — no ground truth for misses. Shadow models excluded since humans verify only the main feed
- Catalog accepts
?kind=classifier|embeddingand?showHidden=truequery parameters. NewPOST /api/models/catalog/dismissendpoint to toggle dismiss state - New
GET /api/models/precision-statsendpoint returns per-model summary + per-species breakdown grouped bymodel_id, computed fromdetections.verification_statusover a configurable window (1–365 days)
v0.17.1 — Embedding model registry + capacity counting
- Embedding models are now first-class entries in the
birdnet_modelsregistry alongside classifiers. Newkindcolumn ('classifier'|'embedding'); the previouskindcolumn was renamed tosource('builtin'|'custom') - Per-kind default — a BirdNET classifier and a Perch embedding model can both be
is_default=TRUEsimultaneously - Perch v2 is installable from the catalog UI (one-click; downloads from HuggingFace, stores in MinIO). Storage bar's existing "Models" segment now counts both classifiers and embedding models
- Bootstrap: existing host-mounted
storage/perch/perch_v2.tflitefiles get auto-uploaded to MinIO and registered on the next worker startup. After bootstrap, the host file can be deleted - Worker embedding loader queries the registry per-job (60 s cache), downloads the active embedding model from MinIO into a local cache. Switching models is one click in the UI
- Migration 049: rename
kind→source, add newkindcolumn with default'classifier', replace single-default index with per-kind index
v0.17 — Acoustic embeddings + cosine-weighted trust score
- Per-chunk Perch v2 audio embeddings (Google bird-vocalization-classifier, 1536-dim float16, Apache 2.0). Worker uses TFLite runtime — no full TensorFlow needed; image stays at ~1.28 GB
- Embeddings only computed for chunks that produced detections (silent chunks skipped). Storage: ~3 KB/chunk; ~30 MB/day per tenant at typical detection rates
- Trust-score behavior: neighbor support is now
Σ confidence × cos(target_emb, neighbor_emb)instead ofΣ confidence. Coincidental same-label hits with dissimilar embeddings get less weight; real co-calling clusters get more. Falls back to weight=1.0 when either embedding is NULL (existing behavior preserved) - Optional install: drop
perch_v2.tfliteintostorage/perch/. Without it, embeddings are skipped and the trust score works exactly as before - Migration 048:
audio_chunks.embedding bytea+ partial index,embedding_enabledandembedding_modelplatform settings - Justin Chuby's HuggingFace mirror (
justinchuby/Perch-onnx) provides the CPU-friendly TFLite build
v0.16.1 — Multi-image gallery + storage breakdown
- Multi-image species gallery via Wikimedia Commons (up to 6 images per species, configurable in platform settings); thumbnail strip + lightbox with arrow / swipe / keyboard nav, attribution overlay, link to source
- Background backfill: existing cached species get extras over time; rate-limited to respect Wikimedia
- Storage cap bar now displays four segments (audio protected / unprotected / images / models) with a hover-tooltip breakdown, plus soft + hard markers
- BirdNET model bytes counted toward the cap (read live from MinIO), image cache extras included
- Multi-image dedup via SHA-256 hash + URL filtering (drops duplicates and non-photo files: range maps, recordings, signatures)
- Cleaner APK delete: Modal confirmation, MinIO removal verified via
statObject, audit log entry - Removed dead code: ~700 lines of unused side-panel
SatelliteDetailfromSatellites.tsx(replaced by the dedicatedSatelliteDetailPagelast release) - Migrations 046 (
species_image_files) and 047 (cleanup of duplicate extras from initial rollout)
v0.16 — Multi-Model BirdNET + Period Picker
- Multi-model BirdNET registry:
birdnet_modelstable, per-detectionmodel_idforeign key, bundled birdnetlib model marked default at install - Catalog of historical models installable from the BirdNET-Analyzer GitHub repo with one click (v2.4 / v2.3 / v2.2 / v2.1)
- Custom model upload (TFLite + labels) stored in MinIO; inference worker resolves the active default within ~60 s of an admin change
- Inference compare mode (A/B): worker runs every enabled non-default model on a configurable sample of chunks, writing to a separate
shadow_detectionstable so user-facing stats stay clean - Model Compare admin page: per-model summary, agreement vs default (compared / agree / default-only / shadow-only / agreement rate), top species the shadow models found that the default missed
- Hard capacity cap for audio retention: third pass that purges keep-best/pinned chunks (oldest first) when usage exceeds the hard cap; bar shows soft + hard markers
- Custom calendar picker for day/week/month/year period navigation across Compare and Timeline; respects the user's "start of week" preference end-to-end (calendar grid, period boundary, hub query)
- Compare page (location comparison) accepts
period=day|week|month|year+offsetinstead of just rollingdays - Platform Settings: styled checkboxes + file inputs globally; Models card with active-model banner and styled file picker
- Migrations 042 (birdnet_models + detections.model_id), 043 (label bundled model with version), 044 (audio_storage_hard_cap_gb), 045 (inference_mode + shadow_detections)
v0.15 — Unified Alerts + Timeline Navigation
- Unified alerts pipeline:
AlertRulesEngineis the sole dispatcher; system rules auto-seeded per tenant (rare_species, first_of_day, first_of_season, satellite_offline) are editable but not deletable - Webhooks merged into
alert_channelsas a channel type; existing webhooks auto-migrated and auto-attached to the system rules matching their event_types - Three fire modes on rules:
cooldown(time-based),once_until_clear(requires acknowledgement),on_state_change(auto-resets when condition clears — used by satellite_offline: alert fires only on transition offline, suppressed while still offline, ready to fire again when the satellite comes back and goes offline next) - Cooldown scope: global / per_species / per_satellite — replaces the old coarse global cooldown
- Message template on rules + payload template on every channel type (email HTML, Slack Block Kit JSON, Telegram HTML, Google Chat cardsV2 JSON, Discord embed JSON, webhook free-form)
- Per-channel-type format toggles: include_image, include_link, use_blocks, use_card, image_in_header, use_embed, embed_color, use_html
- "Load default as starting point" button: dumps the hub's built-in template reflecting the current toggle state into the textarea
- Shared variable namespace across rule + channel templates (27 placeholders: rule/species/satellite/tenant/links to wikipedia/ebird/xeno-canto, etc.)
- Inbox tab in the Alerts page with unread filter, type filter, mark-all-read
- Sidebar "Alerts" link gains an unread-count badge
- Dashboard banner narrowed to warning/critical severity (info-level system alerts live in the inbox)
- eBird-based rarity: species not on the eBird "recently observed near here" list flagged as
is_rarewith reason "unexpected in this region" - Species Timeline: period navigation (prev / now / next) shifts by day/week/month/year, human-readable period label,
Nowreset button - Timeline satellite filter + per-bucket RMS silence detection (bucket painted dark purple when no chunks arrived OR mean RMS below threshold) — surfaces "satellite was running but mic was off" gaps
- Backfill script for
audio_chunks.rms(historical data)
v0.14 — Data Management
- Adjacent clip stitching: consecutive same-species detections merged into single detection with concatenated audio (6s window, max 30s, on-the-fly WAV concatenation)
- Duration badge on stitched detections, stitched audio playback via SpectrogramPlayer
- Audio retention: platform-admin-configurable two-pass purge (capacity cap then time window) to keep MinIO usage bounded
- Keep-best: top-N detections per species by confidence preserved regardless of age (cap may be exceeded)
- User-pinned detections immune to retention (📍 toggle on every detection card)
- Stitched groups preserved as a unit when any member is keep-best
- Detection rows retained forever; only audio blobs purged (audio endpoints return 410 Gone for purged clips)
- Platform Settings UI: live storage usage bar (protected vs purgeable split), cap/days/keep-best inputs, "Run sweep now", last-run summary
- Per-detection call-segment highlights on spectrograms: pink freq × time rectangles per syllable
- Two-stage call-detection pipeline: BirdNET oracle (9s extended audio, 2.5s overlap) localizes the species' coarse region, librosa silence-splitting subdivides into syllables, coverage validation drops non-target syllables, per-species empirical frequency bands filter cross-species noise, per-segment peak/median ratio finds each call's true frequency band
- Empirical species-frequency-bands table refreshed every 12h from trusted detections
- Trust score: logit-space blend of raw BirdNET confidence with neighbor support (same species within ±60s); surfaces weak lone detections
- Trust badge and filter ("Confirmed only") + sort-by-trust on detection cards
- Species Timeline view: one heatmap row per species showing when it's active; period selector (today/week/month/year), sort (recent/count/dawn chorus/A-Z), pink "now" marker, configurable bucket granularity (15 min → 1 day)
- Backfill CLI for call_segments (
--hours,--species,--force)
v0.12 — Satellite Detail & Alerts
- Dedicated per-satellite page with full-width sections (activity, schedule, filters, config, biodiversity, expected species, live audio)
- Stats cards row (status, recording, version, location, CPU, storage, uptime, last seen)
- Today's hourly activity bar chart on satellite detail page
- Schedule 24h bar visualization with sun times for all profiles (right-aligned)
- Editable satellite config with override editor (lock, customize, reset)
- Expected species: centered pills with links to species profile (detected only)
- Alert rules: inline rename, duplicate (disabled by default)
- Alert channels: inline rename
- Species profile: share and comment buttons on detection list
- Chart improvements: pixel-based bar heights, date labels, locale-aware formatting
- Weather correlation: detection bar overlay (from bottom), fixed date format bug
- Dashboard: taller trend and activity charts with detection counts
- i18n: satellite status/recording/version translation keys
- Alert rules: "once per species per day" condition, detection date/time in all channels
- Alert deduplication: watcher_processed flag prevents re-evaluation of detections
- Alert messages: removed redundant detection summary (structured fields only)
- Satellite outbox: auto-recreate corrupted SQLite database on startup
- Live audio overhaul: hub-signaled unfiltered streaming via MQTT (live-start/stop/keepalive)
- Live audio: inline base64 via WebSocket, continuous auto-play with chunk chaining
- Force update button: always visible on satellite detail (bypasses version check)
- Satellite: WAV file cleanup on ack, periodic purge of outbox DB entries
- Satellite: git SHA in startup log and update script output
- Mosquitto ACLs for live-start, live-stop, live-audio, update-status topics
- Mobile app: live audio support (subscribe live-start/stop, publish unfiltered chunks)
- Mobile app: BuildKit Gradle cache for faster APK builds
- Expected species scoped to satellite (not global)
- Activity charts: hide future hours on today's view
- Satellite names: ellipsis truncation on detection lists
- Weather chart: fixed detection date offset (timezone)
- Live audio: gapless LiveSpectrogramPlayer with AudioContext streaming, auto-boost compressor, 15s buffer
- Live audio: hidden when satellite not recording, buffering indicator
- Migration chart: primary language only, compact rows, show all species toggle
- Top species: confidence ring chart (avg + max) replacing two columns
- Satellite immediate heartbeat on state change (recording/error/paused)
v0.11 — Satellite Polish & Live Audio
- Pi satellite audio device autodetection (when AUDIO_DEVICE is not set)
- Satellite heartbeat reports "error" state after consecutive capture failures with auto-recovery
- Remote update via MQTT with status feedback and detached script execution
- Git authentication for remote updates (HTTPS token or SSH key via .env)
- Version and audio device logged on satellite startup
- Chunk filter stats: aggregate tracking (processed/sent/rejected by reason) in heartbeat
- Filter stats display in satellite detail panel with pass rate bar
- Per-session filter pass rate on timeline ("X% sent" pill)
- Live audio streaming: "Listen live" on satellite detail with SpectrogramPlayer
- Field notes API and migration (text, photos, weather, location — backend ready)
- First-of-day/first-of-season duplicate alert fix (atomic conditional UPDATE)
- SpectrogramPlayer: auto-boost (peak normalization), buffering indicator, spectrogram alignment fix
- Species profile: sort detections by confidence or date
- Shared detection page: today's count, static OG page for link previews (Discord, Slack, Telegram, etc.)
- Open Graph link previews for bot-detected platforms (Discord, Slack, Telegram, WhatsApp, Twitter, Facebook)
- Reliable remote satellite update via systemd-run transient unit
- Update script: timestamps, retries, error trapping, PATH fix for detached execution
- Comprehensive release protocol documentation
v0.10 — Internationalization (i18n)
- English and French translations (500+ keys, every page)
- Language switcher, locale-aware formatting, translated map popups and heatmap labels
- Singular/plural support via Intl.PluralRules
- Alert messages in tenant's language (species names, labels, rarity reasons)
- First-of-day detection alerts with badge on detection cards
- Start-of-week day preference for heatmaps
- Species images not-found investigation panel with per-species retry
- Public share URLs in alert notifications with species image
- Notification images via Wikipedia direct URLs
- Shared detection page: translated species names, stats, lightbox, user language preferences
- Open Graph link previews (Discord, Slack, Telegram, WhatsApp, Twitter, Facebook)
- SpectrogramPlayer audio preloading with buffering indicator
- French translation quality pass (gender-neutral phrasing, idiomatic terms)
- Timeline session auto-splitting at 600 chunks
- Abbreviated relative times on detection pills
- Species search across all 38 languages, sort by primary language
v0.9 — Analytics & Visualization
- Biodiversity indices (Shannon, Simpson, Evenness)
- Migration pattern heatmaps, weather correlation (Open-Meteo)
- Enhanced detection map (clusters, heatmap, time filter)
- Enhanced dashboard (trend chart, activity bars, biodiversity snapshot)
- Location comparison (Jaccard similarity, shared/unique species)
v0.8 — Smart Alerts & Communication
- Custom alert rules engine (detection, absence, trend triggers)
- 6 notification channels (Email, Slack, Telegram, Google Chat, Discord, Webhook)
- Platform health banner
- Scheduled automatic exports (cron-based CSV/JSON/eBird/iNaturalist)
v0.7 — Species & Location Context
- Per-satellite config overrides with admin lock
- Species profile pages with trend, activity, seasonal data
- eBird expected species integration
- Activity heatmaps (hour x day-of-week)
- Shareable detection links (HMAC-signed)
- Detection comments
v0.6 — Detection Intelligence
- Detection timeline with session-based browsing
- Species filter pills, expand/collapse sessions
- First-of-season detection with badges
- Species accuracy stats from community verification
- Page layout system, server-side sorting/filtering/pagination on all pages
v0.5 — Scalable Architecture
- Hub split into stateless API + single-instance dispatcher
- Redis event bridge, service registry with auto-discovery
- System monitoring page for platform admins
- Comprehensive audit logging (30+ action types)
- Node.js 24, MQTT over WSS
v0.4 — Audio Intelligence
- SpectrogramPlayer (seek, loop, boost, speed, volume)
- Adaptive noise floor + spectral peak SNR filtering
- GPS per detection, satellite config push
- Satellite management with side panel
v0.3 — User Management & Security
- Account management, self-deletion, ownership transfer
- Login rate limiting, account lockout, last login tracking
- Audit log page
- Unified design system (buttons, badges, states)
v0.2 — Satellite Ecosystem
- Recording profiles (dawn chorus, night migration, low power)
- Community verification with keyboard shortcuts
- Android phone satellite app
- Species images from Wikipedia
- Data export (CSV, JSON, eBird, iNaturalist, xeno-canto)
- Multi-language bird names (38 languages)
v0.1 — Core Platform
- Distributed audio capture (Raspberry Pi satellites)
- BirdNET TFLite inference with geo-aware filtering
- Multi-tenant web UI with JWT auth
- Real-time WebSocket alerts for rare species
- Docker Compose deployment with Traefik
Planned
Bucketed by theme rather than version since priorities shift faster than the version numbers do. Items are roughly ordered by how concrete the use case is, not strict commitment.
Field operations
Things real deployments keep hitting that we don't have a good answer for yet.
- Field notes UI. The
field_notes+field_note_photostables exist (migration 026) but no standalone page reads them. Most useful as a per-satellite journal: weather, mic placement, anything that explains an anomalous detection later. - Persistent foreground-service notification on mobile. Capacitor background mode keeps audio capture alive but Android can still kill it under memory pressure. A foreground service with a persistent notification is the standard Android answer; we sidestep it today with the "Keep screen on" toggle.
- Native push notifications on mobile. Alerts currently land via the channels you configure (email, Slack, etc.). Native push for the mobile satellite operator's own phone would be more direct.
Modeling
- Audio re-analysis on a model upgrade.
recalibrate-historical.tsre-runs trust-score against the current calibration curve, but doesn't re-run inference. The pieces are mostly there (BullMQ, model registry, MinIO retention). What's missing is a "re-process all chunks from date X with model Y" command and the retention guarantee that the audio is still there. - On-device inference on phones. Run a smaller BirdNET TFLite variant locally on Android so a phone in a dead zone still produces detections to upload later. Bigger build effort (model selection, accuracy floor, battery budget) than it looks on paper.
Data lifecycle
- Cold storage archival for purged audio. Today the retention sweep deletes WAVs that fall outside the keep-best / pinned / soft-cap window. An optional shunt to slower / cheaper storage (S3 Glacier, local NAS) would let "we retain everything" stay true without paying full MinIO prices.
- Research export. CSV / JSON / eBird / iNat / xeno-canto exports exist. A structured-dataset export (chunked WAVs alongside detections, annotations, calibration metadata) for sharing with researchers is the missing shape.
Security and account hygiene
- GDPR erasure path audit.
DELETE /api/auth/meexists; needs verification that the cascade actually wipes everything we'd be asked about (votes, comments, annotations, audit log references). - Non-MFA session management. MFA-enabled users see remembered-devices on
/accountalready. There's no equivalent listing of plain non-MFA sessions; revoking a stolen session means rotatingJWT_SECRET. - GDPR / research data export (see Data below).
Operations and observability
- Prometheus metrics endpoint. Today the System page (Platform Admin) shows live worker / infra status, but there's no scrapable export. The data is already in Redis (
birdnet:registry:*) and the BullMQ queue; exposing it under/metricsis largely shape-shifting. - Backup + disaster-recovery runbook. Postgres + MinIO are the only stateful tiers that matter; the runbook for snapshot + restore against the live stack hasn't been written down.
- Rolling updates with zero downtime. Split mode (
api+dispatcher) gets us part of the way. The api containers are stateless and could be replaced one at a time behind Traefik; the dispatcher is single-instance and would need a brief drain. Worth documenting before claiming it works.
Polish
- Unsaved-changes confirmation on settings forms that PATCH on Save.
- Docs search. Express + markdown-it currently serves static pages; client-side search index (lunr.js or similar) would make the docs site more useful.
- API auto-documentation. The hub Fastify routes carry TypeBox / JSON Schema annotations in places; auto-generating an OpenAPI / Scalar reference from them would replace the hand-maintained
docs/reference/api.md.