From 540862fcadc24b2c3f848398a1900f392ac34e43 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Mon, 2 Mar 2026 09:25:29 -0600 Subject: [PATCH] Add profile-aware feedback with vibe context Feedback buttons now target the listener's profile instead of the dashboard filter profile. Adds a persistent vibe context input that replaces the hardcoded "general" vibe. Shows listener profile badge and track ID on each listen item. Adds manual feedback form for submitting feedback on any track by ID. Co-Authored-By: Claude Opus 4.6 --- src/haunt_fm/api/status_page.py | 8 ++++ src/haunt_fm/templates/status.html | 74 ++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/haunt_fm/api/status_page.py b/src/haunt_fm/api/status_page.py index 3b67022..e513f4d 100644 --- a/src/haunt_fm/api/status_page.py +++ b/src/haunt_fm/api/status_page.py @@ -92,6 +92,7 @@ async def status_page( "artist": track.artist, "speaker": event.speaker_name or "Unknown", "listened_at": event.listened_at, + "profile_id": event.profile_id, } for event, track in recent_rows ] @@ -111,6 +112,13 @@ async def status_page( ) ).all() + # Map profile_id to name for recent listens + profile_name_by_id: dict[int, str] = { + profile.id: profile.name for profile, *_ in profile_rows + } + for listen in recent_listens: + listen["profile_name"] = profile_name_by_id.get(listen.pop("profile_id"), "default") + # Speaker mappings keyed by profile_id mapping_rows = (await session.execute(select(SpeakerProfileMapping))).scalars().all() speakers_by_profile: dict[int, list[str]] = {} diff --git a/src/haunt_fm/templates/status.html b/src/haunt_fm/templates/status.html index 11dc1c3..72a1a97 100644 --- a/src/haunt_fm/templates/status.html +++ b/src/haunt_fm/templates/status.html @@ -283,6 +283,17 @@ {% endif %} + +
+
+ Vibe context + +
+
+ Feedback is contextual — describes what mood this feedback applies to +
+
+

Recent Listens

@@ -292,11 +303,11 @@
{{ listen.title }} — {{ listen.artist }} -
{{ listen.speaker }} · {{ listen.listened_at | timeago }}
+
{{ listen.speaker }} · {{ listen.profile_name }} · #{{ listen.track_id }} · {{ listen.listened_at | timeago }}
{% endfor %} @@ -410,8 +421,8 @@ {% endfor %} @@ -424,6 +435,20 @@

Actions

+
+ Feedback + + + + +
Discover @@ -671,20 +696,51 @@ } // --- Feedback --- - async function submitFeedback(btn, trackId, signal) { + function getVibeContext() { + return document.getElementById('vibe-context').value.trim(); + } + + async function submitFeedback(btn, trackId, signal, profileName) { + const vibe = getVibeContext(); + if (!vibe) { + showToast('Set a vibe context first', 'error'); + return; + } + const profile = profileName || CURRENT_PROFILE; btn.disabled = true; - const res = await apiCall('POST', '/api/profiles/' + CURRENT_PROFILE + '/feedback', { - track_id: trackId, signal: signal, vibe: 'general' + const res = await apiCall('POST', '/api/profiles/' + profile + '/feedback', { + track_id: trackId, signal: signal, vibe: vibe }); if (res.ok) { btn.classList.add('sent'); - showToast('Feedback recorded: ' + signal, 'success'); + showToast('Feedback: ' + signal + ' on #' + trackId + ' → ' + profile + ' (' + vibe + ')', 'success'); } else { btn.disabled = false; showToast('Feedback failed: ' + res.error, 'error'); } } + async function submitManualFeedback() { + const trackId = parseInt(document.getElementById('manual-fb-track').value); + const signal = document.getElementById('manual-fb-signal').value; + const profile = document.getElementById('manual-fb-profile').value; + const vibe = getVibeContext(); + if (!trackId) { showToast('Enter a track ID', 'error'); return; } + if (!vibe) { showToast('Set a vibe context first', 'error'); return; } + const btn = event.target; + setLoading(btn, true); + const res = await apiCall('POST', '/api/profiles/' + profile + '/feedback', { + track_id: trackId, signal: signal, vibe: vibe + }); + setLoading(btn, false); + if (res.ok) { + showToast('Feedback: ' + signal + ' on #' + trackId + ' → ' + profile + ' (' + vibe + ')', 'success'); + document.getElementById('manual-fb-track').value = ''; + } else { + showToast('Feedback failed: ' + res.error, 'error'); + } + } + async function retractFeedback(btn, eventId) { btn.disabled = true; const res = await apiCall('DELETE', '/api/feedback/' + eventId);