Serve collage at http://birdnet.local/ (replaces stock index)

install_services.sh now symlinks five frontend files (index.html,
styles.css, apt.js, masks.json, dims.json) from avian/frontend/ into
the Caddy site root. Caddy serves index.html before index.php, so /
shows the collage and BirdNET-Pi's stock UI moves to /index.php
(still linked from the menu drawer).

API shims stay under /avian/api/. apt.js fetches them with
./avian/api/birdnet-api.php?action=X now that the frontend lives at
root instead of /avian/frontend/.

avian/index.html redirect file removed (no longer needed - / serves
the collage directly).

README + menu drawer updated to reflect / as the entry point and
/index.php as the stock-UI link.

Verified in Docker:
  /                        200 text/html (collage, 4 tiles)
  /masks.json              200 (276 KB)
  /avian/api/...           200 (all PHP shims working)
  Caddy serves index.html as default index, index.php still reachable
This commit is contained in:
Twarner491
2026-05-28 12:07:27 -07:00
parent 65ac7efef4
commit b2027fed2c
6 changed files with 42 additions and 35 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ curl -s https://raw.githubusercontent.com/Twarner491/AvianVisitors/avian-visitor
Clones this fork, installs BirdNET-Pi, symlinks the AvianVisitors overlay into the Caddy web root. Takes 20-40 minutes. Reboots when done. Clones this fork, installs BirdNET-Pi, symlinks the AvianVisitors overlay into the Caddy web root. Takes 20-40 minutes. Reboots when done.
Collage: `http://birdnet.local/avian/`. Stock BirdNET-Pi UI: `http://birdnet.local/`. Collage: `http://birdnet.local/`. Stock BirdNET-Pi UI: `http://birdnet.local/index.php` (also linked from the menu drawer).
--- ---
+3 -2
View File
@@ -25,8 +25,9 @@ if (getenv('AV_REQUIRE_AUTH') === '1' && empty($_SERVER['HTTP_AUTHORIZATION']))
echo json_encode([ echo json_encode([
'items' => [ 'items' => [
// Stock BirdNET-Pi UI (sits at the site root) // Stock BirdNET-Pi UI (AvianVisitors took over `/`, so the stock
['label' => 'birdnet-pi', 'href' => '/', 'native' => false], // UI is reachable at /index.php directly)
['label' => 'birdnet-pi', 'href' => '/index.php', 'native' => false],
// BirdNET-Pi log view (php served at /views.php) // BirdNET-Pi log view (php served at /views.php)
['label' => 'logs', 'href' => '/views.php?view=Log+Out', 'native' => false], ['label' => 'logs', 'href' => '/views.php?view=Log+Out', 'native' => false],
['label' => 'system', 'href' => '/views.php?view=Services', 'native' => false], ['label' => 'system', 'href' => '/views.php?view=Services', 'native' => false],
+1 -1
View File
@@ -1,6 +1,6 @@
# Forwarding # Forwarding
Default install hosts the collage at `http://birdnet.local/avian/` on your LAN, no auth. The recipes below are independent. Pick what you need. Default install hosts the collage at `http://birdnet.local/` on your LAN, no auth. The recipes below are independent. Pick what you need.
--- ---
+23 -23
View File
@@ -424,7 +424,7 @@
// com flows through so the worker's JIT Gemini job uses the right // com flows through so the worker's JIT Gemini job uses the right
// common name in its prompt for a freshly-detected species. // common name in its prompt for a freshly-detected species.
// &v=IMG_VERSION busts CF edge cache when we re-render any species. // &v=IMG_VERSION busts CF edge cache when we re-render any species.
var img = '../api/cutout.php?sci=' + encodeURIComponent(s.sci) + var img = './avian/api/cutout.php?sci=' + encodeURIComponent(s.sci) +
(s.com ? '&com=' + encodeURIComponent(s.com) : '') + (s.com ? '&com=' + encodeURIComponent(s.com) : '') +
'&v=' + IMG_VERSION; '&v=' + IMG_VERSION;
var btn = document.createElement('button'); var btn = document.createElement('button');
@@ -596,11 +596,11 @@
// load and by refreshRecent() when the window picker changes. // load and by refreshRecent() when the window picker changes.
var STATS_DAYS = 30; var STATS_DAYS = 30;
var DATA = { var DATA = {
stats: null, // ../api/birdnet-api.php?action=stats (totals/today/week/last_hour/started) stats: null, // ./avian/api/birdnet-api.php?action=stats (totals/today/week/last_hour/started)
lifelist: null, // ../api/birdnet-api.php?action=lifelist (every species ever detected) lifelist: null, // ./avian/api/birdnet-api.php?action=lifelist (every species ever detected)
timeseries: null, // ../api/birdnet-api.php?action=timeseries (daily + hourly aggregates) timeseries: null, // ./avian/api/birdnet-api.php?action=timeseries (daily + hourly aggregates)
firstseen: null, // ../api/birdnet-api.php?action=firstseen (newest lifelist additions) firstseen: null, // ./avian/api/birdnet-api.php?action=firstseen (newest lifelist additions)
recent: null, // ../api/birdnet-api.php?action=recent&hours=N (refetched on picker change) recent: null, // ./avian/api/birdnet-api.php?action=recent&hours=N (refetched on picker change)
}; };
// Derived chart arrays, backfilled so 30 buckets always exist. // Derived chart arrays, backfilled so 30 buckets always exist.
@@ -839,7 +839,7 @@
var recent = DATA.recent || { species: [] }; var recent = DATA.recent || { species: [] };
var firstseen = DATA.firstseen || { species: [] }; var firstseen = DATA.firstseen || { species: [] };
// By Period — pulled directly from ../api/birdnet-api.php?action=stats so the numbers // By Period — pulled directly from ./avian/api/birdnet-api.php?action=stats so the numbers
// are authoritative (BirdNET-Pi's own counts). // are authoritative (BirdNET-Pi's own counts).
var last_hour = (stats.last_hour && stats.last_hour.detections) || 0; var last_hour = (stats.last_hour && stats.last_hour.detections) || 0;
var today_det = (stats.today && stats.today.detections) || 0; var today_det = (stats.today && stats.today.detections) || 0;
@@ -851,7 +851,7 @@
+ liRow('WEEK', 'last 7 days', fmtN(week_det)) + liRow('WEEK', 'last 7 days', fmtN(week_det))
+ liRow('ALL', 'all time', fmtN(all_det)); + liRow('ALL', 'all time', fmtN(all_det));
// Top Species — top 5 species in the current window. ../api/birdnet-api.php?action=recent // Top Species — top 5 species in the current window. ./avian/api/birdnet-api.php?action=recent
// already returns species sorted by last_seen DESC; re-sort by count. // already returns species sorted by last_seen DESC; re-sort by count.
var ranked = (recent.species || []) var ranked = (recent.species || [])
.slice() .slice()
@@ -963,11 +963,11 @@
grid.innerHTML = species.map(function (s) { grid.innerHTML = species.map(function (s) {
var total = +s.total || 0; var total = +s.total || 0;
var win = winBySci[s.sci] || 0; var win = winBySci[s.sci] || 0;
var sketchSrc = '../api/cutout.php?sci=' + encodeURIComponent(s.sci) + var sketchSrc = './avian/api/cutout.php?sci=' + encodeURIComponent(s.sci) +
(s.com ? '&com=' + encodeURIComponent(s.com) : '') + (s.com ? '&com=' + encodeURIComponent(s.com) : '') +
'&v=' + SKETCH_VERSION; '&v=' + SKETCH_VERSION;
var audioSrc = '../api/recording.php?sci=' + encodeURIComponent(s.sci); var audioSrc = './avian/api/recording.php?sci=' + encodeURIComponent(s.sci);
var spectroSrc = '../api/spectrogram.php?sci=' + encodeURIComponent(s.sci); var spectroSrc = './avian/api/spectrogram.php?sci=' + encodeURIComponent(s.sci);
// The "all time" window makes the windowed count identical to the // The "all time" window makes the windowed count identical to the
// all-time count — collapse to a single stat rather than print the // all-time count — collapse to a single stat rather than print the
// same number twice. Otherwise label the count with its span. // same number twice. Otherwise label the count with its span.
@@ -1130,7 +1130,7 @@
// lands later — we discard the stale response so the collage // lands later — we discard the stale response so the collage
// never reverts to a different window. // never reverts to a different window.
var forHours = currentHours; var forHours = currentHours;
return fetchJson('../api/birdnet-api.php?action=recent&hours=' + forHours) return fetchJson('./avian/api/birdnet-api.php?action=recent&hours=' + forHours)
.then(function (j) { .then(function (j) {
if (forHours !== currentHours) return; // window changed mid-flight if (forHours !== currentHours) return; // window changed mid-flight
DATA.recent = j; renderWindowDependent(); DATA.recent = j; renderWindowDependent();
@@ -1140,11 +1140,11 @@
function refreshAll() { function refreshAll() {
var forHours = currentHours; var forHours = currentHours;
return Promise.all([ return Promise.all([
fetchJson('../api/birdnet-api.php?action=stats').catch(function () { return null; }), fetchJson('./avian/api/birdnet-api.php?action=stats').catch(function () { return null; }),
fetchJson('../api/birdnet-api.php?action=lifelist').catch(function () { return null; }), fetchJson('./avian/api/birdnet-api.php?action=lifelist').catch(function () { return null; }),
fetchJson('../api/birdnet-api.php?action=timeseries&days=30').catch(function () { return null; }), fetchJson('./avian/api/birdnet-api.php?action=timeseries&days=30').catch(function () { return null; }),
fetchJson('../api/birdnet-api.php?action=firstseen&limit=10').catch(function () { return null; }), fetchJson('./avian/api/birdnet-api.php?action=firstseen&limit=10').catch(function () { return null; }),
fetchJson('../api/birdnet-api.php?action=recent&hours=' + forHours).catch(function () { return null; }), fetchJson('./avian/api/birdnet-api.php?action=recent&hours=' + forHours).catch(function () { return null; }),
]).then(function (parts) { ]).then(function (parts) {
DATA.stats = parts[0]; DATA.stats = parts[0];
DATA.lifelist = parts[1]; DATA.lifelist = parts[1];
@@ -1223,7 +1223,7 @@
// worker accepts the cookie we skip the lock screen; if it 401s we // worker accepts the cookie we skip the lock screen; if it 401s we
// show the lock as usual. // show the lock as usual.
function tryAutoUnlock() { function tryAutoUnlock() {
fetch('../api/menu.php', { credentials: 'same-origin' }).then(function (r) { fetch('./avian/api/menu.php', { credentials: 'same-origin' }).then(function (r) {
if (r.status === 200) { if (r.status === 200) {
return r.json().then(function (j) { renderMenu(j.items || []); }); return r.json().then(function (j) { renderMenu(j.items || []); });
} }
@@ -1247,7 +1247,7 @@
if (r.status === 200) { if (r.status === 200) {
// Cookie is set — fetch the drawer JSON the same way every // Cookie is set — fetch the drawer JSON the same way every
// protected endpoint will be called from now on (cookie-based). // protected endpoint will be called from now on (cookie-based).
return fetch('../api/menu.php', { credentials: 'same-origin' }) return fetch('./avian/api/menu.php', { credentials: 'same-origin' })
.then(function (m) { return m.json(); }) .then(function (m) { return m.json(); })
.then(function (j) { renderMenu(j.items || []); }); .then(function (j) { renderMenu(j.items || []); });
} else if (r.status === 401) { } else if (r.status === 401) {
@@ -1716,7 +1716,7 @@
var sp = ((DATA.lifelist && DATA.lifelist.species) || []) var sp = ((DATA.lifelist && DATA.lifelist.species) || [])
.find(function (s) { return s.sci === sci; }); .find(function (s) { return s.sci === sci; });
var com = sp ? (sp.com || '') : ''; var com = sp ? (sp.com || '') : '';
var base = '../api/cutout.php?sci=' + encodeURIComponent(sci) + var base = './avian/api/cutout.php?sci=' + encodeURIComponent(sci) +
(com ? '&com=' + encodeURIComponent(com) : '') + (com ? '&com=' + encodeURIComponent(com) : '') +
'&v=' + SKETCH_VERSION; '&v=' + SKETCH_VERSION;
var n = +pose || 1; var n = +pose || 1;
@@ -1817,7 +1817,7 @@
// Species detail (lifelist row + every detection). // Species detail (lifelist row + every detection).
var loadSpecies = SPECIES_CACHE[sci] var loadSpecies = SPECIES_CACHE[sci]
? Promise.resolve(SPECIES_CACHE[sci]) ? Promise.resolve(SPECIES_CACHE[sci])
: fetchJson('../api/birdnet-api.php?action=species&sci=' + encodeURIComponent(sci)).then(function (j) { : fetchJson('./avian/api/birdnet-api.php?action=species&sci=' + encodeURIComponent(sci)).then(function (j) {
SPECIES_CACHE[sci] = j; SPECIES_CACHE[sci] = j;
return j; return j;
}); });
@@ -2568,7 +2568,7 @@
} }
var ctx = getSpecCtx(); var ctx = getSpecCtx();
if (!ctx) { fail('WebAudio not available'); return; } if (!ctx) { fail('WebAudio not available'); return; }
fetch('../api/recording.php?file=' + encodeURIComponent(file)) fetch('./avian/api/recording.php?file=' + encodeURIComponent(file))
.then(function (r) { .then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
return r.arrayBuffer(); return r.arrayBuffer();
@@ -2631,7 +2631,7 @@
prow.classList.add('expanded'); prow.classList.add('expanded');
ensureSpectroImage(prow); ensureSpectroImage(prow);
var strip = prow.querySelector('.rec-spectro'); var strip = prow.querySelector('.rec-spectro');
var audio = new Audio('../api/recording.php?file=' + encodeURIComponent(pfile)); var audio = new Audio('./avian/api/recording.php?file=' + encodeURIComponent(pfile));
modalAudio = audio; modalAudio = audio;
audio.addEventListener('loadedmetadata', function () { audio.addEventListener('loadedmetadata', function () {
strip.classList.add('armed'); strip.classList.add('armed');
-6
View File
@@ -1,6 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title>AvianVisitors</title>
<script>location.replace('frontend/');</script>
<meta http-equiv="refresh" content="0; url=frontend/">
<a href="frontend/">collage</a>
+14 -2
View File
@@ -69,8 +69,20 @@ create_necessary_dirs() {
sudo -u ${USER} ln -fs $my_dir/include_species_list.txt $my_dir/scripts sudo -u ${USER} ln -fs $my_dir/include_species_list.txt $my_dir/scripts
sudo -u ${USER} ln -fs $my_dir/whitelist_species_list.txt $my_dir/scripts sudo -u ${USER} ln -fs $my_dir/whitelist_species_list.txt $my_dir/scripts
sudo -u ${USER} ln -fs $my_dir/homepage/* ${EXTRACTED} sudo -u ${USER} ln -fs $my_dir/homepage/* ${EXTRACTED}
# AvianVisitors overlay — served at http://birdnet.local/avian/ # AvianVisitors overlay. The avian/ symlink keeps assets + PHP shims
[ -d $my_dir/avian ] && sudo -u ${USER} ln -fs $my_dir/avian ${EXTRACTED}/avian # reachable at /avian/. The five frontend files at the EXTRACTED root
# make the collage the default index for http://birdnet.local/ -
# Caddy serves index.html before index.php, so BirdNET-Pi's stock UI
# moves to http://birdnet.local/index.php (still linked from the
# AvianVisitors menu drawer).
if [ -d $my_dir/avian ]; then
sudo -u ${USER} ln -fs $my_dir/avian ${EXTRACTED}/avian
sudo -u ${USER} ln -fs $my_dir/avian/frontend/index.html ${EXTRACTED}/index.html
sudo -u ${USER} ln -fs $my_dir/avian/frontend/styles.css ${EXTRACTED}/styles.css
sudo -u ${USER} ln -fs $my_dir/avian/frontend/apt.js ${EXTRACTED}/apt.js
sudo -u ${USER} ln -fs $my_dir/avian/frontend/masks.json ${EXTRACTED}/masks.json
sudo -u ${USER} ln -fs $my_dir/avian/frontend/dims.json ${EXTRACTED}/dims.json
fi
sudo -u ${USER} ln -fs $my_dir/model/labels.txt ${my_dir}/scripts sudo -u ${USER} ln -fs $my_dir/model/labels.txt ${my_dir}/scripts
sudo -u ${USER} ln -fs $my_dir/scripts ${EXTRACTED} sudo -u ${USER} ln -fs $my_dir/scripts ${EXTRACTED}
sudo -u ${USER} ln -fs $my_dir/scripts/play.php ${EXTRACTED} sudo -u ${USER} ln -fs $my_dir/scripts/play.php ${EXTRACTED}