Skip to content

WebUI: D&D drop on preset slot races, "alt vs. neu" diff missing until reload #2

@tostmann

Description

@tostmann

What

After dragging a radio station from the sidebar library and dropping it on a preset slot, the dual-row "old (HW) vs. new (Store)" diff view sometimes does not appear. The slot card still shows the previous state. The user has to either fully reload the page or click "Refresh Status" to see the diff.

Reported by @fred_feuerstein in the FHEM forum thread: "Teilweise kommt es vor, dass man nach Drag/Drop bspw. eines Radiosenders das Ergebnis nicht direkt auf der Speichertaste sieht (also diese Ansicht: alt / neu, wo man dann pushen kann). Auch hier muss man manchmal einfach mal einen Reload der Seite oder Refresh-Status machen."

Why this happens

The drop handler onPresetDrop in web-src/index.html:1853-2019 for a radio drop from the sidebar library flows through :1900-1945:

  1. Sends PUT /api/speaker/{id}/preset/{slot} (writes to NVS immediately)
  2. Calls markDirty(dst.dev) (:1933)
  3. Calls loadPresetsFor(dst.dev) (:1937)
  4. Calls kickPoll(dst.dev) (:1938)

loadPresetsFor (:1346-1375) fetches /api/speaker/{id}/presets AND /hardware-presets in parallel, then calls renderSpeakerSlots() + updatePushVisibility() — so the diff render should update.

Root cause: loadPresetsFor is async and silently catches all errors at :1374 (catch(e) { /* ignore */ }). When the parallel Promise.all resolves with {error: ...} for storeR (speaker briefly unreachable / ESP busy with migrate worker → 502), line :1352 reads storeR.presets || [] → empty array → renders all 6 slots as EMPTY. The user sees a momentarily wiped card, then the next 10s polling tick recovers. The catch eats the failure silently, so no toast either.

Proposed fixes

  1. loadPresetsFor (:1346-1375): detect {error: ...} shape in storeR/hwR responses and bail without overwriting STATE.presetsByDev instead of normalizing to empty arrays. Retry with backoff.
  2. onPresetDrop (:1933-1938): optimistically patch STATE.presetsByDev[dst.dev].store[dst.slot-1] from the local putBody before the GET completes, so the diff renders immediately regardless of network race. loadPresetsFor then reconciles asynchronously.
  3. :1374: replace silent catch(e) { /* ignore */ } with at minimum console.warn and a retry-once.

Acceptance

  • 10 consecutive D&D drops on the same slot show the diff immediately every time
  • An intentionally-induced backend 502 during the drop does not blank the slot card

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions