Skip to content

feat(singers): device-session provenance + smarter Merge modal (v0.91.0) - #204

Merged
beveradb merged 4 commits into
mainfrom
feat/singer-session-provenance
Aug 14, 2026
Merged

feat(singers): device-session provenance + smarter Merge modal (v0.91.0)#204
beveradb merged 4 commits into
mainfrom
feat/singer-session-provenance

Conversation

@beveradb

Copy link
Copy Markdown
Contributor

Why

Investigating the live "two Chailas" incident: the KJ Singers list showed the same person twice. One entry was her own self-registered singer ("Chaila R", submitted from her phone); the other was a duet-partner label ("Chaila") that another singer typed into their own submission. Nothing in the UI distinguished a real device session from a duet label or a KJ-hand-added entry, and Merge was a bare name dropdown that didn't say which singer was kept or what history moved.

What

Backend

  • Capture the submitting device's User-Agent on /sing/submit and /sing/requests/<id>/change (new additive sing_requests.user_agent column; auto-migrates on boot, safe to roll back).
  • New ua_parse.py: best-effort UA → friendly browser/OS/device (Android UAs expose the model, e.g. SM-S911B; iOS only "iPhone").
  • SingStore.get_requests_for_entries(): night-scoped linked-request lookup (fails closed, mirrors the SMS phone lookup).
  • routes._add_singer_session_info(): attaches a session block to each singer classifying origin as singer_ui (own device → has_device), duet_partner, or kj_added. Wired into GET /rotation and every singer-action response.

Frontend

  • 📱 icon only on real device-session singers → click opens a device-details popup (parsed device, phone on file, songs submitted, sources, first submitted, raw UA).
  • Merge is now a modal: searchable/scrollable singer list (device-linked first, badged), then a confirm step spelling out KEEP vs REMOVE, exact combined sung/queued totals (deduped by entry id), a note that a phone-linked keeper inherits the other's history, and a real-device warning + Swap that defaults the keeper to the device-linked singer.

Testing

  • Unit: ua_parse (incl. Edge-for-Android, embedded-WebView, iOS/Android/desktop); _add_singer_session_info classification reproducing the exact Chaila scenario; UA persisted through /submit.
  • E2E: drives the full Merge modal (search → confirm → Swap → real merge outcome).
  • Verified visually in a real browser (device icon, device modal, merge confirm).

Deploy notes

  • Backend change → requires systemctl restart kj-controller (interrupts playback — deploy between songs).
  • Device capture is forward-looking (existing singers have no UA); the singer_ui/duet_partner/kj_added classification and smarter Merge work retroactively.

Local CodeRabbit review completed and all findings addressed.

@coderabbitai ignore

🤖 Generated with Claude Code

beveradb and others added 4 commits August 13, 2026 23:03
Motivated by the "two Chailas" incident: the KJ Singers list showed the same
person twice — one was her own self-registered singer ("Chaila R", from her
phone) and the other a duet-partner label ("Chaila") typed into someone else's
submission. Nothing distinguished a real device session from a duet label or a
KJ-hand-added entry, and Merge didn't say which singer was kept.

Backend:
- Capture the submitting device's User-Agent on /sing/submit and /change via a
  new additive sing_requests.user_agent column.
- New ua_parse.py: best-effort UA -> friendly browser/OS/device (Android model).
- SingStore.get_requests_for_entries(): night-scoped linked-request lookup.
- routes._add_singer_session_info(): attaches a `session` block to each singer
  (origin singer_ui / duet_partner / kj_added; has_device; device/phone/count),
  wired into GET /rotation and every singer action response.

Frontend:
- 📱 icon only on real device-session singers; click opens a device-details
  popup (parsed device, phone, first submitted, count, sources, raw UA).
- Merge modal replaces the inline dropdown: searchable singer list (device-linked
  first), then a confirm step spelling out KEEP vs REMOVE, combined totals, a note
  that a phone-linked keeper inherits history, and a real-device warning + Swap
  that defaults the keeper to the device-linked singer.

Tests: ua_parse unit tests; session classification reproducing the incident;
UA persisted through /submit; e2e drive of the Merge modal. Docs + version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Merge confirm dialog summed the two singers' sung/queued counts, which
over-counts an entry both were credited on (e.g. a shared duet that collapses to
one row after merge dedup). Compute from the deduped union of entries so the
displayed post-merge totals match what the backend actually produces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ua_parse: recognise EdgA/ (Edge on Android) before Chrome/; add a unit test.
- merge e2e: drop the stale Swap comment, actually exercise Swap, and assert the
  real merge outcome (removed singer's row detaches) instead of the modal merely
  hiding (which happens regardless of merge success).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bare Safari/ compatibility token appears in embedded WebKit clients (in-app
WebViews) that aren't Safari. Require the documented Version/… Safari/ sequence
so those aren't mislabelled. Add a test for a WebView UA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@beveradb
beveradb merged commit 4ce31b2 into main Aug 14, 2026
2 checks passed
@beveradb
beveradb deleted the feat/singer-session-provenance branch August 14, 2026 03:21
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