Skip to content

Batch fix: #11-#16, #18 (untested, needs live device pass) - #18

Draft
dubpixel wants to merge 16 commits into
mainfrom
fix/action-plan-batch
Draft

dubpixel wants to merge 16 commits into
mainfrom
fix/action-plan-batch

Conversation

@dubpixel

@dubpixel dubpixel commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Implements everything from ACTION-PLAN.md that's verifiable without live hardware:

Not done here (need live device access)

Test plan

🤖 Generated with Claude Code

dubpixel and others added 16 commits September 5, 2026 11:52
Found 12.1 GiB of already-expired-but-uncollected artifacts via the API,
deleted them manually, and logged the recurring fix (delete CI artifacts
after they land in a release) as a housekeeping item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Card was single-width at 17px monospace, which wraps mid-octet on a
full IPv4 address (word-break:break-all). Matches the existing
Hostname card's span-2 treatment for another field with unpredictable
length.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
companion-dashboard's main.js checks process.argv.includes('--kiosk-mode'),
not '--kiosk' -- the wrong flag meant kiosk mode (fullscreen + the
auto-started :80 web server) never actually triggered. Confirmed against
upstream source, not guessed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Depends on #13's fix landing first -- companion-dashboard only
auto-starts its /control web server (port 80) when real kiosk mode
is detected. Link only shown while the kiosk service is actually
active (dashboard_enabled() == svc_active), same gating as the
existing Toggle Fullscreen button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
toggle_net()/pin_static() -- the functions the deck's NET key drives --
called write_networkd_config() unconditionally, writing systemd-networkd
files and restarting systemd-networkd. On Raspberry Pi OS, which ships
NetworkManager by default, that does nothing durable: NetworkManager
keeps managing the interface and reasserts its own connection profile's
DHCP setting on next boot, which is exactly the "has to be reselected
every boot" behavior reported.

The manual /network web form already branched on nmcli_available() vs
networkd correctly -- the deck-driven functions never got the same
treatment. Added write_nmcli_config() (nmcli connection modify, which
persists straight to the on-disk profile) and an apply_net_config()
dispatcher, and pointed toggle_net()/pin_static() at the dispatcher
instead of the networkd-only function directly.

get_current_net_mode() in dpx-deck-splash.py already reads live kernel
state via `ip addr`, so it correctly reflects whichever backend actually
applied the change -- no read-side fix needed, this was write-path only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#11, #12)

Two related gaps, one mechanism, per the design decision to solve them
together rather than as separate bolted-on fixes:

- #11: nothing brought dpx-deck-splash.service back once a mode service's
  own Restart=on-failure exhausted its StartLimitBurst -- the deck just
  went dark/stale forever. Fixed with OnFailure=dpx-deck-splash.service
  drop-ins on all three mode units (as .service.d/ overrides, not direct
  edits, since they ship from vendor .deb packages). OnFailure= only
  fires once a unit's ActiveState actually reaches "failed" -- systemd
  holds it in "activating (auto-restart)" between individual retries --
  so this is inherently once-per-real-outage, not once-per-retry.
  Event-driven, no polling.

- #12: dpx-deck-splash.service and the persisted mode service were both
  WantedBy=multi-user.target, racing at boot with Conflicts= picking
  whichever won -- confirmed nondeterministic on hardware ("sometimes
  splash wins and blocks it without a GO press"). Fixed with a new
  dpx-mode-select.service oneshot that reads /etc/dpx-mode at boot and
  starts exactly that one service, falling back to the splash only if
  nothing's persisted or the target refuses to start. Splash is no
  longer auto-enabled on its own, so there's nothing left to race.

Not live-verified yet -- no device access this pass. Needs a real
boot-cycle test and a forced-permanent-failure test once hardware is
available; also worth rechecking the "GO does nothing" symptom against
this fix, since execute_staged()'s existing mode_dead check already
looks correct on paper and may have been a downstream effect of the same
race rather than its own bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two layers: release-action.yaml's release job now deletes its own run's
CI artifacts right after they land in the GitHub Release (they're
redundant once the .img.gz is a real release asset). New weekly
artifact-sweep.yaml workflow catches everything else -- stray feature-
branch/debug builds that never go through the release job -- by deleting
anything already past its own expires_at, since GitHub's own cleanup can
lag by weeks in practice (confirmed this session: found and manually
cleared 12.1GiB that was up to three weeks overdue).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both image builds ran on ubuntu-latest (x86_64) and emulated the whole
chroot provisioning stage via qemu-aarch64-static -- every apt-get/dpkg
call inside the mounted arm64 image ran through QEMU user-mode
translation, which is commonly 5-10x slower than native for that kind
of CPU-bound work.

Packer's arm-image plugin already supports skipping this: it only adds
the QEMU/binfmt steps when `!ImageArch.IsNative()`
(pkg/builder/builder.go). Set image_arch = "arm64" in the shared
dpx-buttonode.pkr.hcl and switched both builder workflows to
ubuntu-24.04-arm (free, GitHub-hosted, native arm64) -- with host arch
matching image_arch, the plugin skips QEMU entirely on its own, no
custom logic needed. Removed the now-unnecessary qemu-user-static
install steps.

Verified before touching CI: Packer itself installs via HashiCorp's own
apt repo (multi-arch, resolves correctly on arm64), and
packer-plugin-arm-image v0.2.7 publishes a linux_arm64 build.

Isolated on its own branch off main, not mixed into the action-plan
hardware-fix branch -- this is unverified until a real CI run confirms
it, and a broken build here shouldn't be confused with a regression in
the already-tested fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removing it broke the build outright: packer-plugin-arm-image's
Prepare() unconditionally resolves a qemu_binary path via
exec.LookPath, regardless of whether the emulation is actually needed
later -- only its separate Run()-time IsNative() check decides that.
Without the binary present, Prepare() falls back to an "embedded qemu"
feature that's amd64-only and fails outright on arm64:

  embedded qemu is not available - currently, embedded qemu is only
  available for linux amd64. please download qemu-user-static manually

Confirmed via a live test build (33982012810) that failed exactly this
way. Re-added the install -- it's a fast package install, not the slow
part we're trying to avoid, and the plugin's own IsNative() check still
skips actually invoking it during the chroot provisioning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…10)

Root cause found via live SSH investigation on dpx-buttonode-2199
(rockpi-s), confirmed with three isolated tests:

1. udev_retrigger() alone (what switch_mode() actually called) does NOT
   recreate /dev/hidraw* once a libusb consumer (Buttons/Satellite/
   deck-splash) has detached the kernel driver to claim the device --
   verified by running it in isolation and confirming hidraw stayed
   missing.
2. The full unbind/bind (usb_power_cycle(), already built and already
   wired into the manual /power-cycle-deck button) DOES recreate it --
   verified live.
3. With hidraw present, Companion picks up the Stream Deck cleanly at
   startup ("Surface panel ready: streamdeck:..."). Companion's surface
   module only scans once at startup and never retries, so if hidraw is
   missing at that exact moment, Companion silently finds nothing --
   this is why the reported symptom looked intermittent/mode-switch-
   specific rather than a permissions problem.

Fix: switch_mode() now calls the same usb_power_cycle() fallback
/power-cycle-deck already used manually, instead of the gentle-only
udev_retrigger(). usb_power_cycle() already tries the gentle retrigger
first and only escalates to the disruptive unbind/bind if that alone
wasn't enough, so this is a safe drop-in with no added cost in the
common case.

Verified end-to-end on real hardware: forced hidraw missing via
deck-splash, ran the actual --apply-mode companion production path with
the fix applied, hidraw came back automatically, Companion started and
opened the surface panel successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Requested live during #10 testing. Deliberately distinct from
switch_mode(): stop_current_mode() doesn't touch /etc/dpx-mode or any
enable/disable state, it just stops whichever mode service is currently
running and starts dpx-deck-splash. Since dpx-mode-select.service only
runs once at boot (see #11/#12's fix), a manual systemctl stop with
nothing to bring splash back would otherwise leave the deck dark --
this starts it explicitly instead.

Verified live: clicked it via the web UI, confirmed /etc/dpx-mode
unchanged, the mode service stopped, and dpx-deck-splash came up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng to fit

Requested live: the single-line render shrank a 10-char password down to
~9-10px to fit the key width, making similar characters (5 vs S, 0 vs O)
genuinely hard to tell apart on the deck's small screen -- confirmed by
directly causing a transcription error reading one off during this same
session. render_password_key() stacks the password into 4-char lines
instead, so each line only needs to fit 4 characters and the font can
stay much larger (18px vs ~9-10px in this test).

Verified visually: rendered the actual key image on real hardware and
pulled it back to look at it, not just checked it doesn't crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ally autostarts (#12)

Confirmed live on hardware: `systemctl is-enabled dpx-dashboard` says
"enabled", but the actual symlink is
/etc/systemd/system/graphical.target.wants/dpx-dashboard.service --
while this system's default target is multi-user.target
(`systemctl get-default`), forced there deliberately by this same
script (line 43) to work around an unrelated boot-hang bug where a
stray xserver-xorg postinst flips the default target. graphical.target
itself is confirmed `inactive`, i.e. never reached at boot, so
Dashboard's enablement symlink lives somewhere that's never visited --
it stays correctly "enabled" (systemd's own persisted state is fine)
but that enablement never actually triggers a start.

This is why toggling Dashboard on via the web UI worked in the moment
(systemctl enable --now starts it immediately regardless of target) but
didn't survive a reboot, exactly as reported on #12: "restarting does
go right back into Companion... [but Dashboard] doesn't remember the
status."

Fix: WantedBy=graphical.target -> WantedBy=multi-user.target, matching
the target this system actually reaches. Also dropped the now-stale
`graphical.target` from After= (harmless as an ordering constraint on a
never-started target, but confusing to leave in). dpx-dashboard.service
launches X directly via xinit on tty7 itself -- it never depended on
graphical.target's own display-manager machinery to begin with, so this
doesn't change how it starts, only when systemd is willing to pull it
in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only rendered when dashboard_installed() -- same gating as the Devices
tab section. Verified live: correctly shows "inactive" while
dpx-dashboard is crash-looping (activating/auto-restart, not active) on
hardware without a real display attached.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Requested directly: dpx-mode-select.service (from #11/#12's original
fix) always starts the persisted mode at boot with no opt-out. Some
setups want to land on the deck splash every boot instead and switch
modes manually.

Existence-based marker (/var/lib/dpx-mode-autostart-disabled), same
convention as /var/lib/dpx-hostname-set -- absent by default, so a
fresh image behaves exactly as before (checkbox defaults to checked).
dpx-mode-select.sh checks it first and goes straight to splash if
present, skipping the mode-service lookup entirely.

Verified live: toggled off via the web UI, confirmed the marker file
appears/disappears correctly, re-enabled to restore default state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant