You are viewing archived documentation for v0.34. Go to latest →

Roadmap

What's been released and what's coming next.

Released

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:6379 failed 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.

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_id UUID with FK to satellites.id, cascade on delete (migration 072). Old tenant-scoped keys keep satellite_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.hubApiKey is preferred over hubToken everywhere via a hubAuthToken(config) helper. Auto-exchange on launch: if hubApiKey is missing but hubToken is still valid, mobile POSTs to rotate-api-key and stores the result.
  • Auth resolver populates request.auth.satelliteId from 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 /detection MQTT topic. The hub's DetectionPublisher service (dispatcher / full mode) subscribes to the in-process eventBus and republishes each DetectionEvent to birdnet/{tenant}/{satellite}/detection QoS 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 with FileReader.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.totalBytes sums size_bytes from outbox rows only. vsat-1.0.2 adds outboxStats.diskBytes (real ${dataDir}/audio/ byte count), and the hub UI now shows max(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.json via 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 of 78%*. Wilson is the right choice for binomial proportions at small n; the previous binary <10 verified asterisk overstated the discreteness of the threshold. Rows with a ≥40-percentage-point CI band grey out, with a footer note explaining.
  • New recalibrate-historical.ts one-shot script (pnpm --filter @birdnet-ng/hub recalibrate:historical [--days=N]) re-walks every historical detection and recomputes trust_score against 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_confidence column (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/recent response we already fetch — distinct subId (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 Spectrogram and SpectrogramPlayer accept an annotations prop 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 with calibrated_confidence. UI shows Conf 78% (62%) inline. New "Confidence calibration" card on the Models tab triggers refreshes; coverage at GET /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) with time_start_ms / time_end_ms / freq_low_hz? / freq_high_hz? / vote / notes; mirrors a verification_votes row 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_enabled platform setting). Wikipedia is always the primary source; iNat fills only what Wikipedia is missing. License-gated (default = commercial-safe cc0 / cc-by / cc-by-sa); admin can widen to include cc-by-nc variants. Per-provider exhaustion markers (extras_exhausted_at for Wikipedia, inaturalist_exhausted_at for iNat — migration 068).
  • License code captured + displayed: species_image_files.license_code and species_images.license_code populated from extmetadata; lightbox attribution pill shows the license as a small uppercase badge. pnpm --filter @birdnet-ng/hub backfill:licenses re-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 with keywords[] 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 tenantId from 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 params lazily and params.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 to tenants[0] instead of undefined (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 + source columns.

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/versions returns {hub, satellite, mobile} — Satellites + SatelliteDetail pages compare per-stream.
  • Heartbeat carries a stream field; the hub stores it in satellites.stream (migration 067) and uses it to pick the right comparison target.
  • Pi birdnet-update.sh checks out the latest vsat-* tag instead of origin/main; hub-only bumps no longer trigger Pi rebuilds.
  • Mobile versionCode derives 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. /ack MQTT 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=1 flag 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_at cooldown (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=false opt-out on /api/detections skips the multi-second COUNT(*) when caller doesn't paginate; Dashboard recent-detections is now configurable (10–100 rows, persisted)
  • /api/stats/activity?period=today filters 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_ids array. "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.kind column ('classifier' / 'embedding'); the previous kind column was renamed to source ('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|embedding and ?showHidden=true query parameters. New POST /api/models/catalog/dismiss endpoint to toggle dismiss state
  • New GET /api/models/precision-stats endpoint returns per-model summary + per-species breakdown grouped by model_id, computed from detections.verification_status over a configurable window (1–365 days)

v0.17.1 — Embedding model registry + capacity counting

  • Embedding models are now first-class entries in the birdnet_models registry alongside classifiers. New kind column ('classifier' | 'embedding'); the previous kind column was renamed to source ('builtin' | 'custom')
  • Per-kind default — a BirdNET classifier and a Perch embedding model can both be is_default=TRUE simultaneously
  • 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.tflite files 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 kindsource, add new kind column 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.tflite into storage/perch/. Without it, embeddings are skipped and the trust score works exactly as before
  • Migration 048: audio_chunks.embedding bytea + partial index, embedding_enabled and embedding_model platform settings
  • Justin Chuby's HuggingFace mirror (justinchuby/Perch-onnx) provides the CPU-friendly TFLite build
  • 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 SatelliteDetail from Satellites.tsx (replaced by the dedicated SatelliteDetailPage last 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_models table, per-detection model_id foreign 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_detections table 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 + offset instead of just rolling days
  • 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: AlertRulesEngine is 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_channels as 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_rare with reason "unexpected in this region"
  • Species Timeline: period navigation (prev / now / next) shifts by day/week/month/year, human-readable period label, Now reset 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_photos tables 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.ts re-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/me exists; 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 /account already. There's no equivalent listing of plain non-MFA sessions; revoking a stolen session means rotating JWT_SECRET.
  • Email notifications for account events (registration confirmation, password reset, account locked). Email is already a configured alert channel for detection alerts. Wiring the auth flows uses the same SMTP plumbing.

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 /metrics is 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.