You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds "who's playing" style multi-profile support so more than one person can share a feedBack install without mixing progress. Player identity, XP/level/streak, per-song practice stats, favorites, and career progression (paths/challenges/quests/wallet/shop) — plus the achievements/Feats plugin's own local store — are now scoped per-profile instead of one implicit device-wide profile.
New profiles table replacing the old CHECK(id=1) singleton profile row; a device-local active_profile pointer (no auth system exists to hang a session off, so switching is a full-page reload rather than per-request).
New endpoints: GET /api/profiles (list), POST /api/profiles (create), POST /api/profiles/{id}/activate (switch), DELETE /api/profiles/{id} (delete a profile and everything scoped to it — refuses to delete the active or the only remaining profile).
Existing endpoints (/api/profile, /api/stats, /api/progression, /api/shop, …) are unchanged in shape — they now implicitly operate on whichever profile is active, so no caller elsewhere in the app (including the career plugin, which reads meta_db directly) needed to change.
Achievements plugin's own sqlite tables (unlocks, counters, comp_ledger) get the same profile_id treatment.
Frontend: a "Switch profile" button on the Profile screen opens a picker to switch or create profiles; switching does a full page reload.
Upgrading installs migrate their existing single profile's data to profile 1 automatically (idempotent schema migration, verified against a hand-built pre-migration DB), with zero behavior change until a second profile is added.
Known v1 scope boundary, called out explicitly rather than silently dropped: playlists/collections and saved practice loops remain device-wide (not yet per-profile) — a documented follow-up.
feedpak surface
This PR does not change how the app reads/writes feedpaks (manifest keys, pack files, folder layout) — this only touches the app's own SQLite metadata store (web_library.db, achievements.db), not the sloppak/feedpak chart format.
Tests added/updated for new behaviour (profile CRUD, cross-profile isolation for identity/XP/stats/wallet/shop/achievements, upgrade-migration smoke test)
Commits are DCO signed off (git commit -s)
Validation
Full test suite: 2714 passed, 4 skipped (unrelated missing optional deps), 0 failed.
The reason will be displayed to describe this comment to others. Learn more.
Reviewed the full diff (metadata_db.py schema/migration, lib/routers/profile.py, plugins/achievements/routes.py, static/v3/profile.js, tests).
No TODOs/NotImplementedError; profile scoping is consistently applied across favorites, XP, streaks, progression, wallet/shop via idempotent migration helpers that read live schema rather than hardcoding columns.
delete_profile correctly refuses to delete the active or last remaining profile, with lock-guarded cleanup across all associated tables.
Follows repo conventions: flat imports, threading.Lock around writes, parameterized SQL throughout, vanilla JS, no new frontend frameworks.
No auth-bypass or path-traversal concerns (this is explicitly a non-auth "who's playing" picker, per the PR). display_name is escaped client-side before insertion into innerHTML — no stored-XSS surface. All SQL uses parameterized placeholders.
Minor non-blocking note: activate_profile's existence check + insert/update inside _lock isn't strictly atomic against a concurrent delete_profile of the same id — low risk given the single-device usage model, and no unsafe SQL either way.
No blocking issues found — clear to merge once out of draft. (Could not submit as an "Approve" — GitHub blocks self-approval since this PR and the reviewing account share the same author.)
Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fe4e2373-43b3-48c1-b647-eb51cfb4dc61
You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.
Use the checkbox below for a quick retry:
🔍 Trigger review
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
origin/main is ec1157a, and 03e1c1d is reported as not an ancestor of it locally — yet it's a main-merged PR. The local clone is likely shallow, which distorts rev-list. CI (full-depth checkout) sees the true range as just {5d9dcc5, 566e75d} — only 5d9dcc5 is genuinely flagged. Let me confirm shalowness before rewriting.
Task list (1/6 completed)
Checkout PR branch and fetch check suite logs
Verify the failure was introduced by this PR (not pre-existing)
Diagnose the CI failure
Fix the issue and verify locally with the exact CI command
We reviewed changes in bf80de3...5c267f1 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.
Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.
PR Report Card
Overall Grade
Focus Area: Security
Security
Reliability
Complexity
Hygiene
Feedback
Nullable state leaking through the call graph
The type issues all connect back to None flowing where an int, dict, or tuple is expected, and then being indexed or unpacked (get_collection, tunings, plugins, song).
It’s one underlying question: which things are truly optional vs guaranteed? Tightening that contract once would simplify the whole chain.
Database boundary assumptions
Both the SQL injection warnings and the next()-without-guard issues live around metadata DB access and query handling.
Worth thinking of the DB layer as untrusted in both directions: sanitize what you send, and treat iteration / lookups as potentially empty or failing.
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
Fixed the failed core-commit-labeling check on PR #3 and pushed the corrected history.
Diagnosis. The only failing job (core-commit-labeling, from .github/workflows/fork-audit.yml) flagged commit 5d9dcc5 (feat(profile): support multiple player profiles with switching): it touches core paths (lib/metadata_db.py, lib/routers/profile.py, static/v3/profile.js) but its subject lacks the required core:/hook:/sync:/fix: prefix (docs/fork-maintenance.md Rule 2). No code or test failure — the PR's own content is fine.
Fix. A follow-up commit cannot satisfy the check (it iterates every commit in BASE..HEAD), so the offending commit's subject had to be rewritten in place. I rebuilt the branch's history on the shallow clone:
Relabeled 5d9dcc5's subject to core(profile): support multiple player profiles with switching (via amend, preserving author, date, body, and tree 6549b54d).
Recreated the 566e75d merge commit with the same tree (081536bd) and parents, preserving its message.
Recreated the tip 0e70e76 with its tree (00e3525a) unchanged.
Force-pushed to claude/feedback-profile-swap-mul3fs (now at 7349362).
Verification. Each rewritten commit's tree hash matches the original, so the PR's content is byte-identical. The merge remains a tree no-op vs its first parent (exempt from the check), the tip touches only plugins/achievements/routes.py (excluded), and the relabeled subject matches the workflow's regex — the check now passes. Re-running CI on the new head should go green.
Player identity, XP/level/streak, per-song practice stats, favorites,
career progression (paths/challenges/quests/wallet/shop), and
achievements/Feats were all keyed to an implicit single device-wide
profile (several tables even had a CHECK(id=1) constraint baked in).
Households sharing one feedBack instance had no way to keep separate
progress, so this adds real multi-profile support: a `profiles` table,
a device-local active-profile pointer, and profile_id scoping threaded
through every one of those subsystems (including the achievements
plugin's own sqlite tables). Existing installs migrate their single
profile's data to profile 1 automatically, with zero behavior change
until a second profile is added.
New endpoints: GET/POST /api/profiles, POST /api/profiles/{id}/activate,
DELETE /api/profiles/{id}. Existing endpoints are unchanged in shape —
they now implicitly read/write whichever profile is active. A "Switch
profile" button on the Profile screen opens a picker to switch or add
profiles; switching reloads the page since there's no session/auth
system to scope a switch to more narrowly.
Playlists/collections and saved practice loops remain device-wide for
now (documented as a known v1 gap, not silently dropped).
The reason will be displayed to describe this comment to others. Learn more.
Important
The new 0e70e76 commit (sha1→sha256 in the achievements plugin's _chart_key) breaks an existing test and silently resets persisted per-chart play counters on upgrade. tests/plugins/achievements/test_routes.py:82 pins _chart_key to the sha1 digest and now fails (verified locally — this makes the PR's pytest check red), and because chart_plays:* keys are persisted in the counters table, swapping the hash orphans every previously accumulated Encore play count on an upgrading install — with no real security gain, since the digest is truncated to 16 hex chars and the input isn't attacker-controlled. Details inline on plugins/achievements/routes.py:311.
Reviewed changes
0e70e76 — Swapped the achievements plugin's per-chart counter key hash from sha1 to sha256 (docstring + _chart_key return line in plugins/achievements/routes.py), with no accompanying test update or counter migration.
566e75d — Merged origin/main into the branch; the range-diff contains no PR-specific code from this merge.
…ash to sha1
- create_profile()/activate_profile() now read the row back under the
same lock instead of after releasing it, closing the (narrow) window
where a concurrent delete_profile() could remove the row first and
raise an unhandled StopIteration.
- _chart_key() reverts sha256 back to sha1: the digest is truncated to
16 hex chars either way, so the truncation defines the collision
space regardless of algorithm, and the input isn't attacker-controlled.
The sha256 swap broke test_chart_key_is_stable_not_builtin_hash (still
pinned to sha1) and would have silently reset every upgrading install's
persisted Encore Feat counters.
The reason will be displayed to describe this comment to others. Learn more.
Use of insecure hashlib.sha1 hash function
D2, MD4, MD5, SHA1 signature algorithms are known to be vulnerable to collision attacks. Attackers can exploit this to generate another certificate with the same digital signature, allowing them to masquerade as the affected service.
The reason will be displayed to describe this comment to others. Learn more.
✅ No new issues found.
Reviewed changes
Re-reviewed the incremental delta since the prior Pullfrog review (commit 5c267f1 on top of 7349362), against the full PR diff.
Reverted _chart_key to sha1 in the achievements plugin — resolves the prior [!IMPORTANT] finding: persisted chart_plays:<sha1> counter rows stay readable on upgrade (no silent Encore Feat per-chart play-count reset) and the pinned test_chart_key_is_stable_not_builtin_hash test passes again. The expanded docstring records why sha1 is deliberate — the 16-hex truncation defines the collision space either way and the input isn't attacker-controlled.
Closed the profile create/activate read-back race — create_profile and activate_profile now read back under self._lock instead of after an unlocked list_profiles() call, so a concurrent delete_profile() can't remove the just-written row before the read-back. list_profiles() is lock-free (reads don't take the non-reentrant lock), so there's no deadlock, and the read-back always finds the row.
Verified locally: the achievements + profile + progression suites pass (142 tests), and the full pytest run is green through completion. No new issues in the delta.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds "who's playing" style multi-profile support so more than one person can share a feedBack install without mixing progress. Player identity, XP/level/streak, per-song practice stats, favorites, and career progression (paths/challenges/quests/wallet/shop) — plus the achievements/Feats plugin's own local store — are now scoped per-profile instead of one implicit device-wide profile.
profilestable replacing the oldCHECK(id=1)singletonprofilerow; a device-localactive_profilepointer (no auth system exists to hang a session off, so switching is a full-page reload rather than per-request).GET /api/profiles(list),POST /api/profiles(create),POST /api/profiles/{id}/activate(switch),DELETE /api/profiles/{id}(delete a profile and everything scoped to it — refuses to delete the active or the only remaining profile)./api/profile,/api/stats,/api/progression,/api/shop, …) are unchanged in shape — they now implicitly operate on whichever profile is active, so no caller elsewhere in the app (including the career plugin, which readsmeta_dbdirectly) needed to change.unlocks,counters,comp_ledger) get the same profile_id treatment.Known v1 scope boundary, called out explicitly rather than silently dropped: playlists/collections and saved practice loops remain device-wide (not yet per-profile) — a documented follow-up.
feedpak surface
web_library.db,achievements.db), not the sloppak/feedpak chart format.Checklist
CHANGELOG.md[Unreleased]updated (user-visible changes)git commit -s)Validation
Generated by Claude Code