Add interactive mutation UI to dashboard

Dashboard now supports all existing mutation APIs: feedback
(thumbs up/down, retract), profile CRUD, speaker mapping,
playlist generation, track discovery, taste rebuild, and
requeue failed embeddings. All controls use vanilla JS fetch
with toast notifications.

New endpoint: POST /api/admin/requeue-failed resets failed
embedding tracks back to pending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 08:08:23 -06:00
parent 551b4c6ff9
commit 3e66f31df9
3 changed files with 527 additions and 24 deletions

View File

@@ -87,6 +87,7 @@ async def status_page(
recent_rows = (await session.execute(listens_query)).all()
recent_listens = [
{
"track_id": track.id,
"title": track.title,
"artist": track.artist,
"speaker": event.speaker_name or "Unknown",
@@ -119,6 +120,7 @@ async def status_page(
profiles = [
{
"id": profile.id,
"raw_name": profile.name,
"name": profile.display_name or profile.name,
"event_count": event_count,
"track_count": track_count,
@@ -193,6 +195,7 @@ async def status_page(
feedback_rows = (await session.execute(fb_recent_query)).all()
recent_feedback = [
{
"id": event.id,
"signal": event.signal,
"signal_weight": event.signal_weight,
"title": track.title,
@@ -233,6 +236,7 @@ async def status_page(
for event, track in influence_rows:
if track.id not in tracks_map:
tracks_map[track.id] = {
"track_id": track.id,
"title": track.title,
"artist": track.artist,
"vibes": [],
@@ -268,6 +272,23 @@ async def status_page(
# Profile names for selector (use raw Profile.name, not display_name)
all_profile_names = [profile.name for profile, *_ in profile_rows]
# Speaker entities for dropdowns
speaker_entities = [
("Living Room", "media_player.living_room_speaker_2"),
("Dining Room", "media_player.dining_room_speaker_2"),
("Basement", "media_player.basement_mini_2"),
("Kitchen", "media_player.kitchen_stereo_2"),
("Study", "media_player.study_speaker_2"),
("Butler's Pantry", "media_player.butlers_pantry_speaker_2"),
("Master Bathroom", "media_player.master_bathroom_speaker_2"),
("Kids Room", "media_player.kids_room_speaker_2"),
("Guest Bedroom", "media_player.guest_bedroom_speaker_2_2"),
("Garage", "media_player.garage_wifi_2"),
("Whole House", "media_player.whole_house_2"),
("Downstairs", "media_player.downstairs_2"),
("Upstairs", "media_player.upstairs_2"),
]
template = _jinja_env.get_template("status.html")
html = template.render(
data=data,
@@ -280,6 +301,7 @@ async def status_page(
vibe_influence=vibe_influence,
selected_profile=selected_profile.name if selected_profile else None,
all_profile_names=all_profile_names,
speaker_entities=speaker_entities,
now=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
)
return HTMLResponse(html)