Files
Bose-SoundTouch/pkg
Tobias GesellchenandClaude Opus 4.7 49904635f2 fix(marge): preserve CreatedOn + IPAddress across the device rename PUT
The PUT handler shipped in 5f31616 + the routing fix in 66b83b6 made
the rename PUT reach AfterTouch and return 200. But the response and
the on-disk record both drifted away from real Bose's parity on every
rename: CreatedOn was rewritten to now() (so the "first paired in
2017" semantics evaporated on the second rename) and IPAddress
landed empty (because the speaker's PUT body doesn't carry it and
the marge handler had no preservation path).

Pre-shutdown capture at
data/parity_mismatches/1771797308__streaming_account_3230304_device_A81B6A536A98.json
shows real Bose's 200 OK shape: createdOn pinned to the original
pairing timestamp (2017-02-07), ipaddress populated, only updatedOn
and name change across renames. Aligning with that.

Three small persistence additions:

  - models.ServiceDeviceInfo grows CreatedOn + UpdatedOn (ISO8601
    strings, omitempty so existing JSON consumers don't break).
  - datastore.SaveDeviceInfo persists them inside the DeviceInfo.xml
    payload as <createdOn> / <updatedOn> alongside the other fields.
  - mergeWithExistingDeviceInfo preserves CreatedOn unconditionally
    (it's the "first-paired" timestamp and never re-derived from
    inbound data) and preserves UpdatedOn only if the caller didn't
    set a fresh one.

marge.AddDeviceToAccount becomes precedence-aware:

  - Reads the existing record once at the top.
  - CreatedOn: preserved from existing if present, else now() for
    first registration.
  - IPAddress: preserves what's in the existing record; falls back
    to r.RemoteAddr's host portion only when no prior IP exists.
    Lets first-time PUTs seed an IP from the inbound connection
    without later renames clobbering a known-good value.
  - UpdatedOn: always now().
  - Response XML now re-reads the persisted record so the
    response body matches what's on disk — no parallel hand-built
    XML drifting from the merge result.

Function signature gained a remoteAddr parameter. Both callers
(HandleMargeAddDevice and HandleMargeUpdateDevice) pass r.RemoteAddr.

Test coverage:

  - TestIssue285_RenamePutAcceptedAndPersisted seeds the datastore
    with a 2017 CreatedOn and a known IP, then PUTs the rename;
    asserts both survive on disk AND in the response body, and
    that UpdatedOn refreshes. The same pre-shutdown capture cited
    above is the parity reference.

  - TestIssue285_NewDeviceGetsRemoteAddrAndFreshTimestamps (new)
    covers the no-prior-record path: first-time PUT against an
    unknown device produces CreatedOn = now() and IPAddress
    pulled from the inbound TCP connection. Pins the fallback
    behaviour so it can't quietly stop seeding new devices.

Authorization is still not enforced — the speaker has no Bose token
to send post-shutdown, and we don't (yet) have a token-authority
story of our own. Adding a warn-only auth check is a deferred
follow-up (see NEXT.md). Real Bose returned 401 for this PUT in the
2026-05-15 capture; we knowingly accept anything.

Refs #285.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:04:41 +02:00
..