Skip to content

docs: bring READMEs, ARCHITECTURE, and wiki up to date - #61

Merged
hoangsonww merged 5 commits into
masterfrom
docs/update-latest-state
Jun 28, 2026
Merged

docs: bring READMEs, ARCHITECTURE, and wiki up to date#61
hoangsonww merged 5 commits into
masterfrom
docs/update-latest-state

Conversation

@hoangsonww

Copy link
Copy Markdown
Owner

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

  • Sign in with username or email (case-insensitive email fallback).
  • Cold-start transient DB errors are retried → 503 "waking up" (client auto-retries) instead of a misleading 401.

RL / track feedback

  • Signals documented as like / unlike / open_deezer / clear.
  • Set-vote semantics: a track's current vote is its only contribution — like↔unlike reverts-then-applies (no double-count), clear (un-vote) nets zero, re-send is idempotent.
  • Revert subtracts the exact stored feature vector, clamped at the Beta(1,1) prior with events ≥ 0; a seq tiebreaker makes "latest vote" deterministic.
  • New GET /api/feedback/tracks/ read-back endpoint that restores 👍/👎 button state across reloads / re-sorts / refetches.

Data / UX

  • listening_history now stores full track dicts (legacy "Name - Artist" strings still read) → rich profile cards.
  • Frontend README: persisted/un-votable feedback, rich listening cards, instant dark/light theme, username-or-email login.

Correctness fix

  • ARCHITECTURE diagrams said Bcrypt; code uses Django's default PBKDF2 — corrected both references.

Files

README.md, ARCHITECTURE.md, backend/README.md, frontend/README.md, modal_inference/README.md, index.html

Notes

🤖 Generated with Claude Code

hoangsonww and others added 5 commits June 28, 2026 16:48
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>
@hoangsonww hoangsonww self-assigned this Jun 28, 2026
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
moodify-emotion-music-app Ignored Ignored Jun 28, 2026 12:04pm

@netlify

netlify Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploy Preview for moodify-emotion-music-app ready!

Name Link
🔨 Latest commit 5bee5f7
🔍 Latest deploy log https://app.netlify.com/projects/moodify-emotion-music-app/deploys/6a410de3447a0d000821e366
😎 Deploy Preview https://deploy-preview-61--moodify-emotion-music-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +373 to +376
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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)

Comment on lines 192 to 202
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,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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,
})

@hoangsonww
hoangsonww merged commit de8337a into master Jun 28, 2026
15 checks passed
@hoangsonww hoangsonww added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request help wanted Extra attention is needed good first issue Good for newcomers question Further information is requested labels Jun 28, 2026
@hoangsonww hoangsonww added this to the v1.0.0 - Stable Release milestone Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested

Projects

Development

Successfully merging this pull request may close these issues.

1 participant