nuc: media over NFS from nas, on-demand role, i915 and input traps

nuc keeps only what needs its iGPU. blocky, privoxy and transmission-bt
moved to nas so the box can be powered off when not watching Jellyfin or
using the Spotify kiosk.

- /srv/media is now an NFSv4 mount from nas; jellyfin-server reads it
  with shift=false (idmapped mounts are unsupported on NFS, as the
  container doc already noted for CIFS) and readonly=true
- replication to nas is a systemd timer with Persistent=true, not cron —
  an on-demand host misses its 03:30 window and cron cannot catch up

Two failures documented in full, both diagnosed from the wrong layer
first:

- booting with the TV connected and powered on kills the i915 probe
  (drm_WARN_ON in intel_modeset_setup_hw_state), so /dev/dri never
  appears, snd_hda_intel deferred-probes forever holding the PCI device
  lock, and incusd blocks in sriov_numvfs_show — no container starts at
  all, including LAN DNS. Identical on 6.12.107 and 6.12.105.
- the kiosk input gid mismatch is real but was NOT the cause of the
  2026-08-30 outage (flat K400 batteries were); seatd opens input devices
  as root, so kiosk group membership is not on that path. Records the
  one-line raw capture that settles hardware-vs-software immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Julien Lutran
2026-08-30 23:53:30 +02:00
co-authored by Claude Opus 5
parent 66c7bca28f
commit 203728674f
4 changed files with 225 additions and 21 deletions
+145 -1
View File
@@ -116,6 +116,15 @@ EOF'
# --- Kiosk user + seat management ----------------------------------------------
incus exec "$CNAME" -- bash -c 'id kiosk >/dev/null 2>&1 || useradd -m -G video,render,input,audio kiosk'
# ⚠️ Group names are NOT enough. The host (Debian) and the container
# (Ubuntu) allocate dynamic system gids independently, so the container's
# `input` group does not necessarily have the same gid as the group that
# owns /dev/input/* on the host. Bind the kiosk user to the *numeric*
# host gid, whatever it is called inside:
HOST_INPUT_GID="$(stat -c %g /dev/input/event0)"
incus exec "$CNAME" -- usermod -aG "$HOST_INPUT_GID" kiosk
incus exec "$CNAME" -- id kiosk # must list $HOST_INPUT_GID
# In a container seatd must NOT bind the seat to a VT (there is no usable VT;
# it would try to open the host's active tty and hang the compositor forever).
incus exec "$CNAME" -- mkdir -p /etc/systemd/system/seatd.service.d
@@ -245,10 +254,17 @@ it works regardless of which USB port the dongle lands on.
`/etc/udev/rules.d/99-jellyfin-kiosk-recover.rules`:
```
# Logitech Unifying receiver (re)plugged -> recover the jellyfin-client kiosk
# A Logitech receiver was (re)plugged -> recover the jellyfin-client kiosk.
# c52b = Unifying receiver (K400); c539 = Lightspeed receiver (G603).
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="046d", ATTR{idProduct}=="c52b", RUN+="/usr/bin/systemctl --no-block start jellyfin-kiosk-recover.service"
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="046d", ATTR{idProduct}=="c539", RUN+="/usr/bin/systemctl --no-block start jellyfin-kiosk-recover.service"
```
⚠️ The match is per product ID, so **a receiver not listed here will not
auto-recover** — the kiosk must be restarted by hand after plugging it in
(`incus exec jellyfin-client -- systemctl restart jellyfin-kiosk`). Add
the new id here when introducing different input hardware.
`/etc/systemd/system/jellyfin-kiosk-recover.service` — oneshot, so a burst of
udev events during one plug merges into a single restart (natural debounce):
@@ -285,6 +301,134 @@ udevadm control --reload-rules && systemctl daemon-reload
journalctl -t jellyfin-kiosk-recover -f
```
## ⚠️ Never boot nuc with the display active (2026-08-30)
**Symptom:** after a reboot, *no* Incus container starts. `incus list`
answers but `incus start <anything>` hangs forever, `systemctl status
incus` sits in `activating (start-post)`, and LAN DNS is down because
blocky never came up. Nothing in the incus logs explains it.
**Cause — nothing to do with incus.** If the TV/projector is connected
**and powered on** when nuc boots, firmware hands i915 an already-lit
pipe. The driver's state readback then trips a series of warnings and
the probe never completes:
```
drm_WARN_ON(!pll_active) intel_ddi.c:4019 intel_ddi_get_clock
drm_WARN_ON(p0 == 0 || p1 == 0 || p2 == 0) intel_dpll_mgr.c:2878
drm_WARN_ON(pixel_rate == 0) skl_watermark.c:1729
```
(all inside `intel_modeset_setup_hw_state``intel_display_driver_probe_nogem`)
The cascade:
1. i915 probe dies → **`/dev/dri` never appears** (no GPU at all)
2. `snd_hda_intel` waits forever for i915's audio component →
permanent **deferred probe** holding the PCI device lock on `0000:00:1f.3`
3. incusd reads that device's `sriov_numvfs` while enumerating
resources → blocks in **D state** → the daemon never signals ready,
so nothing autostarts and every `incus start` hangs
**Reproduced identically on 6.12.107 and 6.12.105** — it is the display
path, not a kernel regression. Do not waste time pinning kernels.
**Diagnosis, in order:**
```sh
ls /dev/dri/ # empty = i915 probe failed
cat /sys/kernel/debug/devices_deferred # snd_hda_intel entry = the deadlock
ps -eLo pid,tid,stat,wchan:26,comm | awk '$3 ~ /D/' # incusd in sriov_numvfs_show
dmesg -T | grep -E 'drm_WARN_ON|deferred probe pending'
```
**Fix:** disconnect HDMI (or power the display fully off — not standby),
reboot, then **hotplug the cable back in**. Connecting after boot goes
through normal connector detection instead of firmware state readback
and works fine.
In-place recovery is *not* possible: `modprobe -r i915` fails (module in
use by the wedged probe) and `modprobe i915` times out. A reboot is the
only way out.
**After hotplugging, restart the kiosk**`cage` started with zero
outputs and will not pick the display up on its own:
```sh
incus exec jellyfin-client -- systemctl restart jellyfin-kiosk
cat /sys/class/drm/card0-HDMI-A-2/status # expect: connected
```
Note the HDA controller this wedges is only used for **HDMI audio**,
which this setup does not use — audio goes to the Pioneer USB DAC via
`/etc/asound.conf`. It is pure collateral damage, but it takes the whole
host down with it.
## Input gid mismatch — latent, fix it anyway (2026-08-30)
> ⚠️ **This was not the cause of the 2026-08-30 outage.** That turned out
> to be a flat/switched-off K400 — a G603 on the same port and the same
> `event0` worked immediately. The mismatch below is real and worth
> correcting, but with `LIBSEAT_BACKEND=seatd` it is **seatd (running as
> root) that opens input devices** and passes the fd to cage, so the
> kiosk user's group membership is not on the critical path for input.
> Fix it for the direct-open fallback path, not as a debugging lead.
>
> **Before suspecting software, prove the hardware emits anything:**
> ```sh
> timeout 60 cat /dev/input/eventN | wc -c # press keys; 0 bytes = nothing reached the kernel
> ```
> That one check would have saved an hour.
>
> **Dead K400 batteries are invisible from the host.** This K400 exposes
> no `hidpp_battery_*` node under `/sys/class/power_supply/`, so charge
> cannot be read. Worse, the receiver still lists the keyboard as a paired
> peer (`0003:046D:4024.*` under the `C52B` receiver) whether or not it is
> awake, and `/dev/input/event0` plus a `Logitech K400` entry in
> `/proc/bus/input/devices` are present either way — so every software
> check looks perfectly healthy. **Zero bytes from the raw capture is the
> only signal.** Swapping in a different receiver on the same port is the
> quickest A/B confirmation.
**Symptom (if it ever does bite):** kiosk renders but input does nothing,
with no errors — `WLR_LIBINPUT_NO_DEVICES=1` keeps cage alive rather than
failing loudly.
**Cause:** `/dev/input/*` is `crw-rw---- root:<host input gid>`. The
install script adds `kiosk` to the group *named* `input` inside the
container, but Debian (host) and Ubuntu (container) allocate dynamic
system gids independently:
```
host /dev/input/event0 gid 996 -> "input"
container "input" group gid 995 <- kiosk was here
container gid 996 -> "systemd-timesync"
```
So the kiosk user was in the wrong group and could not open any input
device. The udev side was fine — check it first to rule it out:
`cat /run/udev/data/c13:64` should show `E:ID_INPUT=1` etc.
**Diagnose:**
```sh
stat -c '%n %a %u:%g' /dev/input/event0 # host gid
incus exec jellyfin-client -- id kiosk # does it include that gid?
# open() test — do NOT use `head`/`cat`, reading an event device blocks
# with no pending events and looks like a permission failure:
incus exec jellyfin-client -- su -s /bin/bash kiosk -c 'exec 3< /dev/input/event0 && echo OPEN_OK'
```
**Fix** (persists in the container's `/etc/group`):
```sh
HOST_INPUT_GID="$(stat -c %g /dev/input/event0)"
incus exec jellyfin-client -- usermod -aG "$HOST_INPUT_GID" kiosk
incus exec jellyfin-client -- systemctl restart jellyfin-kiosk
```
Re-check this after any host reinstall — the host's `input` gid is
dynamically allocated and can come back different.
## Troubleshooting
```sh