Skip to the content.

Next steps for setlist-admin-usb

Leer en español

Read this first if you’re picking this project back up – whether you’re Claude or any other AI agent, human contributor, or future-me. It’s written to be understood cold, without the conversation history that produced it.

Where things stand (as of commit 9209a29, 2026-09-27 – see the

git log for anything newer, this file lags slightly behind commits by nature)

The Show/Set/Bank terminology rename (see CHANGELOG.md’s “Renamed the library’s terminology…” entry) and a full real-hardware validation pass of setlist-admin-usb are committed, pushed to main, and confirmed persistent on the real Pi (survived an actual reboot cycle after being deployed correctly – see “Deploying to the Pi” below for why that’s not automatic). setlist-admin-wifi got the same code changes for parity but has not been hardware-tested itself (this snapshot is from 2026-09-27, before a working WiFi dongle was found – see “Where things stand” entries below and its own SPECIFICATION.md for the current status).

Real bugs found and fixed during this pass are listed in detail in CHANGELOG.md’s Unreleased section and in SPECIFICATION.md sections 0/14 – don’t re-discover these:

An Export Set feature was added: a full-screen, large-print running order for the selected Set, with a “Share” button that renders it to a PNG and hands it to the phone’s native share sheet. See SPECIFICATION.md section 14 for the design.

Fixed and user-confirmed working (2026-09-27, after the above): selecting a Set from the dropdown never actually marked it as the pedal’s active Set – only creating a new Set did, via set_active_set. Switching between Sets that already existed silently did nothing for playback, no matter how many times you selected a different one. Fixed: “Reboot now to apply” now calls POST /api/sets/active for whichever Set is currently selected immediately before rebooting. Not ported to setlist-admin-wifi – that expansion has no reboot button of its own (its USAGE.md already tells users to ssh pedal sudo reboot by hand instead), so the equivalent fix there would need its own UI decision (e.g. set-active immediately on dropdown selection, since there’s no “apply” moment to hook into) – not yet designed, let alone built.

Also documented (2026-09-27, no code change): a consolidated answer to “is the Pi safe against a power cut, and where are the real risk windows” in TROUBLESHOOTING.md/docs/es/TROUBLESHOOTING.md (new section near the top) and systemd/README.md section 4. Short version: safe during normal band use (root/boot/USB are all RAM-backed or ro at that point); the real windows are a library edit’s brief /media/usb rw remount, the overlay disabled for development, and hand-editing cmdline.txt – the last two maintenance-only. An underpowered supply (not a clean unplug) is flagged as the more likely real-world trigger, per this project’s own observed under-voltage event.

2026-10-01, real incident investigated and fixed (code change, not yet deployed to the Pi as of this file – check VERSION/git log for whether it’s landed there yet): the user reported audio stopping completely after about an hour of intensive real-hardware testing. While investigating live over SSH, the M-VAVE turned out to be powered off, and pedal-core.service was caught mid-crash-loop (restart counter climbing every ~10s) – main.py used to treat “MIDI controller not found” as fatal, exiting and relying entirely on systemd blindly restarting the whole process (tearing down and relaunching both mpv lanes every cycle – visible flicker, total silence, zero on-screen explanation) for as long as the controller stayed missing. Fixed: see CHANGELOG.md’s Unreleased entry and TROUBLESHOOTING.md’s new “Audio (and video) stop completely…” section for the full writeup: main.py now retries the MIDI connection in-process without tearing mpv down. (A second fix landed the same pass – logging mpv’s own stderr instead of discarding it – but was reverted the same day: that log lives in the RAM-backed root overlay with no rotation, so leaving it on permanently was judged not worth the standing RAM-exhaustion risk for a benefit that’s only useful during an active debugging session. Capture it temporarily, by hand, next time it’s actually needed – don’t redeploy that patch permanently. See CHANGELOG.md’s v2026.10.01 entry for the full reasoning.) Also added, same session, per explicit user request: standby video management in both setlist-admin apps (pick any library video as the new standby.mp4 – USAGE.md, library_ops.set_standby_video()). Deployed and verified on the real Pi (2026-10-01, commit 886d4d0, cut as v2026.10.01): full overlay-disable/deploy/test/re-enable/ reboot dance completed; all 324 tests (27+139+158) passed on the Pi itself; pedal-core.service restarted cleanly with the M-VAVE connected and confirmed persistent after the final reboot; get_standby() confirmed reading the real mounted USB’s actual standby.mp4 correctly. Not independently confirmed: actually tapping “Set as standby” from a phone and visually seeing the new video loop – this would overwrite the band’s real, currently-in-use standby.mp4 (754MB), so it wasn’t done live without the user there to confirm the result and restore if needed. Do this first, same as the Export Set precedent below.

2026-10-01, later the same day – library-wide “Optimize” button, built, deployed, and real-hardware-tested (commits e1fafe6, 8f187fb): real user request, right after the standby-video manual conversion above – instead of re-encoding a flagged video by hand over SSH every time, the Song library now shows an “Optimize” button next to any file codec_check.is_optimized() flags, backed by a new always-on library-optimizer.service daemon and a persistent, file-based job queue (optimize_queue.py) so a long job survives the phone that queued it disconnecting. See CHANGELOG.md’s Unreleased entry and SPECIFICATION.md section 15 for the full design. Ported to both expansions, 181 (USB) + 200 (WiFi) tests passing.

Deployed to the real Pi the same session, full overlay dance (see “Operational notes” below – raspi-config nonint do_overlayfs 1 alone did not strip the custom :recurse=0 suffix from cmdline.txt, needed a manual sed on top, same as disabling it; worth remembering next time instead of re-discovering it). Found and fixed a real bug in the process: library-optimizer.service crash-looped under systemd (ModuleNotFoundError: No module named 'admin') because library_optimizer.py never added src/ to sys.path the way server.py does – invisible to the test suite, which already puts src/ on the path itself. Fixed, redeployed, confirmed genuinely running (not just “active” mid-crash-loop).

Then ran a real end-to-end test over SSH: a tiny synthetic HEVC clip (ffmpeg -f lavfi testsrc, 3s) uploaded via AdminAPI.upload_song(), queued via request_song_optimization(), picked up by the daemon in a few seconds, confirmed re-encoded to H.264 and the queue cleared (needs_optimization: False afterward). pedal-core.service and standby.mp4 playback were unaffected throughout. Test song deleted afterward. Not yet confirmed: the actual phone UI flow – tapping “Optimize” from the app itself, watching the button read “Optimizing…”, and specifically disconnecting/reconnecting the phone mid-job to confirm the state really does persist visually, not just at the AdminAPI/queue-file level already proven above. Do this next – the user is about to run it as of this writing.

2026-10-01, later the same day – a real standby.mp4 data-loss incident, found and recovered: the user reported the band’s actual standby video (~55 minutes, ~754MB) was gone – replaced earlier that day through the app’s own “Set as standby” picker with no way back. Root cause, confirmed in code: library_ops.set_standby_video() just does _atomic_copy_file() straight over standby.mp4; it never backs up whatever it’s replacing. Searched the whole USB (library, every Set/Bank, the old backup/ folder) – no trace. Recovered only because the user still had the original, unconverted source file (a real band video file, 6.68GB, H.264 1080p but at ~15.7Mbps – far too heavy to use directly) on a separate computer. Re-encoded it to ~1.8Mbps (matching the lost original’s own bitrate almost exactly: 754MB over 55.66 minutes is ~1.8Mbps) using h264_v4l2m2m for both decode and encode – confirmed live at ~0.87x realtime, dramatically faster than the ~0.1x this project saw earlier for software-only HEVC decode, since the source here was already H.264 and this Pi’s hardware codec handles that natively both ways. Uploaded the ~800MB result into the library under a new name (plain AdminAPI.upload_song(), deliberately not set_standby_video() – the user wanted to choose if/when to make it standby again themselves, not have it forced). They later did exactly that through a temporary web session (see below) and rebooted to apply it – confirmed via mpv’s own IPC socket that standby.mp4 is now that exact file. set_standby_video() still has no backup step – see “Deferred product decisions” below.

Same investigation also surfaced a transfer-side gotcha worth remembering: a new USB WiFi dongle (see “Where things stand” above) was connected while a 6.68GB file was mid-scp-transfer to the Pi, and the user asked to switch off Ethernet once it was safe – confirmed dual-interface (eth0+wlan0) mDNS/pedal.local resolution gets flaky once both are up (same root cause as the long-documented eth0+eth1 version of this issue, just with a WiFi interface instead of a second wired/tethered one). Falling back to the direct WiFi IP with the right -i ~/.ssh/id_ed25519_pedal -o IdentitiesOnly=yes resolved it every time.

Also found live, worth remembering verbatim: pkill -f '<pattern>' run over SSH can kill its own SSH session if the pattern text you pass happens to appear in the invoking shell’s own command line (which it will, trivially, since you just typed that exact string as an argument) – pkill -f matches full command lines, not just the target process’s. Killed the remote bash wrapper instead of ffmpeg, surfacing as a bare SSH exit 255 with no remote-side error at all. Fix: match by exact process name instead (pkill -TERM ffmpeg, no -f), or make the pattern specific enough that it can’t also match its own invocation.

Also this same investigation: a code fix that was only ever “quick-deployed” (pulled while the protective overlay was active, so it only ever lived in the RAM-backed upper layer) was silently lost when the overlay was later disabled-and-rebooted for an unrelated reason (needing real disk space for the 6.68GB conversion above) – the git checkout reverted to the last commit that had actually been durably deployed. Re-pulled once noticed (confirm with git log --oneline -1 after any reboot, any reboot, not just ones from your own deploy work – don’t assume a quick-deployed commit survived just because nothing you did should have rebooted the Pi).

2026-10-02: Optimize/Cancel UI confirmed live, a full day of

real-hardware bugs, and a language toggle

The “Optimize” button’s real phone-UI flow (item 1 below, as it stood at the end of 2026-10-01) is now thoroughly confirmed – not just once, but across an extended real-usage session: tapped “Optimize” from the phone itself multiple times, watched the warning popup, watched the standing green “Optimizing” button alongside “Cancel”, tapped “Cancel” itself (including while ffmpeg was actively mid-encode, confirmed via direct AdminAPI timing: the call returned in ~0.02s and the real ffmpeg process was gone within ~10s), and confirmed the song returns to a clean, un-optimized state afterward ready for a fresh “Optimize” tap. The disconnect/reconnect persistence this item asked for is inherent to the design (the queue is file-based, the daemon is a separate systemd unit) and was exercised incidentally throughout the day’s testing without issue.

A long chain of real bugs were found and fixed the same day, each from a genuine user report while actively using the app, not from a review pass – full detail, one entry per fix, in CHANGELOG.md’s [Unreleased] section (newest first): a tablet-only form-centering bug; a ~37s login-to-songs-loaded delay (serial, uncached ffprobe calls); a transient ffprobe timeout under load getting cached permanently as “not optimized”; ffmpeg holding the USB-mounted source open for an encode’s entire duration, blocking every other library write (e.g. an unrelated “create Bank”) the whole time; a leftover disclaimer baked into the Export Set share image specifically (the on-screen one had already been removed); no Cache-Control on static files, letting a stale app.js/index.html combination serve a page that looked stuck showing only the topbar; the same gap for GET /api/songs specifically, showing a song as already-optimized while a job was genuinely still running; and, found last, cancelling a job could 500 if tapped within roughly the first minute (while the new scratch-copy step was still reading the source off the USB) – fixed by moving the cancel marker off the USB entirely, onto local Pi storage (optimize_queue.DEFAULT_STATE_DIR), since it never needed to live there in the first place. Confirmed live via a direct, timed AdminAPI.cancel_song_optimization() call against a real, actively-encoding job.

Also added, same day, real user request: a full ES/EN language toggle (static/i18n.js, both expansions) – a dropdown in the topbar (now reading “ChocolatePi - Setlist Admin”) switches every label, button, confirm, alert, and toast between English and Spanish, persisted per-browser via localStorage. Song/track/Set/Bank names are explicitly never translated (they’re user data, not UI chrome) – every render function in app.js keeps those out of the translation calls on purpose. Server-sent error messages stay in English for now, a separate, explicitly deferred piece of work if ever wanted. An auto-refresh (GET /api/songs every 5s while anything is queued/running, stopping itself otherwise) was added alongside it so the Cancel/Optimize button state never needs a manual refresh to catch up.

Not yet done, worth doing soon: USAGE.md/docs/es/USAGE.md (both expansions) and CHANGELOG.md/docs/es/CHANGELOG.md were updated for all of the above as of this writing – but VERSION was not bumped (no version was “cut” this session; everything above still sits in [Unreleased]). If a clean checkpoint is wanted, that’s the next small step: confirm nothing’s regressed, bump both expansions’ VERSION files, rename [Unreleased] to today’s date.

2026-10-02, later the same day – a second real incident, a full user QA pass, and a disk-space cleanup. Starting a second “Optimize” job while another’s source was still mid-copy off the USB could also 500, for the same underlying reason as the cancel fix above (setlist-admin.service and library-optimizer.service are two separate processes – the in-process lock in usb_mount.py never coordinated between them). Fixed with a real cross-process file lock; see CHANGELOG.md’s Unreleased entry.

The user then ran a full manual QA pass against every feature touched today, reporting back item by item. Real findings, all fixed the same session (detailed, one per item, in CHANGELOG.md): a pedal-core. service crash-loop under heavy CPU load (confirmed live – the pedal showed mpv’s idle screen for a couple of minutes); list_sets() listing filesystem-reserved folders as selectable Sets; “Queued” and “Optimizing” rendering identically (confirmed live: this caused cancelling the wrong job by mistake); a slow library upload with no early duplicate-name check; raw untranslated network errors; missing tap feedback on the login/standby buttons; and manual Bank numbering (now auto-sequential, real user request: a MIDI controller steps through Banks one at a time, so there’s never a real reason to pick anything but the next number – not capped at any specific controller’s own physical bank count).

Two things that came up during this pass are not code bugs, confirmed by digging into the actual behavior rather than guessing:

That low-disk-space finding led to a real cleanup: the two largest not-yet-optimized video files in the library (several GB combined) were deleted by the user from the app itself, freeing roughly 2.7GB (from ~925MB to ~3.7GB free, confirmed via df before/after). A smaller handful of oddly-generic-named .wav files (named like “track N” rather than a real song title, ~180MB combined) were flagged as possible leftover test data but intentionally left alone pending the user’s own confirmation – don’t delete those without being told to.

Operational note, worth remembering: usb-tether-watchdog.service actively enforces “setlist-admin.service only runs while a phone is USB-tethered” – it re-stops the admin app within a few seconds of any manual systemctl start, confirmed live via its own journalctl output (repeated systemctl stop setlist-admin.service calls, several seconds apart). As of 2026-10-03, this is no longer the only way in – the watchdog also starts the admin app automatically whenever the Pi’s WiFi is connected to its own trusted home network, with no manual override needed (see the entry right below, and USAGE.md’s “Reaching the app over WiFi”). The override trick below is now mainly useful for reaching the admin app over some other network the watchdog doesn’t trust (developer/SSH use): sudo systemctl stop usb-tether-watchdog.service first, then sudo systemctl start setlist-admin.service – and remember to sudo systemctl start usb-tether-watchdog.service again afterward to restore the normal automatic behavior, or the admin app will simply stay up (and reachable) indefinitely instead of following the tether/WiFi convention the rest of this project relies on.

2026-10-03: resource-aware video lane (base pedal), and the WiFi

expansion formally deferred to the roadmap

Base pedal change, not this expansion, but relevant to anyone touching the Pi this week. src/core/ gained a resource-aware video lane: with no HDMI display connected, the video-playing mpv process (confirmed costing ~106% CPU and ~300MB RAM continuously, even just looping standby) is never started at all, audio-only tracks still play normally, and video tracks fall back to audio-only until a display shows up. Plugging in HDMI starts the video lane fresh (including catching a display connected after boot, which previously needed a manual pedal-core.service restart – confirmed as today’s opening bug report). Unplugging HDMI mid-clip never cuts the clip’s audio – the lane only actually stops once the current clip (or standby) is free to do so. A real regression was found and fixed the same day: _MpvProcess’s stop flag was never reset between stop/start cycles, so the first reconnect after a disconnect left mpv showing its idle screen (“Drop files or urls to play here”) instead of standby – confirmed fixed across multiple real disconnect/reconnect cycles via the user’s own pasted logs. Unrelated to this expansion’s own code; mentioned here only because it changes what “the pedal is idle” looks like on the Pi this expansion also runs on.

setlist-admin-wifi is now formally a roadmap item, not a near-term one. Confirmed live via nmcli that the Pi’s initial setup (Raspberry Pi Imager’s wireless-LAN configuration step) already saved a WiFi profile (autoconnect: yes) for the band’s home network – meaning any USB WiFi dongle plugged into the Pi reconnects to that same network by default, with no extra configuration needed. Given that, a full revival of the setlist-admin-wifi expansion (its own hotspot-configuration UI, encrypted on-USB credential storage, etc.) isn’t worth doing short-term. Instead, a smaller, independent feature was added right here in setlist-admin-usb: see “Reaching the app over WiFi” in USAGE.md. setlist-admin-wifi itself is untouched code-wise – just reframed in its own docs as “implemented, unit- tested, deliberately not being taken further for now.”

Later the same day: the WiFi-reachability feature’s first real incidents, a design question answered, and the first-ever uninstall/ reinstall run. A real user QA pass against the new feature found a real bug within minutes of using it: upload_song()/assign_track() used to read an upload’s network-backed stream inside _writable_usb(), so a slow WiFi upload (confirmed live, 76MB over ~15 minutes) held pedal-core.service stopped – no standby, no playback at all – for the entire transfer, not just the actual (fast) disk write. Fixed by receiving to local scratch first (AdminAPI._receive_to_scratch()), same scratch-then-copy pattern library_optimizer.py already used. Verified live with a throttled synthetic upload against the real Pi: pedal-core.service stayed active for the whole “slow” phase, down only ~1.4s at the very end – the same brief, pre-existing cost any write already has when mpv happens to have the file open.

Checking on that same Optimize job surfaced a second real finding: ~470MB of orphaned scratch files had piled up in ~/pedal-optimizer-scratch/ from a day of service restarts interrupting in-progress jobs – recover_orphaned_jobs() reset the queue’s state but never knew those files existed. Fixed with the same pattern: run_forever() now sweeps the whole scratch directory unconditionally at every startup, before the first tick – always safe, since a job reset to “queued” gets a fresh scratch copy under a new random name next time. Confirmed live: a restart logged “Removed 7 leftover scratch file(s),” ~391MB freed instantly.

Design question, explicitly asked by the user: does relocating the band’s whole setup to a new home WiFi network (new router, new location) need any code change, or does the admin app just come back up automatically once the documented network-change procedure is followed? Confirmed no code change needed, by the implementation itself – wifi_connected_to_profile() only ever reads the active connection’s name via nmcli, never its SSID or password, so it structurally cannot be affected by which real network a profile points to, as long as the profile keeps the name "preconfigured" (which the documented nmcli connection modify procedure does). Added a dedicated regression test proving this and documented the guarantee explicitly in SPECIFICATION.md, USAGE.md, systemd/README.md, and TROUBLESHOOTING.md (all EN+ES).

That same documentation pass also caught a few smaller, pre-existing loose ends while reviewing everything end to end: a stale “SETLIST_ADMIN_USB_SPECIFICATION.md” filename reference (the real file has always been SPECIFICATION.md) left over in several docstrings from before a rename, the same issue in setlist-admin-wifi’s own code (27 occurrences, mechanically fixed the same way), and SPECIFICATION.md section 11 undercounting install.sh’s services (said two, it’s actually three, has been since 2026-10-01) with no mention of the scratch directories rollback.sh cleans up.

Finally: see item 4 in the “Ordered test plan” below – rollback.sh was run for real, for the first time ever, which is how the ordering bug above was actually found (not by reading the code – by running it).

What’s genuinely unconfirmed – do these before trusting them

  1. Export Set’s visual fix – confirmed by the user, 2026-10-02, during the full QA pass: opened with “Live” selected (title + numbered list populated correctly), closed via ✕ and via the phone’s back gesture, “Share” produced a clean image with no leftover disclaimer. Done.
  2. The scroll-jump fix – never got one isolated, dedicated check, but was exercised incidentally dozens of times across the same QA pass (renames, deletes, Bank/track edits) with no complaint raised. Treating this as confirmed by extensive informal use; revisit only if a jump is ever actually seen again.
  3. Desktop/PC browser support for Export Set’s “Share” button is still explicitly uncertified – navigator.share() with file attachments has poor desktop browser support; the code falls back to a plain download, but this has never been tested from an actual PC. Do this once setlist-admin-wifi’s own real-hardware validation happens (see “Where things stand” above), since that’s the expansion meant to be reached from a computer on the home network.
  4. Whether the four oddly-generic-named .wav files in the library (“track N” rather than a real song title, ~180MB combined) are real setlist content or leftover test data is still an open question – flagged to the user 2026-10-02, never answered. Don’t delete them without being told to; ask again if it comes up.

Ordered test plan – continue here

This is the same list the user asked to go through “paso a paso” (step by step), several sessions ago. Items 1-4 are now done:

  1. Reboot-to-apply – done, validated with a real reboot.
  2. Mid-edit disconnect resilience – done; found and fixed the orphaned-temp-file bug (see above).
  3. Rename/delete a song from the library UI – confirmed working, including the scroll-jump concern (see “genuinely unconfirmed” #2 above).
  4. Uninstall / rollback – done, 2026-10-03, run for the first time across this entire project’s history. Found and fixed a real bug doing it: rollback.sh disabled setlist-admin.service before usb-tether-watchdog.service, leaving a window where the watchdog (now also WiFi-aware, not just phone-tether-aware) started it right back up moments later – confirmed live via systemctl status showing it active again minutes later with Loaded: not-found. Fixed by disabling the watchdog first. Re-ran the full cycle after the fix (rollback.sh → install.sh) and confirmed: pedal-core. service was active throughout, never touched; the PIN hash (md5sum-compared before/after) and all 6 Banks in the Live Set survived byte-for-byte; the admin app came back up on its own over WiFi with no phone tethered, immediately after the fresh install, exactly as designed.
  5. (Optional, non-blocking) Test with a second phone (ideally Android, to exercise the rndis_host/cdc_ether/cdc_ncm driver paths that this session only ever exercised with one iPhone’s ipheth).

Deferred product decisions – not yet built, need a decision first

Highest priority of the three below, given it already cost real data once: back up the previous standby.mp4 before replacing it. library_ops.set_standby_video() currently just copies the chosen library song straight over standby.mp4, no questions asked – fine right up until someone picks the wrong one, which is exactly what happened 2026-10-01 (see “Where things stand” above): the band’s real ~55-minute standby was gone, unrecoverable from the Pi itself, and only came back because a copy happened to still exist elsewhere. The user was asked in the moment whether to add an automatic backup and said yes to recovering the lost video specifically, but the actual backup feature itself was never built in the session that followed – revisit this explicitly rather than assuming it’s done. A reasonable shape: before the copy, move the current standby.mp4 into something like backup/standby-previous.mp4 (single slot, overwritten each time, not an ever-growing history – this USB has limited space and a full history isn’t the goal, just “don’t lose the one before this one”). Same _atomic_copy_file()/writable_usb() machinery already in use elsewhere, so this is a small, self-contained change once someone decides on the exact backup naming/retention shape.

Two other things the user raised and explicitly asked to defer:

Operational notes for whoever deploys to the Pi next

These cost real time to discover this session – don’t re-learn them the hard way: