Repository navigation
Added
-
Charts with a vocabulary of their own. The 24-hour charts sit on the day's real sky (night, a dawn
wash, day, a dusk wash, placed by the station's own sunrise and sunset, and absent when the station has no
location); columns are notched so a column can be counted (a seam at every 1, 2 or 5 times a power of ten,
at most twelve of them, and the chart says what a notch is); heat grids are discs whose area and colour are
the same number; a note in the display italic names the busiest bar, falling back to the bare number when
there is no room; the sun and moon ride a rail under the hours. Today's hour columns are now stacked by who
was heard (the day's eight most heard each in their own colour, everyone else as one band, the column still
exactly the hour's count) and the strip is drawn twice, for a card and for a phone, where it used to be
scaled to a third of its size.docs/design/charts.md("The vocabulary beyond the bar") lists each mark and
what a test holds it to. New tokens:--sky-night,--sky-dawn,--sky-day,--sky-dusk. -
Three views, each the form its question needs. Every song (Today, beside "By the hour"; the choice is
remembered on the device): every detection of the day as a glowing note, time across and the station's
confidence up, the station's own cut-off drawn as a line, on the day's real sky; it is fed by a new JSON
route,/pages/today-songs, which carries at most 2,000 songs and says how many the day holds when it is
cut. Who led, and when it changed (Year in review): the year's eight most-heard birds as ranks over the
months with a month scrubber, the true counts as bars from zero, a gap for a month with none and "not yet"
for a month that has not come. When each bird joined your list (the life list): a timeline of firsts with
each bird's portrait and a month-by-month list on a phone. The last two carry a table of the same numbers
behind the picture (and the first of them says its finding in a sentence), so a browser with no script
still has them; Every song is drawn by its
script alone (skysong.js), and with none the page keeps "By the hour", which is the default view. The
scripts are sealed modules, and/pages/today-songsanswers a real 500 on a failed read, because its
reader isfetch()and not htmx; the source scan that forbids a 5xx from a page handler names it as the
one exemption, and a test holds the reason. -
Who answers whom (Patterns, Behavior). After one bird is heard, which others are heard within a minute?
A grid whose rows are the bird heard first and whose columns are the birds heard after it, from the
extension'ssequence_countwith a time constraint: not symmetric, which is the point. A pair seen fewer than
three times is a small ring and never a finding; a best pair at the floor is "nothing stands out yet", not a
headline. New inbirdnet-behavioral:AnalyticsDb::answers,queries::answers_sql,types::Answers; the
live test seeds a day in which each rule (a reply in time, a reply too late, the same instant, a bird
answering itself, direction) has its own count, and was watched failing against a pattern without(?t>0). -
The chorus assembling (Patterns, Sequences). Once the dawn starts, how long before it is a chorus? A field
of discs: a column is a time allowed (1 minute to 5 hours), a row is how many of the station's usual dawn
voices have joined in their usual order, and a disc's area is the share of mornings with at least that many
together within that time; a bold line is the typical morning and a pale band the middle half. It is drawn from
window_funnelasked at a ladder of 14 windows, not fromwindow_funnel_events, which the plan named: a probe
againstbehavioral0.10.1 showed that function reports the latest occurrence of each voice before the next,
so the gap between two of its steps depends on a tie-break and not on the birds (recorded in
docs/design/insights-and-organisation.md, section 6). Modestrict_increase, so voices heard in one audio
chunk do not join in either order. A sentence leads the card and is silent below three voices or seven
mornings; a phone gets a drawing of its own. New:AnalyticsDb::assembly,queries::assembly_sql,
types::{Assembly, AssemblyParams, ASSEMBLY_WINDOWS},GET /api/v2/analytics/assembly, and, for the grid
above,GET /api/v2/analytics/answers. The live test seeds eight mornings whose depths at every window are
worked out by hand (including one morning of voices that share an instant); the figure's mutants and the
handler's, run against a real engine, are listed in the handoff. -
The Patterns bar says what kind of question each tab answers. The seven tabs are grouped under When
(When active, Dawn chorus, Trends), Who (Who sings together, Migration) and How (Visits, Sequences).
No address changes: every?tab=key is the one it had, andbehaviorkeeps meaning the bursts-and-returns
page; the running order, the chorus and who-answers-whom moved to the new?tab=sequences. On a phone the
strip scrolls to the tab you are on. The return-rate ledger and the running order are full width: each shared
a row with the other on the one Behavior tab, and alone in half of a tab of its own they left the other half
empty and squeezed three of the ledger's "100%" cells onto two lines at 1100 to 1440 px (33 at 1000).
relational.mjsnow fails on a rate broken over two lines, at 1440 and at 1000 px (red on the half-width
card, green on the full-width one). -
Every card on Visits and Sequences has the same four layers: one sentence of what the figure found (a
function of the numbers that says its basis and is silent when there is too little to say), the picture, the
table behind a disclosure, and a Method and export panel naming the extension function and its version,
what was asked, what a number counts, the REST route that returns the same numbers where one exists, and a
button that saves the table as a CSV. The other Patterns tabs are regrouped but do not have the four layers
yet. New:frame::Method,static/figure-export.js,static/tab-strip.js,pages/behavioral_glance.rs. -
scripts/preflight.sh: every check CI runs on a pull request that a developer's machine can run, in one
command. Format; clippy on the whole workspace at each of the three feature sets CI builds; rustdoc with
-D warnings; spelling (typosat the version CI pins); unused dependencies;cargo deny;cargo audit;
OSV (scripts/osv-check.py, which asks the database CI's scanner reads about every locked package and applies
the waivers inosv-scanner.toml, expiry included); the generated installer and CLI help; the manual's links;
the licence notices; doc tests; with--msrvthe check on the declared minimum Rust, and with--teststhe
workspace tests. A tool it needs and cannot find fails the run with the install line, and the closing list
names what only CI can run (CodeQL, the container image, the install and upgrade smoke tests, the aarch64
cross-check). Added because three kinds of failure had each cost a CI cycle on recent pull requests, all of
them a check the workflows run and the author had not: the spell check, the manual's link check, and rustdoc.
Its first run found a rustdoc error (a public doc comment linking a private function) and five spell-check
hits in earlier commits of this branch; the root crate's tests then found that the new
/analytics/assemblyroute answered400to a blankhour_start, which a page's control can send (it reads
as "not given" now:a_blank_hour_is_not_given_rather_than_refused, red against the old handler). -
A plan for organisation and insight (
docs/design/insights-and-organisation.md): which of the extension's
nine functions the product uses (measured, not assumed: two are drawn by no page and the modes, bases and
pattern operators are mostly unused), options for regrouping the pages, and a ladder of insights from the
casual birder to the researcher. New inbirdnet-db:monthly_species_counts. -
The kiosk is a plate.
/kioskdraws today's birds as a page from a field guide: one figure
per species, larger the more it was heard (area follows the square root of the count, with a
floor so a bird heard once is still drawn), the Latin name beneath, a numbered key at the foot
and the sheet numbered by the day of the year. It is redrawn only when the birds change (the
page polls with a digest and the server answers204), shows yesterday's birds in the small
hours and says so, and has a day and a night palette chosen from the station's sunrise and
sunset (?night=1|0,?paper=0for a small board). Each photograph is engraved in the browser
and shown whole; a picture inCUSTOM_IMAGE_DIRis drawn as it was made, and aCREDITS.txt
beside it is printed in the foot. The old display is?mode=numbers;?mode=latestshows the
bird heard last. The idea is fugleramme's (https://github.com/arnegiacomo/fugleramme, Arne
Giacomo Munthe-Kaas); no code or artwork is shared. New inbirdnet-db:
species_heard_on, fromdetections_analytic, so a rejected bird is not on the wall.
The sheet is drawn on 1600 units and its smallest caption is 11.5, so the plate reads at the 8.5 px
the browser gates ask of a chart label only from about 1180 px of window up; at 390 px they were 2.8 px
(legibility.mjs, which found it). A window narrower than that now opens/kioskon?mode=numbers
(the old display, readable at any width) unless the address names a mode, andkioskOpensReadable
ininteractions.mjsholds the rule: the phone case failed before, the two cases that keep the plate
passed. The plate on a small screen is still small if it is asked for. -
Every picture is shown with its credit. A photograph fetched from Wikipedia and Wikimedia Commons,
AviCommons or Flickr is kept with its title, author, source and licence (a{key}.credit.jsonbeside the
file) and shown under the picture on the species page, in/api/v2/species/image/{scientific_name}as
credit, and on a new page,/system/credits, which every page's footer links to and which lists the
credited pictures the station holds for the birds it has heard. Until now the credit existed, for two of
the three sources, only in thedescriptionof a JSON answer no page read, and the licences those
pictures are under (CC BY, CC BY-SA) require it to be shown. A picture whose credit cannot be read, or
whose licence is not a free one, is not used; a picture cached before credits were kept is replaced by a
credited one the first time it is asked for (it is not given a guessed credit), and the credits page says
how many are waiting. A link in a credit is followed only if it is anhttporhttpsaddress.
The picture cache now refuses a key that is not a single file name or that holds..(../victim,
a/b,a..b,., the empty name): the key is derived from the name in a request path, and with the
credit file added beside each picture, CodeQL'srust/path-injectionflagged the removal of it. Before,
remove("../victim")on a cache besidevictim.jpgdeleted that file; three tests fail on the old code,
anda_key_with_punctuation_in_it_is_still_a_file_nameholds that species names with dots, brackets and
×still cache. The alert stayed open through three forms of the check (a comparison withPath::file_name
inside a helper, acontains("..")written in place, and deleting a file found by listing the cache
directory). The last alert's argument is the configured cache directory, which CodeQL takes as a remote
source through axum'sState(tools/code-scanning/dismiss.shrecords the same for the cache's earlier
alert), so no check on the key can clear it. The credit lines on the species page are underlined like the other prose links (axe's
link-in-text-block, in the light theme).
On a phone each row stacks, its cells printing their column's name. The browser gates grade the page
(/system/creditsis in their route list; the screenshot fixture seeds credited pictures, an uncredited one and a
plate), andbook.mjstakesONLY=<image name>to refresh one picture of the manual. -
Display → Charts → Plate. A per-browser choice (
bnb-illustration,data-illustration="plate") that
draws each figure as a ruled sheet of paper with an italic title and draws each categorical series (the
species in the streamgraph, the dawn and dusk bars, the migration ridges) in its own colour with straight
strokes cut across it, in the manner of a field guide. The first version hatched everything and was
worse to read: measured with the lines averaged into the area, adjacent steps of the heat-grid legend
differed by 5.3 to 5.7 L* against 6.9 to 9.2 flat. It now keeps colour as the first cue and the stroke as a
second, and draws sequential scales (the heat grid, co-occurrence ribbons, the terrain) flat. The stroke
is the darker of the page's two colours in each theme, 3.19:1 or better against every series colour in
light and 4.49:1 in dark, whicha_plate_stroke_can_be_seen_on_every_colour_it_crossesholds (WCAG
1.4.11 asks 3:1).the_plate_chart_style_keeps_its_promises.rsholds that no sequential class is
hatched and that the stylesheet and the pattern file name the same patterns.tools/visual-qa/axe.mjs
andlayout.mjstakeILLUSTRATION=plate; both are clean with it on (axe: 168 route, theme and
viewport states; layout: 336). -
The soundscape terrain says what it found and marks the sun. Above the terrain, one sentence compares
the share of the window's detections heard within two hours either side of sunrise with an even spread
(four hours of twenty-four) and says which of three things is so: clearly more (at least 1.25 times the
even share), clearly less (at most 0.8 times), or about the same. It is left out for a window under
seven days or under 30 detections. A dashed line on each day marks sunrise and sunset on the station's
local clock, with the offset in force on that day, so a change of clocks bends the line where it should.
The text is in the JSON astake. -
The kiosk draws public-domain plates where there are any.
/api/v2/species/plate/{scientific_name}/file
serves the operator's own art (X-Bnb-Image-Source: custom), else the nineteenth-century plate a built-in index
names for the species (plate), else204(nothing to show; a404logged a failed request in the browser for
every species without a plate, which the strict visual sweep caught on the kiosk), and the kiosk then asks for the photograph as before, so a bird with
a plate never downloads a photograph it will not draw. The index (crates/birdnet-integrations/data/plates/index.json,
built bytools/plate-index/, report intools/plate-index/report.md) names a file on Wikimedia Commons from
Gould's bird books, Naumann's Naturgeschichte der Vögel Mitteleuropas or the von Wrights' Svenska fåglar, only
when Commons' own metadata calls it public domain, CC0 or Public Domain Mark and it shows one species; scans their
uploader re-licensed (CC BY-SA) are left out. It covers 458 of BirdNET V2.4's 6,515 species (7.0%), mostly European and Asian
ones: 17 of a fixed sample of 40 common European and North American species, and 1 of the 15 species in the North
American screenshot fixture. A plate is fetched from Commons the first time the bird is heard, intoplates/
inside the picture cache, under the same offline switch as the photographs. In the browser its page is cut away
(pageBounds), then cut again to its ink, so the birds fill the figure; its paper is levelled and, by day,
multiplied into the sheet, so only the plate's ink and colour remain, and by night it is a lit print on the dark
ground. A plate that is a picture to its edge is drawn as it is. The foot is rewritten from what was drawn: the
books, whether a photograph was engraved, and the address of/system/credits, which lists the books and the
plates held. A plate is credited to its work's author, not the file's free-text one, which on Commons is what the
uploader typed (for Naumann it holds a digitiser's name and an email address). -
Patterns pages share one frame.
pages/frame.rsgives every tab the same head (the help
link in the same place) and every chart a numbered figure with a how-to-read caption;
pages/insight.rswrites the one-sentence finding above a chart from numbers the chart already
holds, and says nothing on too little data.
Changed
-
The browser gates' fixture serves its own pictures.
screenshot_server, which the accessibility, layout,
legibility and visual sweeps run against, used to ask Wikipedia for each species' picture, as the shipped binary
does. A run behind a proxy or a rate limit then drew initials where a run in CI drew photographs, so the strict
sweep failed on one and passed on the other with the same code (the life list's six newest birds all 404ed),
and an edit to an article could have moved CI's layout. Every species the fixture knows now has a credited
picture and a line of prose from the fixture's own source (FixturePictures, served by the fixture at
/__fixture/picture.jpgthrough the provider, download, disk and credit path the binary uses); a species it does
not know has none, and the one picture seeded without a credit keeps its legacy state. With outbound HTTPS
pointed at a dead proxy all 15 of the fixture's species answer200, and so do they under a base path. -
axe.mjsfails on any finding, not only serious and critical ones (AXE_FAIL_ONnow defaults to
minor,moderate,serious,critical). The species page had carried 12 moderate findings, one rule, through every
sweep because nothing read them; a station's own gate should not have a tier that is printed and not read. -
osv-scanner.tomlcarries no waiver. The one it held,GHSA-qwgh-2vcv-g2f7for block_buffer 0.10.4 (waived
until 2026-12-01), matched nothing: the advisory's record, last modified 2026-10-30, lists block_buffer 0.11.0
up to 0.12.1 as affected and the lockfile holds 0.10.4 and 0.12.1.scripts/osv-check.pyreports no advisory for
any of the 649 locked packages. -
The release workflow builds macOS on
macos-15, notmacos-14. GitHub retires the macOS 14 runner image
on 2026-11-02 and failed every job on it in scheduled brownouts before that, which is how the release dry run
for this version lost itsaarch64-apple-darwinleg (and, with it, thepackageandUpgrade from v…jobs,
which wait for the whole build matrix). Two steps were added to that leg: one refuses any runner whose
uname -mis notarm64, so a label that resolved to an Intel image cannot cross-compile unnoticed, and one
prints the binary'sminos(the oldest macOS that will load it) into the run's summary. No deployment target
is pinned anywhere, so that number is whatever the runner image's toolchain defaults to; nothing promises a
floor while macOS is a preview. -
Chart axes are a little darker in the light theme (
--chart-axisis nowoklch(61% 0.010 70)), because
an axis label drawn on the new sky is graded against the sky and the old value read 2.75:1 there
(chart-ramps.mjsgrades it; it reports 146 checks, 0 failures). The sun on the daylight rail is
--warningand not--dawn, which is 2.85:1 on the page. -
Flickr's CC BY-ND is no longer fetched (
ALLOWED_LICENSESis now4,5,7,8,9,10, without 6). The kiosk
engraves a photograph, which is an adaptation, and an ND licence does not allow one. A Flickr photo whose
licence id is not in that list, or that has no page to credit, is refused. -
Recordings and the three Reports tabs open with the shared page head (
frame::page_head, one<h1>,
the help link in the eyebrow; the print button is in the head on Weekly and Year in Review), and the
CSS that styled the old openings, and other CSS no markup used any more, is gone. Today keeps its own hero, whose headline is written from the day's data and set at the larger hero size,
and is held to the two rules it can share bytoday_has_one_h1_and_the_help_link_in_its_eyebrow. -
Fourteen hand-written figure cards (on the Patterns tabs and the analytics dashboards) carry the
fig
class their "Fig. N" number implies; they were plain cards among sheets under the Plate style.
every_card_that_numbers_itself_is_a_figure_sheetreads the marker and demands the class. -
DuckDB 2.0.0 (
duckdb1.20000.0, from 1.5.6), withbehavioralv0.10.1 andicu
v2.0.0 embedded and pinned by sha256 (extension_pins.rs). Four things the move needed that a
version bump does not show:- The crate's engine called itself
v2.0.0-dev88614.libduckdb-sys1.20000.0 compiles the
2.0.0 release commit (d54cbbe3df, thegit_revisionof the official wheel) with DuckDB's
fallback version macros. DuckDB names the extension directory after the version string only
when it is a release, and after the commit hash otherwise; no registry serves a build under
the hash, and the publishedicuwas refused ("built specifically for DuckDB version
'v2.0.0'"), which would have emptied every date-ranged dashboard..cargo/config.tomlnow
setsDUCKDB_VERSIONandDUCKDB_PATCH_VERSIONthroughCXXFLAGS, and
the_engine_names_itself_as_a_releasefails without it. A build made outside this tree
(cargo install) does not get the define;RELEASING.mdsays what that costs. - DuckDB 2.0.0's stable C API is still v1.5.6. The runtime check that decides whether an
embeddedC_STRUCTextension fits the engine read the engine's release version as its C API
version, which would have reportedbehavioral(targetv1.2.0) as unloadable on 2.0.0
although DuckDB loads it. It now reads the C API from a table
(stable_c_api_of);a_correctly_built_binary_reports_no_mismatchfailed on the old rule. - GNU ld could not link the binary.
libduckdb-sys1.20000.0 links a small archive whole-archive,
which asks forduckdb_register_static_extensions, and rustc puts it on the link line after the
rlib that carries libduckdb, so GNU ld has passed the defining object by then
("undefined reference"). lld takes any order, and it is what rustc 1.99.1 links x86_64 Linux
with here, which is why the debug builds linked. The Docker image build for aarch64 (Rust 1.95,/usr/bin/ld) failed this way; the same
error comes from the x86_64 debug binary with-C link-arg=-fuse-ld=bfd.build.rsnow passes
-Wl,--undefined=duckdb_register_static_extensionson Linux withanalytics, and that link
succeeds. Only the root package's own targets get it: a test binary of another crate linked with
GNU ld was not tried. - The licence set follows the engine:
third_party/duckdb-2.0.0/(nolibpg_query, which 2.0.0
no longer bundles; the ICU text is carried over from 1.5.6), and
gen-third-party-licenses.pynow reads the engine's major from the crate version instead of
assuming 1.
- The crate's engine called itself
-
rusqlite0.41 and the SQLite it bundles, 3.54.1 (from 3.53.2). One test helper needed a change:
Connection::trace_v2takes&mut selfand returns aResultnow (the helper ignored what would have
been a silent failure to install the trace the test reads). The workspace library tests and every
birdnet-dbintegration test pass. Comments elsewhere that say "measured on SQLite 3.53.2" are dated
measurements and were not re-taken. -
A plain
cargo update(20 packages, all inside their declared ranges):notify8.2.1,realfft3.5.1,
uuid1.29.0,smallvec1.16.4,rustix1.1.6,xattr1.7.0,unicode-width0.2.3,openssl-probe0.2.2,
ctutils0.4.6 and thepestfamily 2.9.5 for the manual's renderer, plusbacktrace,gimli,
objectandaddr2line. Every new package is MIT, Apache-2.0 or Zlib, whichdeny.tomlallows
(read from the package metadata;cargo denyitself is not installed here). The workspace library tests
pass with the same counts as before the update. Not checked: thatrealfft3.5.1 leaves the mel
spectrogram bit-identical (the parity harness needs the model files, which this session did not have). -
OSV-Scanner waives GHSA-qwgh-2vcv-g2f7 until 2026-12-01 in a new
osv-scanner.toml
(block_buffer0.10.4, reached throughsha10.10.7 from axum and tungstenite; release builds
abort on panic, which the advisory's precondition needs to be caught). The fixed 0.12.1 is
already in the lock for the newer digest chain; the entry goes when axum and tungstenite
move to it. -
CI configholds a pull request to the budgets it edits. Its run-history checks (a job
near its timeout, a stale# observed) readmain's last five runs, so a stalled apt mirror
on 2026-10-25 and 10-26 put nine mutation rows over the line on every pull request that ran the
check afterwards, whether or not it touched a budget. On apull_requestevent those two checks now
apply to the job rows whose budget, and thetimeout-minutes:lines whose budget or# observed,
differ from the pull request's base (the first parent of the merge commit, so the job's checkout is
two commits deep). Push, schedule and dispatch runs still check every row, so an outgrown budget
goes red onmain; a pull request whose base cannot be found is checked on every row too.
scripts/check-ci-config.pyrecords the incident (12) and tests the rule in both directions.
Fixed
-
/pages/history-calendarwrote itsselvalue into the month buttons unescaped. The handler took any ten
characters as the selected day, and"><zq-p>12is ten, so a link carrying such a string put up to ten
characters of markup into the buttons'hx-getattributes. The buttons are drawn when the history spans two
months, which is every station past its first month; the strict CSP stops a script, so this was markup
injection and not script execution. Thesel_paramcode is in the source of v0.18.0 and v0.18.1 (from
git log; those tags were not rebuilt). Now only aYYYY-MM-DDis accepted at the handler, and the value is
percent-encoded where it is written. It surfaced because the release gate failed: the sweep that grades every
page for this (a_query_value_is_never_echoed_as_markup) seeded today and yesterday, which fall in two months
on the 1st only, so on every other day of the month the buttons were never drawn and the sweep was green. On
2026-11-01 it was red formain's own CI and for thev0.19.0release run. The seed now spans two months
whatever the day (the_seed_spans_two_months_whatever_day_the_sweep_runs), and the handler and the renderer have
tests on fixed dates, each seen failing against the shipped code.v0.19.0was first tagged one commit before
this fix; its release run stopped at that gate and published no GitHub Release, and the tag was moved to the fix.
Its container image was pushed from the first commit and is replaced by the one built from the moved tag. -
The morning running order drew its lines through its own words. The newest morning's heavy line cut through
the right-hand count of the lanes below it ("167 mornings · 82%"), and the time written beside a large dot sat
where that line left the dot whenever the morning moved steeply or doubled back, which is the case the figure
is for. The words were drawn last with a halo, which hides a thin line and not a heavy one. The lines are now
drawn in pieces that leave out the band each lane's words are written in (a header strip: the slant on either
side still says which way the morning went), and each time is put on a side of its dot that its own line, the
lane's words, the edge of the picture and (where the lane has room) every other morning's dot leave free: the
first render of the book's figure had "08:22" with a dot under its first digit. Tests read the markup back as
geometry: no line meets a name, a count or a typical time, no time is struck, and no time is written over a dot,
for a late shallow morning, a steep one, one that doubles back and one at the edge of the clock; red against the
old drawing, and twelve single-site mutants of the new code are each caught. -
The species page had a landmark axe objected to. An
<aside>holding the best-recording card was reported
bylandmark-complementary-is-top-levelon all three species-detail states in both themes and sizes (12
findings); it is a<div>now, and a recording card is not content complementary to the page. -
A webhook test failed about one run in a hundred.
a_redirect_within_one_origin_still_carries_the_credential
answers a302and closes the connection without saying so, so the client sometimes sent the redirected request
down the closed connection and met "Connection reset by peer" (6 failures in 600 runs of the test alone; it
failedmain's CI once, after the merge of #274). The stub now sendsConnection: close(0 in 1,500). The
product code is unchanged: a closed connection reused is an ordinary HTTP/1.1 race, and the error is logged. -
"Neighbouring species" said a share of sessions that it is not. The probability is the share of the
sessions that had a bird on that side at all (a session the bird opens has no bird before it, one it closes
none after): measured, a bird followed by Jay on 2 of its 7 sessions reports Jay at 100%, and the flow's
tooltip ("Share of sessions it came just after"), the guide and the card's Method note all read as if it were
2 of 7. They now say what it is a share of (the_reading_says_what_the_share_is_of, red against the old
tooltip;the_neighbour_card_says_what_its_share_is_of, which runs a real engine). -
The kiosk answered
400to a blankar. The page sends the screen's width over height from a hidden
control;?ar=with the value empty (a script that failed, an address typed by hand) was rejected with "cannot
parse float from empty string", on a display that runs unattended. A blankarnow draws the default sheet; a
value that is not a number is still refused. (a_blank_aspect_ratio_draws_the_default_sheet_but_a_garbled_one_is_refused,
red against the old handler.) -
The weekly report called an unfinished week "quieter". It held this week's detections so
far against the whole of last week's, so until Sunday every station was told its week was
quieter than the one before, and by how much. It now compares the days that are over with the
same days of last week, says "so far", and makes no comparison on a Monday morning.
the_weekly_report_compares_like_with_likefails on the old page. -
The When tab had two range controls that did not agree, and Trends drew three charts that
When already draws. One control sets all four charts; Trends links to where the others live. -
NOTICEanddocs/book/about.mdsaid photographer attribution is "shown on each image". No
page prints it; the credit for AviCommons and Flickr pictures is only in the image API's
description. Both now say so. The gap itself is not closed. -
A stalled apt mirror no longer spends a mutation shard's budget. In
main's five latest
Mutation testingruns, 19 of 172 shard jobs spent 5 minutes or more inInstall ffmpeg
(16 of them 15 or more, up to 35.7), all on 2026-10-25 and 10-26, against a median of 0.3;
the log of one showsazure.archive.ubuntu.comgoing quiet for 2.5 to 4 minutes mid-download.
Nine rows were then reported by theCI configjob at 78% to 101% of their budget, six of them
cancelled at it, and each had such an install in the run it was measured on. The step now
gives up on a stalled connection after 20 s (three retries) and is limited to 15 minutes, so
a bad mirror fails that step by name. The check readsmain's last five runs, so those nine
findings clear as five runs without a stall replace the ones that had it. -
The a11y job's
# observedannotation is regenerated (21m41s→37m21s, the median of
its last five runs onmain; the job has grown with the gates added since it was written). -
The kiosk's "Exit" link shows a readable focus ring on the day sheet. The plate's own rule,
.plate-ui a:focus-visible, reaches the links inside the mode bar but not the exit link, which
carries the class itself, so it kept the app's green ring: 1.47:1 on the paper, readable only on
the dark ground. The focus gate passed at 20:15 UTC (lamplight) and failed at 06:50 on the same
code, because the kiosk picks its palette from the sun and the route list held one bare/kiosk.
The selector now covers it, and the browser gates list/kiosk?night=0and/kiosk?night=1, so
both palettes are graded on every run whatever the hour.
Installation
One-line installer (Raspberry Pi / bare-metal Linux)
curl --proto '=https' --tlsv1.2 -fsSL https://github.com/tomtom215/BirdNet-Behavior/releases/download/v0.19.0/install.sh | sudo bash -s -- --version 0.19.0Run it as root via the pipe above. Do not use
sudo bash <(curl ...)— sudo
closes the process-substitution file descriptor, so the script never loads.
Download a binary directly
| Target | Platform |
|---|---|
aarch64-unknown-linux-gnu |
Raspberry Pi 4 / 5, ARM64 Linux |
x86_64-unknown-linux-gnu |
Standard 64-bit Linux |
aarch64-apple-darwin |
Apple Silicon Mac (no sudo) |
Archives are gzipped tarballs containing the stripped binary,
README.md, CHANGELOG.md, LICENSE, LICENSE-UPSTREAM, NOTICE,
THIRD_PARTY_LICENSES.txt, install.sh, and a help/ directory with the rendered operator
manual (served offline at /help). Each binary ships the DuckDB
behavioral-analytics engine built in; install.sh enables it by
default (for a manual run, pass --analytics-db).
Runtime requirement: glibc 2.39 or newer — Raspberry Pi OS
Trixie, Debian 13, or Ubuntu 24.04. Pi OS Bookworm
(glibc 2.36) is not supported by the native binary: run the
Docker image instead (it bundles its own runtime) or upgrade to
Trixie. There is no 32-bit (armv7) binary — use the 64-bit Pi OS.
Model
The BirdNET+ V3.0 model (~541 MB) is not bundled in the archive.
On first run, install.sh and the Docker image fetch it
(sha256-verified) from the shared, arch-independent model release
https://github.com/tomtom215/BirdNet-Behavior/releases/tag/models-v3.0-preview3
— the same GitHub origin as the binary — falling back to Zenodo
(the upstream source) if that asset is unavailable. The install is
offline-capable after that one fetch.
Docker
docker pull ghcr.io/tomtom215/birdnet-behavior:0.19.0The image includes DuckDB analytics (no separate -analytics tag).
Multi-arch manifests cover linux/amd64 and linux/arm64.
Documentation
The operator manual for exactly this version, built from this tag:
https://tomtom215.github.io/BirdNet-Behavior/v0.19.0/
The binary serves the same manual offline at /help.
Verification
Checksums
sha256sum -c SHA256SUMS --ignore-missingSLSA build provenance
Every release archive carries a signed SLSA build provenance
attestation produced by this workflow run. Verify it with the GitHub CLI:
gh attestation verify \
--repo tomtom215/BirdNet-Behavior \
birdnet-behavior-0.19.0-aarch64-unknown-linux-gnu.tar.gzSoftware Bill of Materials (CycloneDX)
A CycloneDX 1.5 SBOM for the full workspace is attached as a
release asset in both JSON and XML formats:
birdnet-behavior-0.19.0-sbom.cdx.jsonbirdnet-behavior-0.19.0-sbom.cdx.xml
Consumers can ingest these into Dependency-Track, GitHub
Dependency Graph, or any CycloneDX-aware vulnerability
scanner. Both files are signed by the same SLSA build
provenance attestation as the binaries.