Skip to content

BirdLense Hub configuration

Русский


Config file: app/app_config/user_config.yaml

Defaults: app/app_config/default_config.yaml. User config is merged on top.

Precedence: environment variables > user_config.yaml > default_config.yaml. Example: GO2RTC_URL in env overrides video.go2rtc_url in YAML.

UI: Most options are editable in the web app (Settings → gear). YAML remains for advanced cases and env-based overrides.

Related: ACCESS_CONTROL (password tiers), API (HTTP surface), GLOSSARY (terms).


How keys are named

  • Tables use dotted paths that mirror YAML nesting, e.g. video.go2rtc_urlvideo:go2rtc_url: in user_config.yaml.
  • Boolean defaults like “empty password = open hub” are documented in ACCESS_CONTROL.

Environment variables

Variable Description
DATA_DIR Data directory (/app/data in Docker)
REDIS_URL Docker: set by default in docker-compose.yml (redis://redis:6379/0, service birdlense-redis). Override in app/.env for an external Redis. Local run without Compose: unset → in-process memory cache.
DATABASE_URL Optional. SQLAlchemy URI. Default: SQLite under DATA_DIR. For high write load use PostgreSQL, e.g. postgresql+psycopg://user:pass@host:5432/dbname.
SQLALCHEMY_POOL_SIZE PostgreSQL pool size (default 5)
SQLALCHEMY_MAX_OVERFLOW PostgreSQL pool overflow (default 15)
FLASK_SECRET_KEY Flask session key (settings protection)
PROCESSOR_SECRET Processor API protection (X-Processor-Token)
MCP_TOKEN MCP token (overrides mcp.token)
BIRDLENSE_PORT Nginx port (default 8085)
CORS_DEFAULT_ORIGINS Baseline CORS origins (comma-separated) for non-localhost defaults
CORS_ORIGINS Extra CORS origins (comma-separated)
OPENWEATHER_API_KEY OpenWeather key
MQTT_BROKER, MQTT_PASSWORD MQTT if not in config
HA_TOKEN Home Assistant token
GO2RTC_URL Go2RTC URL if not in config
BIRDLENSE_STARTUP_BACKFILL_SPECIES_TAXA 1 — run species→taxon backfill on app startup; default off; otherwise use POST /api/ui/system/species-registry/backfill
BIRDLENSE_STARTUP_CLEANUP_LEGACY_IMPORT 1 — remove legacy disk-import placeholder detections on startup; default off; recording scan still cleans
BIRDLENSE_STARTUP_REPAIR_SPECIES_METADATA 1 — background metadata/image repair on startup; default off
BIRDLENSE_NOTIFY_APP_STARTUP 0 — skip Telegram “App is UP!” on startup; default on

See app/.env.example. Secrets are generated by make setup (via make start / make pull).


General

Key Description
settings_password Admin password: settings, feeder dispense, system tools, processor restart. Empty = no lock (home lab default)
require_auth_for_video_stream false (default): guests can play recordings (/api/ui/videos/:id/stream), consistent with ACCESS_CONTROL. true: stream requires Contributor/Admin (legacy lock-down).
contributor_password Optional Contributor password: species fixes, Unknowns, iNaturalist, dataset export, reports — not settings/feeder/system. Empty = single-tier mode (see ACCESS_CONTROL)
enable_notifications Enable notifications (global)
notification_excluded_species Species excluded from notifications
birdnet_url Link to your BirdNET installation (BirdNET-Pi/Go). Empty = link/icon hidden.
heimdall_url Heimdall base URL for Hub → Heimdall health probe only (System page). You may use http://heimdall.local if that hostname resolves from the Hub host/container (Docker: shared network, extra_hosts, or LAN DNS). This does not configure Heimdall to read Hub; see below.
donate_url Support link. When non-empty, only the heart icon in the top app bar is shown. Empty = hidden.

Platforms: RU — Boosty, DonationAlerts, DONAT24, YooMoney. Elsewhere — Ko-fi, GitHub Sponsors, Patreon. Settings → General → paste page URL.

Heimdall vs Hub metrics (direction)

  • Heimdall → Hub: To show BirdLense status in Heimdall, add an external link or monitoring tile to your Hub, for example:
  • Prometheus text: http://<hub-host>:<port>/metrics or /api/metrics
  • JSON snapshot (same counters as Prometheus): http://<hub-host>:<port>/api/metrics/summary
  • Hub → Heimdall: The heimdall_url field is only used so the Hub can probe your Heimdall instance from the server (latency, HTTP status, title). It is not a substitute for exporting Hub metrics.

The System page also lists these endpoints under Notification observability (authenticated UI).


Processor

Key Description
tracker Tracker config (bytetrack.yaml)
max_record_seconds Max recording length (seconds)
max_inactive_seconds Max gap without detections
post_record_seconds Post-roll: added to the no-detection gap before stopping the clip. Effective gap = max_inactive_seconds + post_record_seconds. See #157.
min_confidence_binary Detector threshold: bird vs non-bird. Default 0.15
min_track_duration Min track duration (s). Default 3 — fewer false triggers
min_confidence_to_process Classifier threshold: species. Default 0.30. Lower = more detections, higher = stricter
species_confidence_overrides Per-species thresholds: {"Rare Bird": 0.05}
ebird_regional_top_auto_confidence If true (default), merge lower thresholds for species in the regional eBird top (needs secrets.ebird_api_key, ebird.*). Manual species_confidence_overrides keys always win. See #128.
ebird_regional_top_confidence_delta Subtracted from min_confidence_to_process for each auto top species (default 0.05).
ebird_regional_top_confidence_floor Minimum auto threshold (default 0.05).
birdnet_mqtt_auto_confidence If true, lower classifier thresholds for species seen in recent BirdNET MQTT messages (similar to eBird top). Default false. Manual species_confidence_overrides win. See #129.
birdnet_mqtt_bias_window_seconds Look-back window from recording start for BirdNET species (seconds, default 120).
birdnet_mqtt_bias_delta Subtracted from min_confidence_to_process for auto BirdNET species (default 0.05).
birdnet_mqtt_bias_floor Minimum auto threshold for BirdNET bias (default 0.05).
multi_camera_groups List of Frigate camera-id groups at one location, e.g. [["BirdBox","Forest"]]. See #153.
multi_camera_confidence_boost When Frigate reports the same species from two or more cameras in one group, add this to merged confidence (default 0.05, capped at 1.0).
spectrogram_px_per_sec Spectrogram pixels per second (only when BirdNET event in recording window)
regional_species Local species for BirdNET (empty = YOLO all classes)
single_stage_coco_animals_only_auto Default true: if single_stage loads a model with exactly 80 classes (typical COCO, e.g. yolov8n.pt), detect only animal classes (bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe) — excludes person and inanimate COCO objects. Set false for a custom 80-class detector. Legacy: single_stage_coco_bird_only_auto is read if this key is unset.
included_bird_families Family filter list (Perching Birds, Squirrel, …)
save_images Save detection frames
detection_strategy single_stage or two_stage
models.single_stage Single-stage model path (NCNN)
models.binary Binary detector path (.pt)
models.classifier Classifier path (.pt)

Video

Key Description
source go2rtc (file — CLI only)
go2rtc_url Go2RTC URL (http://YOUR_HOST:1984)
cameras List: {id, stream_name, name}
pre_record_seconds Pre-roll before trigger
auto_reconnect Auto-reconnect to stream
video_width, video_height Resolution

Motion

Key Description
source opencv | frigate | mqtt | esphome
frigate_camera_filter Frigate cameras (from cameras) or empty = all
frigate_label_filter Frigate labels to allow (bird, Bird)
frigate_label_exclude Labels to ignore (cat, dog — mouse as cat)
mqtt_topic MQTT binary sensor topic (Tasmota PIR)
esphome_url ESPHome URL
esphome_sensor_id binary_sensor id in ESPHome

MQTT

One connection — Frigate and BirdNET topics. Triggers: Frigate, BirdNET (when MQTT), ESPHome, MQTT binary, OpenCV. YOLO runs after trigger.

Key Description
broker Broker address
port Port (1883)
frigate_topic Frigate events topic
birdnet_topic BirdNET topic
publish_topic BirdLense detection publish topic
reconnect_min_delay Minimum MQTT reconnect/backoff delay (seconds)
reconnect_max_delay Maximum MQTT reconnect/backoff delay (seconds)
ha_discovery Home Assistant MQTT discovery — Last Species, Bird at Feeder, etc. Default true.

Topics: frigate/events (Frigate), birdnet (BirdNET), birdlense/detections (publish), birdlense/sensor/last_species/state (HA), birdlense/binary_sensor/bird_detected/state (HA). Feeder relay: homeassistant/switch/bird_feeder/command.

BirdNET: CommonName, Confidence, BeginTime (merge), ScientificName, BirdImage.URL. Frigate: aftercamera, label, sub_label (species from Bird Classification), frame_time. sub_label wins over label.

Missed-event note: during outages, MQTT events can be missed and are usually not replayed later (Frigate events are typically a live stream, not backlog replay). Use Frigate recording/clip retention as the source of historical truth.


Feed

Key Description
source mqtt | esphome
duration_seconds Relay on duration
mqtt_topic MQTT relay topic (Tasmota)
esphome_url ESPHome URL
esphome_switch_id Switch/button id
esphome_type switch | button

Last dispense: Hub writes data/feed_last_dispense.json on successful dispense. Overview feeder card shows last dispense time.


Weather

Key Description
source openweather | homeassistant
ha_url Home Assistant URL
ha_entity_id Weather entity (weather.home)

Detection (merge YOLO + Frigate + BirdNET)

Sources: YOLO (video, EU ~491 species), Frigate (sub_label from Bird Classification), BirdNET (audio). UI shows source per species. One result per species (max confidence).

Canonical names: Common name (Eurasian Jay), not scientific. species_mapping maps variants. species_canonical_mapping.txt for “Merge duplicates” (System → Recordings). Format: variant|canonical.

Catalog quality: app/web/seed/species_suspect_blocklist.txt lists terms used to hide non-bird / object rows from filtered species pickers (GET /api/ui/species?exclude_suspects=1 when requested). Full report (suspects, duplicate-name merge candidates): System → “Species catalog data quality” or GET /api/ui/system/species-registry/data-quality (settings password). New ingest matching the blocklist does not create a junk species row — it is routed to “Unknown”.

Classifier dataset alignment (EU ~491 / US NABirds ~400): in user_config.yaml, species.catalog_allowlist_file points to a text file of class display names (one per line, same as merged_cls / YOLO-normalized). Generate from your best.pt with scripts/datasets/dump_classifier_allowlist.py (e.g. write models/classification/weights/class_names.txt under app/processor). Set species.catalog_strict_ingest: true to block new species outside that list (detections go to “Unknown”). Bulk cleanup of existing junk and duplicate names: POST /api/ui/system/species-catalog/reconcile (always try {"dry_run": true} first). Compare classifier vs DB vs data/dataset folders: System → “Classifier vs catalog vs dataset”.

Key Description
merge_window_seconds MQTT merge window (8 s)
dedup_window_seconds Gap > N s = new visit (60 s)
one_per_species One result per species (true)
source_priority On conflict: ["yolo", "frigate", "birdnet"]
cross_source_confidence_bonus When MQTT (Frigate/BirdNET) first merges into an existing YOLO detection, add this to confidence once (cap 1.0). Default 0.02 — small lift without retraining. Set 0 to disable.
min_confidence_to_store Min confidence (0.05)
species_mapping Species name mapping

EU model: best.pt. US: best_US.pt. Training: TRAINING.

Retention

Key Description
days Delete recordings older than N days
max_gb Max size in GB (optional)

Opt-in: when enabled=true and upload_url is set, Hub POSTs best frames. Multipart: image, species, confidence, timestamp, detection_id, video_id, latitude, longitude. Filters: min_confidence (0.5), only_manually_corrected. Test: docker compose -f docker/gallery-test/docker-compose.yml up -dhttp://localhost:8086/api/upload.

Key Description
enabled Enable uploads
upload_url Receiver API URL
min_confidence Only detections ≥ threshold
only_manually_corrected Only human-verified

Troubleshooting

  • Visits in UI but nothing in gallery: Timeline/Overview can include audio (BirdNET) visits. Gallery uploads only VideoSpecies with source=video, non-null frames (track bboxes from the processor), and confidence >= gallery.min_confidence. Pure audio or video rows without frames are not uploaded.
  • min_confidence: default 0.5. If your model outputs e.g. 0.35, lower it in user_config.yaml (e.g. gallery.min_confidence: 0.35).
  • only_manually_corrected: true: only manually corrected detections are uploaded; everything else is skipped until you correct species.
  • upload_url: full POST receiver URL (multipart). Server should return 200, 201, or 204.
  • upload_url from Docker: hostname must be reachable from the web container (e.g. http://gallery-test:5000/api/upload). http://127.0.0.1:… on the host often points at the container itself, not your PC.
  • Logs: Gallery upload: (success), Gallery upload failed (HTTP status), Gallery: video N — нет строк для загрузки (no rows matched filters), Gallery upload thread failed (thread exception).
  • Image quality: Uploads use the same frame extraction as before, plus JPEG normalization (minimum edge, max width/height) for picky receivers. If a bbox crop cannot be produced, Hub falls back to a full frame at the clip midpoint (similar to Telegram preview fallbacks).

Integrations (scales)

Key Description
integrations.scales.enabled Feeder / smart-scale weight path (default false). When enabled, the processor subscribes to MQTT (or reads Home Assistant) and persists the latest weight for the web UI.
integrations.scales.source mqtt (default) or homeassistant.
integrations.scales.mqtt_topic MQTT topic carrying a numeric payload or JSON with weight (processor persists state under DATA_DIR; in Docker the default data tree is app/data).
integrations.scales.homeassistant_entity_id Entity id (e.g. sensor.smart_scale_weight) when source is homeassistant.
integrations.scales.unit kg or g for display and stored values.

Future work: trigger on sharp weight change (threshold / debounce) — tracked in #167.


Notifications (Telegram)

Key Description
general.enable_notifications Enable notifications
notifications.telegram_bot_token Bot token (@BotFather → /newbot)
notifications.telegram_chat_id Chat or channel id (e.g. -1001234567890)
notifications.base_url Hub URL for video/Live links. If empty, relative links cannot be turned into a full URL and Telegram link previews become less useful
notifications.telegram_proxy_type none — no proxy; socks_http — URL below (typical); mtproto — server/port/secret like the Telegram app + api_id/api_hash
notifications.telegram_proxy_url With socks_http: proxy for Bot API (socks5h://…, http://…). Empty = direct. Web image includes requests[socks].
notifications.telegram_mtproto_host / telegram_mtproto_port / telegram_mtproto_secret Only for mtproto; secret is hex from the Telegram app
notifications.telegram_api_id / telegram_api_hash Only for mtproto; from https://my.telegram.org → API development tools (or env TELEGRAM_API_ID / TELEGRAM_API_HASH)
notifications.telegram_api_base Empty = https://api.telegram.org; or your HTTPS reverse proxy base
notifications.telegram_timeout Max timeout seconds for Telegram requests (text uses half)
notifications.telegram_retries Retry count on timeout/connection errors
notifications.compress_photo_over_kb Compress JPEG when larger than N KB (0 = off)
notifications.telegram_max_side_px Max image side in px before send (0 = no resize)
notifications.message_thread_id Forum topic id
notifications.disable_notification Silent messages
notifications.protect_content Disallow forward/save
notifications.link_preview_large Large link previews (Bot API 9.4). This complements photo delivery; it does not replace sendPhoto
notifications.use_custom_emoji icon_custom_emoji_id on buttons (bot owner needs Premium)
notifications.custom_emoji_id_bird Custom emoji id for birds (@Stickers)
notifications.custom_emoji_id_chipmunk Chipmunk/squirrel
notifications.custom_emoji_id_open_live Open Live button
notifications.paid_media_view_star_count Stars for photo view (0=free, 1–25000). sendPaidMedia
notifications.paid_media_forward_star_count Free view: 0=allow forward, >0=block. Paid: forward allowed.
general.notification_excluded_species Excluded species
processor.save_images If true — save detection frames to disk for debugging. It does not control Telegram photo delivery
processor.save_dataset_crops If true — save best_frame to data/dataset/train/<Species>/
processor.dataset_min_confidence Min confidence (0.0–1.0) for dataset crop. Default 0.5

How BirdLense sends Telegram notifications: first it tries to send an actual photo (sendPhoto / MTProto media) from best_frame; if that is unavailable, it falls back to a bbox crop from the video, then to a full frame. If Telegram rejects the media or the preview is broken, BirdLense falls back to a text message with link/button and records the fallback reason in observability (System → Observability).

Telegram Bot API 9.4/9.5: styled buttons, <tg-time format="r">, large previews (link_preview_large).

If my.telegram.org shows ERROR (cannot create app / get keys)

https://my.telegram.org is run by Telegram; BirdLense cannot fix it. It often fails from some networks (VPN on/off, captcha, rate limits).

Without api_id / api_hash: do not use MTProto proxy type in Hub. Choose SOCKS5 / HTTP and set a proxy URL so your server can reach https://api.telegram.org over HTTPS (e.g. your own socks5h://…), or no proxy if Bot API is already reachable. No api_id/api_hash needed — bot token and chat_id are enough.

MTProto mode is only for traffic via an MTProto proxy (like the Telegram app). It uses Telethon and requires api_id+api_hash from my.telegram.org. If the site keeps failing, use SOCKS/HTTP (or direct) until you can obtain keys (other network, VPN, device, or help from someone who can open the site).

A practical public SOCKS5 source for quick testing: ProxyGenerator.

Proxy check example (expect 404/401 from Telegram API — this is normal and means the path to Telegram works): curl --proxy socks5h://IP:PORT --max-time 12 -s -o /dev/null -w "%{http_code}" https://api.telegram.org/botINVALID/getMe

⚠️ Public proxies are unstable and unsafe for long-term production use; prefer your own SOCKS5/HTTPS proxy.

Auto-select best proxy on production server (one-shot): make refresh-telegram-proxy

Scheduled auto-rotation (easy setup): - Install server cron (default every 6 hours): make proxy-rotation-install - Check status and recent logs: make proxy-rotation-status - Remove schedule: make proxy-rotation-remove

The script scripts/refresh-telegram-proxy.sh tests proxies from the Hub host, picks the fastest working one, updates notifications.telegram_proxy_type=socks_http and notifications.telegram_proxy_url, makes a user_config.yaml backup, and restarts containers only when the selected proxy changes.

Important: after updating repository scripts, run make deploy once, then install schedule.

Custom emoji on buttons (Premium)

use_custom_emoji and id fields control button emoji:

Mode Behavior
Off (default) Unicode (🐦, 🐿️, 📺) — visible to everyone
On icon_custom_emoji_idTelegram Premium required for bot owner

When on, configure:

  • custom_emoji_id_bird — bird notifications
  • custom_emoji_id_chipmunk — squirrel/mouse
  • custom_emoji_id_open_live — Open Live

If id missing — Unicode fallback.

How to get custom emoji id:

  1. Send the emoji to @RawDataBot — reply contains custom_emoji_id.
  2. Or @Stickers for pack ids.
  3. Paste numeric id (example: 5368324170671202286) into settings.

Web Push

Browser push (addition or alternative to Telegram). Requires HTTPS (or localhost).

Key Description
web_push.enabled Auto-enabled on first UI subscription
web_push.vapid_public_key Public VAPID (auto-generated)
web_push.vapid_private_key Private VAPID (secret, masked in API)

Setup: Settings → Notifications → Enable Web Push. Browser prompts; subscription stored server-side. Push to all subscribers on species detection.

Requirements: HTTPS (or localhost), general.enable_notifications, notifications.base_url for link in push. UI subscription now requires the same access as Settings (settings_check_access()), so a random LAN client cannot silently enable web_push.enabled.

UI

Key Description Where
unknown_confidence_threshold Threshold (0–1) for “Unknowns” list. Default 0.5 Settings → Advanced

Webhook

Key Description
url POST URL per detection. JSON: species, confidence, time, source. IFTTT, Zapier, scripts

Security limits: only http/https URLs are allowed. Private / loopback / link-local targets (127.0.0.1, 192.168.x.x, 10.x.x.x, localhost, etc.) are blocked so the webhook cannot be abused as an SSRF bridge into your internal network.

Trusted proxy: if Gunicorn is behind a trusted reverse proxy and you want rate limiting to honor X-Real-IP / X-Forwarded-For, set TRUSTED_PROXY=1. Otherwise BirdLense uses only remote_addr.


eBird

Key Description
ebird.country Country code (2 letters: US, RU, …)
ebird.state Region (1–3 chars: NY, CA, MOS for Moscow Oblast)
ebird.location_name Location name for checklist
ebird.protocol Stationary | Traveling | Incidental | Historical
ebird.species_mapping eBird ↔ BirdLense for “Compare to region”. Example: Gray-headed Woodpecker: Grey-headed Woodpecker
secrets.ebird_api_key eBird API for Overview “Compare to region”. Get key: ebird.org/api/keygen

Settings → Advanced. Timeline “Export for eBird” does not need API key. Key is for “Compare to region”.

Semi-automatic mapping hints: Settings → Advanced, button next to ebird.species_mapping loads the regional eBird top and suggests lines (case / fuzzy); GET /api/ui/settings/ebird-species-mapping-suggestions (same access as settings). See #136.

The species filter Regional uses the same regional species list (eBird top in your ebird.* region) plus any species with a BirdNET MQTT detection (detection_provider = birdnet_mqtt in stored detections). See issue #132.

Example Russia, Moscow Oblast: ebird.country=RU, ebird.state=MOS (or MO). API region: RU-MOS.


MCP

Key Description
enabled Enable MCP server
token Access token (or MCP_TOKEN in env)

Prometheus / Grafana

GET /metrics and GET /api/metrics — Prometheus format.

Prometheusprometheus.yml:

scrape_configs:
  - job_name: 'birdlense'
    metrics_path: '/api/metrics'
    static_configs:
      - targets: ['birdlense:8085']  # or YOUR_HOST:port
    scrape_interval: 15s

Metrics: CPU, memory, disk, GPU (if present), birdlense_detections_total, birdlense_species_count, birdlense_videos_total.

Optional (hub exposed beyond a trusted LAN): set BIRDLENSE_METRICS_TOKEN to a non-empty secret — then GET /metrics, GET /api/metrics, and GET /api/metrics/summary return 401 unless the request includes Authorization: Bearer <same token>. Configure your Prometheus scrape job with authorization / bearer credentials per Prometheus docs.

Grafana — Prometheus datasource, dashboard from metrics.

System page metrics history

Separate from Prometheus: SQLite table system_resource_sample, background sampler stores CPU/RAM/disk/GPU snapshots. The UI calls GET /api/ui/system/metrics/history.

Environment variable Default Range Purpose
BIRDLENSE_SYSTEM_METRICS_INTERVAL_SEC 30 10–600 Seconds between samples
BIRDLENSE_SYSTEM_METRICS_RETENTION_HOURS 72 6–720 Drop rows older than this (hours)
DISABLE_SYSTEM_METRICS_SAMPLER 1 / true Disable sampler (tests, debugging)

Alerting (Prometheus + Alertmanager)

Ready-made examples live in the repo (tune thresholds and job labels to match your scrape config):

File Purpose
examples/prometheus/birdlense.rules.yml Alerts: target down, disk/memory/CPU pressure, optional “no new detections in 24h”
examples/prometheus/alertmanager.birdlense.example.yml Minimal Alertmanager route / receivers skeleton

Prometheus — add rule_files next to scrape_configs:

rule_files:
  - 'birdlense.rules.yml'   # path to the copied example

Notes:

  • Default rules assume scrape job_name: birdlense (see up{job="birdlense"}). If you rename the job, update every job= matcher in the rule file.
  • BirdlenseDetectionsUnchanged24h is optional and noisy when the feeder is off-season — increase for, mute in Alertmanager, or remove the rule group birdlense-optional-activity.
  • GPU alerts are not included: birdlense_gpu_usage_percent is only exported when the container sees a usable GPU sysfs path; “stall” is better diagnosed via System → Processor logs and /api/ui/status.

Tracked as issue #57.


Secrets

Coordinates and API keys. Settings → Advanced. Prefer env for keys: OPENWEATHER_API_KEY.

Key Description
openweather_api_key OpenWeather for weather widget
xeno_canto_api_key Xeno-canto for bird songs (xeno-canto.org/account)
ebird_api_key eBird “Compare to region”
latitude, longitude Weather and eBird

Operational rotation (backup, restart, verification, rollback): SECRETS_ROTATION.md.


Bird food (default catalog)

The app ships a curated default list of feeder foods (US + EU-oriented names and hints). Source of truth in code: app/web/seed/seed.pyseed_bird_food(). Image paths point at data/images/food/* in the app bundle.

Existing databases: on each startup, seed() merges any catalog entries missing by name — upgrades pick up new defaults without duplicating rows. The legacy catalog row Apple pieces is removed on startup (see seed.py), including clearing video_bird_food_association links. Operators can still add custom foods via GET / POST /api/ui/birdfood (see API.md).

Tracked as issue #134.


See also

INSTALL · ARCHITECTURE · ACCESS_CONTROL · API · SCENARIOS · GLOSSARY · SECRETS_ROTATION