docs: bring READMEs, ARCHITECTURE, and wiki up to date - #61
Conversation
A batch of UX fixes plus persisted like/dislike state.
Dark mode
- Scope the global element-level `input/textarea` rules with
`:not([class*="Mui"])` so they stop painting MUI inner inputs a
different shade than their outlined container (`body.dark-mode input
{ background:#333 }` was leaking into every field app-wide).
Mood feedback widget
- The X now dismisses the "Was that right?" widget outright (renders
null) instead of falling into the "Thanks…" terminal state -- skipping
is not feedback. Re-arms on the next detection.
Profile page
- "Clear all" no longer overlaps the section title/hint on mobile: the
action is a normal right-aligned row on phones, absolute top-right only
on tablet+.
- Rename stat "Moods logged" -> "Mood Detections".
- Replace the confusing "Stored as plain strings, freshest at the bottom"
hint with "Songs you've played or opened, newest first."
- "Tracks you've opened" now renders the same rich card as saved
recommendations (cover art, preview player, Deezer link). Listening
history is persisted as full track dicts (services/listening.js +
ListField() so dicts are accepted); legacy "Name - Artist" string rows
are normalised on read.
Home page
- Hero stat bubbles (Moods / Saved / Listened) show a spinner until the
profile fetch settles, instead of flashing an incorrect 0.
Results page
- Filters/search start disabled during `initializing` (were briefly
enabled before the first load kicked in).
- Loading body is now a centered spinner + message instead of
left-squished skeletons.
Like / dislike (RL)
- Verified: like/unlike already updates the Thompson-sampling bandit
posterior (when the full track dict is sent, which it is) and re-ranks
once a user has >=20 events -- no change needed there.
- New read path so button state persists across reloads: backend
`query_track_feedback()` (latest explicit vote per track, open_deezer
excluded) + GET `/api/feedback/tracks/?ids=...`; frontend
`getTrackFeedbackState()` hydrates ResultsPage, seeding each row's
thumb state and keeping it across re-sorts/refetches.
Tests: backend query + endpoint (auth, ids parsing, failure -> {});
frontend tests updated for the dismiss + extra feedback GET; snapshots
refreshed. 60 FE tests / 10 snapshots, 236 BE tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Toggling a like/dislike off now records and persists the un-vote, so the button state and the bandit posterior both stay in sync after a reload. - New "clear" track signal (feedback_store.TRACK_SIGNALS). - query_track_feedback now considers like/unlike/clear and returns the latest; a trailing "clear" means no active vote, so the track is omitted -> the button rehydrates empty. - The feedback endpoint's clear branch looks up the vote being retracted, persists the clear event, and reverses that vote's contribution via the new bandit.revert_posterior (subtracts exactly what update_posterior added, clamped at the prior floor and events >= 0). - Frontend: TRACK_SIGNALS gains "clear"; ResultsPage sends signal:"clear" when a vote is toggled off (and clears local + voteMap). Tests: bandit revert (round-trip, clamp, open_deezer non-revertable), query omits trailing clear, and the endpoint clear path reverts the posterior to zero events while persisting the clear event. 241 backend / 60 frontend tests pass; eslint + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…current vote Switching a track directly from like to dislike (without toggling off first) previously applied the dislike but never reverted the like, so the bandit posterior double-counted both signals on the same track. Reconcile every like/unlike/clear against the prior vote: look it up before recording the new event, revert the prior contribution, then apply the new one. A track's current vote is now the only thing that contributes -- switching like<->unlike nets a single vote, clearing nets none, and re-sending the same vote is idempotent. open_deezer stays a purely additive implicit signal (never a vote, never reverted). Test: like -> unlike switch reverts the like (alpha back to prior) and applies the unlike (beta bumped) with the event count staying at one. 242 backend tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two correctness gaps from review, both on the un-vote / vote-switch path.
1. context-emotion drift: reverting a vote re-featurized the track with the
CURRENT request's context emotion, so retracting a like cast under one
mood (e.g. joy) while the track was surfaced under another (e.g. sad)
subtracted from the wrong emotion axis -- the original contribution was
never fully undone. Now the exact feature vector applied for a like/unlike
is stored on the event and the revert subtracts that stored vector, so it
cancels precisely regardless of track/context drift at revert time.
2. non-deterministic "latest vote": the time-series ts is millisecond-
precision, so two rapid taps on the same track could share a ts and make
$last ambiguous. Each event now carries a high-resolution `seq`
(time.time_ns()) and the aggregations sort by {ts, seq}.
New `feedback_store.get_active_vote()` returns the current vote + its stored
feature vector (latest of like/unlike/clear by ts,seq; trailing clear ->
None). The endpoint reconciles like/unlike/clear against it. Feature-based
`_apply_posterior` / `_revert_posterior` replace the re-featurizing helpers.
Tests: get_active_vote (mapping / trailing-clear / empty + seq tiebreaker);
switch and clear now assert the revert uses the stored vector. 245 backend
tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh the docs to reflect the current app + RL state. - Auth: sign in with username OR email (case-insensitive email fallback); cold-start transient DB errors are retried and return 503 "waking up" (client auto-retries) instead of a misleading 401. - Track feedback signals are like / unlike / open_deezer / clear. Document set-vote semantics (a track's current vote is its only contribution; like<->unlike reverts then applies, clear nets zero, re-send idempotent), the exact-stored-feature-vector revert (clamped at the Beta prior, events >= 0), and the seq tiebreaker for deterministic latest-vote. - New GET /api/feedback/tracks/ read-back endpoint that restores like/ dislike button state across reloads / re-sorts / refetches. - listening_history now stores full track dicts (legacy "Name - Artist" strings still read); profile renders rich cards. - Frontend README: persisted/un-votable feedback, rich listening cards, instant dark/light theme, username-or-email login. - Fix stale password-hash references (Bcrypt -> PBKDF2) in ARCHITECTURE diagrams to match the code. Files: README.md, ARCHITECTURE.md, backend/README.md, frontend/README.md, modal_inference/README.md, index.html. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
✅ Deploy Preview for moodify-emotion-music-app ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Code Review
This pull request introduces 'set-vote' feedback semantics (like, unlike, and clear/un-vote) to the reinforcement learning loop, allowing users to toggle or switch votes with precise posterior reconciliation. It adds a new endpoint GET /api/feedback/tracks/ to persist and restore vote states across reloads, updates the user profile to store rich listening history cards, and improves dark-mode styling for Material-UI inputs. The code review highlights two critical performance improvements: optimizing sequential database operations in feedback_views.py into a single roundtrip to prevent race conditions, and moving track_id into the MongoDB time-series meta sub-document in feedback_store.py to enable efficient compound indexing.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if prior_signal in ("like", "unlike") and prior_signal != signal: | ||
| _revert_posterior(username, prior.get("features"), prior_signal) | ||
| if signal in ("like", "unlike") and signal != prior_signal: | ||
| _apply_posterior(username, new_features, signal) |
There was a problem hiding this comment.
When a user switches their vote (e.g., from like to unlike), this block performs two separate database reads and two separate database saves sequentially (one inside _revert_posterior and one inside _apply_posterior). This results in redundant database roundtrips and increases the risk of race conditions (lost updates) under concurrent requests.\n\nWe can optimize this into a single database roundtrip by loading the UserProfile once, applying both the revert and update operations to the taste_profile dictionary in memory, and then saving the document once.
revert_active = prior_signal in ("like", "unlike") and prior_signal != signal
apply_active = signal in ("like", "unlike") and signal != prior_signal
if revert_active or apply_active:
try:
profile = UserProfile.objects(username=username).first()
if profile is not None:
taste_profile = profile.taste_profile or {}
if revert_active and prior.get("features"):
taste_profile = bandit.revert_posterior(taste_profile, prior.get("features"), prior_signal)
if apply_active and new_features:
taste_profile = bandit.update_posterior(taste_profile, new_features, signal)
profile.taste_profile = taste_profile
profile.save()
except Exception: # noqa: BLE001
logger.warning("taste_profile update failed for user=%s (silently skipping)", username)| coll.insert_one({ | ||
| "ts": datetime.now(timezone.utc), | ||
| "seq": time.time_ns(), | ||
| "meta": { | ||
| "username": username, | ||
| "signal": signal, | ||
| "context_emotion": context_emotion, | ||
| }, | ||
| "track_id": track_id, | ||
| "features": list(features) if features is not None else None, | ||
| }) |
There was a problem hiding this comment.
In MongoDB time-series collections, fields outside the metaField (like track_id and features) are treated as measurement fields. Secondary indexes on measurement fields are either unsupported (MongoDB < 6.0) or have significant performance and indexing restrictions.\n\nSince track_id is a stable identifier used as a primary query filter in get_active_vote and query_track_feedback, it should be placed inside the meta sub-document (e.g., meta.track_id). This allows creating a highly efficient compound index on [("meta.username", 1), ("meta.track_id", 1), ("ts", -1)] across all MongoDB versions, preventing costly collection scans as the feedback log grows.
| coll.insert_one({ | |
| "ts": datetime.now(timezone.utc), | |
| "seq": time.time_ns(), | |
| "meta": { | |
| "username": username, | |
| "signal": signal, | |
| "context_emotion": context_emotion, | |
| }, | |
| "track_id": track_id, | |
| "features": list(features) if features is not None else None, | |
| }) | |
| coll.insert_one({ | |
| "ts": datetime.now(timezone.utc), | |
| "seq": time.time_ns(), | |
| "meta": { | |
| "username": username, | |
| "signal": signal, | |
| "context_emotion": context_emotion, | |
| "track_id": track_id, | |
| }, | |
| "features": list(features) if features is not None else None, | |
| }) |
Refreshes the core docs to match the current app + RL state. All edits were verified against the code (route greps, feedback_views/store/bandit, users/views, ResultsPage/Profile/feedback service).
What changed
Auth
RL / track feedback
like / unlike / open_deezer / clear.clear(un-vote) nets zero, re-send is idempotent.events ≥ 0; aseqtiebreaker makes "latest vote" deterministic.GET /api/feedback/tracks/read-back endpoint that restores 👍/👎 button state across reloads / re-sorts / refetches.Data / UX
listening_historynow stores full track dicts (legacy"Name - Artist"strings still read) → rich profile cards.Correctness fix
Files
README.md,ARCHITECTURE.md,backend/README.md,frontend/README.md,modal_inference/README.md,index.htmlNotes
<details>balance verified;frontend/README.mdrun through the project prettier (root docs are outside the format script's scope by repo convention)./api/feedback/tracks/); merge after/with feat(frontend): profile/results UX overhaul + persisted track feedback #60.🤖 Generated with Claude Code