create_user wrote the profile with .add() (random doc ID). On first login the
FlutterFlow app looks for users/{uid}, doesn't find it, and creates a second,
bare doc - so every console-created user ended up duplicated, and devices
assigned in the console pointed at the doc the app never reads.
Now the profile is written to users/{uid} with created_time set, and the email
is lowercased to match what Firebase Auth stores. If the Firestore write
fails, the just-created Auth account is deleted so no orphan is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TLS for the phone app went live on the VPS on 2026-09-30:
- Mosquitto got a second, non-published listener on 8083 with
`protocol websockets`; port 1883 stays plain TCP for the boards
(ESP32s can't spare RAM for TLS).
- The mosquitto service joined the external Docker network npm_npmnet so
NPM (NPMplus) can reach mosquitto:8083 by name; it stays on `default`
to reach the Console backend at 172.20.0.1:8000.
- NPM proxy host mqtt.bellsystems.net -> http://mosquitto:8083 terminates
TLS and renews the Let's Encrypt cert. proxy_read/send_timeout 3600s
added so NPM doesn't drop idle MQTT connections after 60s. NPMplus has no
"Websockets Support" toggle (always on).
- Verified end to end: a paho client over wss://mqtt.bellsystems.net:443
(path /mqtt) authenticated via the Console backend and received a
heartbeat.
Chosen over native 8883 because NPM already owns 80/443 and certificate
renewal, so there is no extra cert handling on the host, and 443 also gets
through networks that block 8883.
The doc now gives the app's final transport (wss, 443, /mqtt, never 1883,
keepalive < 3600s), the listener/network/NPM layout, the end-to-end test,
rollback steps, and a new known gap: the backend's port 8000 is published
on 0.0.0.0, so the /mqtt/auth/* endpoints are reachable from the internet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Broker logs after the 2026-09-30 restart show some boards (PV26B02BP01R01,
BSVSPR-26C20B-STD10R-2KCDPH) subscribing to vesper/{serial}/control rather
than control/command. The app ACL only allows publishing to control/command,
so the app can't command those boards until their firmware is updated.
Recorded so nobody widens the ACL by accident.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full mosquitto.conf (2026-09-30) has a single plain listener on 1883,
used by the boards. The phone app sends a Firebase ID token as its MQTT
password, so a TLS listener must be added before app users go live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked the live broker on 2026-09-30:
- mosquitto.conf sets no files-backend ACL path. A live test (device A
subscribing to device B's topics) was denied while A's own topics were
delivered, so the files backend does not grant-all and every ACL check
reaches the Console. The missing ACL file is therefore harmless.
- Recorded the container/image, config and passwd locations, which lines
already match the new code, and the one change still pending
(auth/acl cache 300s -> 60s before app users go live).
- Added the copy-paste isolation test so it can be re-run after any broker
config change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/mqtt-app-user-auth.md records what future sessions (and whoever builds
the phone app) need and can't get from the code alone:
- app connection contract: username app_<uid>, Firebase ID token as
password, client id prefix app_<uid>_, TLS only, allowed topics per acc,
- serial field (serial_number, legacy device_id) and the device_serials
mirror + every code path that must keep it in sync,
- the uid-field lookup rule and ACL cache invalidation,
- legacy "vesper" password flag and its log line,
- rollout checklist: backfill, go-auth VPS config (required/recommended),
files-ACL check, TLS listener,
- decisions/gaps: FlutterFlow must sync device_serials itself; the
device_users subcollection is intentionally ignored.
CLAUDE.md gets a short section pointing agents at it before they touch
MQTT auth or anything that edits user_list/status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
melody.uid is never actually populated anywhere in MelodyForm.jsx, so keying
local .bsm storage on it (as the previous commit did) would silently break
for every existing melody. pid is the correct key anyway: it identifies the
underlying archetype binary, and multiple melodies legitimately share one
pid (each remaps the same note sequence to different bells/speed/duration
via its own settings). Deletion is now share-aware — a melody's binary is
only removed from disk once no other melody still references its pid.
Also adds backend/scripts/migrate_melody_binaries_to_local.py to backfill
existing melodies from their old Firebase URLs to local storage, with
--dry-run support and a warning list for pids whose melodies point at
different source files (a pre-existing data issue, flagged for manual
review via each melody's playback button rather than silently resolved).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ESP32 devices can't spare the 40KB+ RAM a TLS client needs, so Firebase
Storage's HTTPS-only download URLs were blocking melody downloads. Binaries
are now written to local disk (./data/melody_binaries) and served through a
new unauthenticated /api/melodies/download/{pid} route, exposed publicly on
a separate melodies.bellsystems.net vhost (plain HTTP, no TLS) so the main
console domain can stay HTTPS-only with no exceptions. Preview audio still
uses Firebase Storage since it's only ever fetched by the HTTPS admin UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>