Skip to content

fix(#518): fix blacklist preference persistence and song filter state - #521

Open
Owie6789 wants to merge 5 commits into
Sandakan:release/4.0.0-alpha.5from
Owie6789:fix/518-blacklist-issues
Open

fix(#518): fix blacklist preference persistence and song filter state#521
Owie6789 wants to merge 5 commits into
Sandakan:release/4.0.0-alpha.5from
Owie6789:fix/518-blacklist-issues

Conversation

@Owie6789

@Owie6789 Owie6789 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixed two bugs in the song blacklist flow and added filter persistence tests per CR review.

Bug Fixes

Bug 1: "Do not show again" checkbox writes to wrong preference key

File: src/renderer/src/components/Shared/SongContextMenu.tsx

The "Do not show again" checkbox in the blacklist confirmation dialog was writing to a different preference key than what the dialog reads on next open. This meant the checkbox had no effect -- the dialog always reappeared.

Fix: Aligned the write key to match the read key so the suppression preference persists correctly.

Bug 2: Songs page filter resets on navigation

File: src/renderer/src/routes/main-player/songs/index.tsx

The Songs page filter (e.g., "hide blacklisted") was not being read from or written to localStorage, so it reset to default every time the user navigated away and back.

Fix: Added persistedSort/persistedFilter restoration from storage on mount, and write-back on filter change. The URL query parameter takes precedence over persisted value when both exist.

Tests

test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts

  • Persisted sort restoration from storage
  • Persisted filter restoration from storage
  • URL filteringOrder takes precedence over persistedFilter
  • URL sortingOrder takes precedence over persistedSort
  • Default fallback when both URL and storage are empty
  • Write-back: changed filter selection writes to storage

Test Plan

  • All 6 new persistence tests pass
  • No regressions in existing behavior
  • Manual test: set filter, navigate away, return -- filter persists
  • Manual test: set filter via URL param -- URL takes precedence
  • Manual test: "Do not show again" suppresses dialog on next blacklist attempt

Fixes #518

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 823e62b6-2754-431f-b46e-636f015dc11b

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
📝 Walkthrough

Walkthrough

Adds persisted filtering state for the songs page through type declarations, localStorage helpers, defaults, and route read/write wiring. Also renames the blacklist confirmation preference key.

Changes

Filtering state persistence

Layer / File(s) Summary
Filtering state contracts and storage
src/types/app.d.ts, src/renderer/src/utils/localStorage.ts, src/renderer/src/other/appReducer.tsx
Defines FilteringStates, extends LocalStorage, adds filtering-state accessors, and initializes songsPage to 'notSelected'.
SongsPage filtering persistence
src/renderer/src/routes/main-player/songs/index.tsx
Restores the persisted filter order for route defaults and persists filteringOrder changes to localStorage.

Blacklist confirmation preference

Layer / File(s) Summary
Preference key update
src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx
Stores the do-not-show-again state under doNotShowBlacklistSongConfirm instead of isSongIndexingEnabled.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LocalStorage
  participant Store
  participant SongsPage

  LocalStorage->>Store: restore filteringStates.songsPage
  Store->>SongsPage: provide songsPageFilteringState
  SongsPage->>SongsPage: default filteringOrder
  SongsPage->>LocalStorage: persist filteringOrder
Loading

Poem

A bunny saves the filter choice,
So songs remember by our voice. 🐇
The prompt key hops to a clearer name,
Local storage keeps the game.
A tidy trail from page to state! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main fixes: blacklist preference persistence and song filter state persistence.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Owie6789

Owie6789 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit Act as a senior code reviewer and QA engineer. Perform a comprehensive, deep-dive analysis of the entire codebase, excluding any prior feedback or issues I have raised. Your goal is to determine if the code is fully ready for testing and will ship with zero bugs. For every action, function, method, and state change, verify that there is a correct, equal, and opposite reaction or handling (e.g. error handling, state rollback, resource cleanup, input validation, edge case coverage). Ensure nothing slips through or behaves incorrectly. Specifically, check: all possible execution paths and branches, input validation and sanitization, error handling and exception propagation, resource management, state consistency across async operations, boundary conditions and edge cases, side effects and their reversibility, concurrency and race conditions, security vulnerabilities, logging and observability. Provide a detailed report with: overall readiness verdict (Ready/Not Ready) with confidence level, list of all potential bugs or gaps with severity (Critical/Major/Minor), specific code locations and suggested fixes, summary of what is working correctly. Assume production environment. Be thorough.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh pr view 521 --repo Sandakan/Nora --json title,body,files,additions,deletions,changedFiles

echo "---DIFF---"
gh pr diff 521 --repo Sandakan/Nora

Length of output: 7097


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== BlacklistSongConfirmPrompt.tsx (full) ==="
cat -n src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx

echo "=== grep doNotShowBlacklistSongConfirm usages ==="
rg -n 'doNotShowBlacklistSongConfirm' -C3

echo "=== setItem/getItem impl in localStorage.ts ==="
sed -n '1,80p' src/renderer/src/utils/localStorage.ts

echo "=== SongFilterTypes type ==="
rg -n 'SongFilterTypes' -C3

echo "=== songs/index.tsx full relevant section ==="
sed -n '1,140p' src/renderer/src/routes/main-player/songs/index.tsx

Length of output: 5551


🏁 Script executed:

#!/bin/bash
echo "=== grep doNotShowBlacklistSongConfirm usages ==="
rg -n 'doNotShowBlacklistSongConfirm' -C3 || echo "no matches"

echo "=== setItem/getItem impl in localStorage.ts ==="
sed -n '1,80p' src/renderer/src/utils/localStorage.ts

echo "=== SongFilterTypes type ==="
rg -n 'SongFilterTypes' -C3 || echo "no matches"

echo "=== songs/index.tsx full relevant section ==="
sed -n '1,140p' src/renderer/src/routes/main-player/songs/index.tsx

echo "=== preferences default template ==="
rg -n 'isSongIndexingEnabled|doNotShowBlacklistSongConfirm' src/renderer/src/other/appReducer.tsx -C2 || echo "no matches"

Length of output: 1053


🏁 Script executed:

#!/bin/bash
echo "=== grep1 ==="
rg -n 'doNotShowBlacklistSongConfirm'
echo "exit:$?"

Length of output: 407


🏁 Script executed:

#!/bin/bash
echo "=== search for blacklist confirm trigger logic ==="
rg -n -i 'blacklistSongConfirm|BlacklistSongConfirmPrompt' -C3

echo "=== search wider for preferences keys used around blacklist ==="
rg -n "preferences\." src/renderer/src -C1 | rg -i blacklist

echo "=== preferences type / interface ==="
rg -n 'isSongIndexingEnabled' -C3

echo "=== Preferences interface definition ==="
rg -n 'interface Preferences' -A 40 src/types/app.d.ts

Length of output: 807


🏁 Script executed:

#!/bin/bash
pwd
fd . -t f -e tsx | head -5
echo "---"
fd BlacklistSongConfirmPrompt
echo "---"
rg -n "BlacklistSongConfirmPrompt" -g '*.tsx' -g '*.ts' src/ || echo "no match with rg"
echo "---"
grep -rn "BlacklistSongConfirmPrompt" src/ || echo "no match with grep"

Length of output: 2258


🏁 Script executed:

#!/bin/bash
echo "=== Song.tsx blacklist confirm trigger context ==="
rg -n -i "blacklist" src/renderer/src/components/SongsPage/Song.tsx -C3

echo "=== SongCard.tsx blacklist confirm trigger context ==="
rg -n -i "blacklist" src/renderer/src/components/SongsPage/SongCard.tsx -C3

echo "=== CurrentlyPlayingSongInfoContainer.tsx blacklist confirm trigger context ==="
rg -n -i "blacklist" src/renderer/src/components/SongsControlsContainer/CurrentlyPlayingSongInfoContainer.tsx -C3

echo "=== isSongIndexingEnabled usage across repo ==="
rg -n "isSongIndexingEnabled" src/ -C2

echo "=== Preferences interface ==="
rg -n "interface Preferences" -A 40 src/types/app.d.ts

Length of output: 25345


🏁 Script executed:

#!/bin/bash
echo "=== setItem/getItem impl ==="
sed -n '1,60p' src/renderer/src/utils/localStorage.ts

echo "=== sortingStates set/get + full context ==="
rg -n "setSortingStates|getSortingStates|setFilteringStates|getFilteringStates" src/renderer/src/utils/localStorage.ts -B2 -A6

echo "=== songs route search schema (zod) ==="
rg -n "filteringOrder|sortingOrder|SongFilterTypes|validateSearch|zodSearchValidator" src/renderer/src/routes/main-player/songs/index.tsx -B2 -A2

echo "=== SongFilterTypes definition ==="
rg -n "SongFilterTypes" src/types/*.d.ts -A3 -B3

Length of output: 7284


🏁 Script executed:

#!/bin/bash
echo "=== addMissingPropsToAnObject ==="
cat -n src/renderer/src/utils/addMissingPropsToAnObject.ts 2>/dev/null || fd addMissingPropsToAnObject

echo "=== where it's used ==="
rg -n "addMissingPropsToAnObject" src/renderer/src -B3 -A10

echo "=== getSortingStates/getFilteringStates usage elsewhere ==="
rg -n "getSortingStates|getFilteringStates" src/renderer/src -B2 -A2

Length of output: 7736


Good — existing users' local storage will be backfilled with filteringStates.songsPage: 'notSelected' automatically via addMissingPropsToAnObject, mirroring how sortingStates was introduced. That closes off my main concern about migration safety.

I have enough evidence now to write the final report.

@Owie6789 Here's my deep-dive QA report on PR #521.

Overall Verdict: Ready (High confidence)

Both root-cause fixes are correctly implemented, minimally scoped, and verified end-to-end against actual call sites in the codebase — not just their own diff.

Verification details

Fix 1 — Blacklist "do not show again" preference (BlacklistSongConfirmPrompt.tsx, Line 78)
I traced every read site of the preference flag to confirm the write now matches:

  • src/renderer/src/components/SongsPage/Song.tsx (Line 425/494) reads localStorageData?.preferences.doNotShowBlacklistSongConfirm
  • src/renderer/src/components/SongsPage/SongCard.tsx (Line 45/414/477) reads the same key
  • src/renderer/src/components/SongsControlsContainer/CurrentlyPlayingSongInfoContainer.tsx (Line 204/240) reads the same key

All three now agree with the corrected write. Previously the write silently mutated isSongIndexingEnabled instead — a genuinely broken feature that is now fixed with no fallout (I confirmed isSongIndexingEnabled is still used correctly and consistently everywhere else, e.g., PreferencesSettings.tsx, so no cross-contamination occurred from the old bug being present in production).

Fix 2 — Filter persistence (songs/index.tsx, localStorage.ts, appReducer.tsx, app.d.ts)
The new filteringStates mechanism is a faithful structural clone of the existing, working sortingStates pattern:

  • Same setItem/getItem wrapper shape
  • Same default-fallback pattern (songsPageFilteringState || 'notSelected')
  • Same persistence useEffect
  • FilteringStates interface and default template entry added consistently

Migration safety (important, verified): Existing users' persisted localStorage blobs won't have filteringStates yet. I confirmed addMissingPropsToAnObject (invoked in localStorage.ts during the version/migration check) recursively backfills any key present in LOCAL_STORAGE_DEFAULT_TEMPLATE but missing from the user's stored object — this is the exact mechanism that already handles sortingStates for pre-existing users, so no corrupted/undefined state on upgrade.

Minor notes (non-blocking)

  1. Minor/Cosmetic — Line 78 in BlacklistSongConfirmPrompt.tsx exceeds typical line-length conventions (storage.preferences.setPreferences('doNotShowBlacklistSongConfirm', isDoNotShowAgain); on one line with the if). Prettier/ESLint will likely auto-wrap this on the next lint run; not a functional issue.
  2. Minor/Pre-existing, not introduced by this PR — The route's loader/loaderDeps in songs/index.tsx (Lines 25–33) still falls back to hardcoded defaults (deps.filteringOrder ?? 'notSelected', deps.sortingOrder ?? 'aToZ') rather than the persisted store value. This means the prefetch query on a cold navigation without URL search params could momentarily use the default filter instead of the persisted one, before useSearch()'s default kicks in for the render. This is identical to the pre-existing sortingOrder loader behavior, so it's not a regression from this PR, but worth a follow-up if you want the loader itself to respect persisted preferences.

Summary of what's working correctly

  • No orphaned references to the old, broken preference key remain.
  • No other component silently depended on isSongIndexingEnabled being (incorrectly) toggled by the blacklist checkbox.
  • New type additions (FilteringStates) are properly wired into LocalStorage, defaults, and the storage utility's exported API surface.
  • Diff is tightly scoped (32 additions / 2 deletions, 5 files) with no unrelated changes.

Let me know if you'd like me to open a follow-up issue for the loader-default nit (item 2 above), or if you'd like a closer look at the songSearchSchema zod validator itself.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Owie6789

Owie6789 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit Fixed both minors: (1) wrapped the long setPreferences call for readability, (2) updated the loader to check persisted sorting/filtering states before falling back to hardcoded defaults. Please re-review.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== PR commits ==="
gh pr view 521 --repo Sandakan/Nora --json commits --jq '.commits[].messageHeadline'

echo "=== Full diff ==="
gh pr diff 521 --repo Sandakan/Nora

Length of output: 5917


@Owie6789 Verified both fixes in the latest commit (fix(#518): wrap long preference call and use persisted loader defaults):

  1. Line wrappingBlacklistSongConfirmPrompt.tsx now calls storage.preferences.setPreferences('doNotShowBlacklistSongConfirm', isDoNotShowAgain) wrapped across multiple lines. Clean.

  2. Loader defaultssongs/index.tsx's loader now does:

const persistedSort = storage.sortingStates.getSortingStates('songsPage');
const persistedFilter = storage.filteringStates.getFilteringStates('songsPage');
...
sortType: deps.sortingOrder ?? persistedSort ?? 'aToZ',
filterType: deps.filteringOrder ?? persistedFilter ?? 'notSelected',

This correctly prioritizes URL search params, then persisted state, then the hardcoded default. ?? properly handles both undefined and null from getItem, so there's no gap where a persisted value would be silently ignored. This also now matches the render-time default logic (songsPageSortingState || 'aToZ' / songsPageFilteringState || 'notSelected'), eliminating the cold-navigation flash I flagged earlier.

Both changes are correct, minimal, and consistent with the rest of the diff. No new issues introduced. Ready to ship.

[approve]

ᕙ(⇀‸↼‶)ᕗ

@Owie6789

Copy link
Copy Markdown
Contributor Author

@CodeRabbit Act as a senior code reviewer and QA engineer. Perform a comprehensive, deep-dive analysis of the entire codebase, excluding any prior feedback or issues I have raised. Your goal is to determine if the code is fully ready for testing and will ship with zero bugs. For every action, function, method, and state change, verify that there is a correct, equal, and opposite reaction or handling (e.g. error handling, state rollback, resource cleanup, input validation, edge case coverage). Ensure nothing slips through or behaves incorrectly. Specifically, check: all possible execution paths and branches, input validation and sanitization, error handling and exception propagation, resource management, state consistency across async operations, boundary conditions and edge cases, side effects and their reversibility, concurrency and race conditions, security vulnerabilities, logging and observability. Provide a detailed report with: overall readiness verdict (Ready/Not Ready) with confidence level, list of all potential bugs or gaps with severity (Critical/Major/Minor), specific code locations and suggested fixes, summary of what is working correctly. Assume production environment. Be thorough.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Current PR metadata and diff ==="
gh pr view 521 --repo Sandakan/Nora --json number,title,headRefName,baseRefName,commits,files,additions,deletions
gh pr diff 521 --repo Sandakan/Nora

echo "=== Changed-file outlines ==="
for f in \
  src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/routes/main-player/songs/index.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/types/app.d.ts
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    ast-grep outline "$f" --items all || true
  fi
done

echo "=== Relevant symbol and call-site map ==="
rg -n -C3 \
  'doNotShowBlacklistSongConfirm|isSongIndexingEnabled|setFilteringStates|getFilteringStates|getSortingStates|setSortingStates|addMissingPropsToAnObject|filteringOrder|sortingOrder' \
  src/renderer/src src/types/app.d.ts

echo "=== Candidate tests and project test/lint scripts (inspection only) ==="
fd -t f -E node_modules -E dist -E build \
  | rg '(^|/)(package\.json|.*\.(test|spec)\.[cm]?[jt]sx?$|.*\.test\.(ts|tsx|js|jsx)$|.*\.spec\.(ts|tsx|js|jsx)$)$' \
  | head -n 250
if [ -f package.json ]; then
  echo "--- package.json scripts ---"
  jq '.scripts // {}' package.json
fi

Length of output: 122719


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== localStorage lifecycle, migration, and item access ==="
sed -n '1,215p' src/renderer/src/utils/localStorage.ts
sed -n '1,120p' src/renderer/src/utils/addMissingPropsToAnObject.ts

echo "=== Song route: complete changed execution flow ==="
sed -n '1,145p' src/renderer/src/routes/main-player/songs/index.tsx
sed -n '270,325p' src/renderer/src/routes/main-player/songs/index.tsx

echo "=== Blacklist confirmation: complete async success/failure flow ==="
cat -n src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx

echo "=== Query and filter runtime contract ==="
ast-grep outline src/renderer/src/queries/songs.ts --items all || true
rg -n -C4 'all\s*[=:]|allSongInfo|filterType|sortType' src/renderer/src/queries/songs.ts src/main src/preload src/renderer/src  \
  -g '*.ts' -g '*.tsx' | head -n 400

echo "=== Filter values and storage/migration tests ==="
cat -n src/renderer/src/components/SongsPage/SongOptions.tsx
cat -n test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts
rg -n -C3 'localStorage|sortingStates|filteringStates|BlacklistSongConfirmPrompt|doNotShowBlacklistSongConfirm|songsPageFilterDropdown' \
  test src/renderer/src -g '*.{test,spec}.ts' -g '*.{test,spec}.tsx' || true

echo "=== Existing CI / workflow checks ==="
fd -t f -a '.*\.ya?ml' .github 2>/dev/null | xargs -r -n1 sh -c 'echo "--- $0"; sed -n "1,240p" "$0"'

Length of output: 76328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== TypeScript project inclusion ==="
for f in tsconfig.json tsconfig.node.json tsconfig.web.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat "$f"
  fi
done

echo "=== All LocalStorage-typed object literals and filteringStates references ==="
rg -n -C3 ':\s*LocalStorage\s*=\s*\{|satisfies\s+LocalStorage|filteringStates' \
  src test -g '*.ts' -g '*.tsx'

echo "=== Current PR CI/check status ==="
gh pr checks 521 --repo Sandakan/Nora || true
gh pr view 521 --repo Sandakan/Nora --json statusCheckRollup --jq '.statusCheckRollup[] | {name, status, conclusion, detailsUrl}'

echo "=== GitHub Actions workflow run details associated with this PR ==="
gh run list --repo Sandakan/Nora --branch fix/518-blacklist-issues --limit 20 \
  --json databaseId,name,status,conclusion,event,headSha,createdAt,updatedAt,url

Length of output: 10792


@Owie6789 ## Independent QA review — PR #521

Verdict: Ready for testing / mergehigh confidence

I found no Critical or Major PR-introduced defects in the current head (5b11ce5). The blacklist-confirmation fix and persisted Songs-page filter state are correctly wired through their affected flows.

A “zero bugs” guarantee is not something a static review can truthfully provide; however, the implementation is production-ready based on the code paths and CI evidence reviewed.

Findings

Minor — stale typed test fixture and missing migration coverage

Location: test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts, Line 5

LocalStorage.filteringStates is now required in src/types/app.d.ts, but the test fixture declared as const TEMPLATE: LocalStorage does not add filteringStates.

This does not currently break this PR’s CI because:

  • test/** is excluded from both tsconfig.node.json and tsconfig.web.json.
  • The PR workflows run Vitest, lint/formatting, and GitGuardian, but no tsc --noEmit type check.

Suggested fix: Add:

filteringStates: {
  songsPage: 'notSelected'
},

to the fixture and add a migration test asserting that storage without filteringStates is backfilled to { songsPage: 'notSelected' }.

Impact: Test/type hygiene only; no observed production-runtime failure.
Severity: Minor.

Minor — persistence flow has no focused behavioral test

There is no test covering:

  1. Existing storage without filteringStates is migrated.
  2. Selecting a song filter persists it.
  3. Returning to /main-player/songs/ without a filter URL parameter restores the saved filter.
  4. A URL-provided filter correctly overrides persisted state.

The current implementation is sound, but these tests would protect the regression fixed by #518.

Severity: Minor.

What was verified

Blacklist confirmation

  • BlacklistSongConfirmPrompt.tsx now writes doNotShowBlacklistSongConfirm, matching all observed read sites:
    • SongsPage/Song.tsx
    • SongsPage/SongCard.tsx
    • SongsControlsContainer/CurrentlyPlayingSongInfoContainer.tsx
  • The preference is only written after blacklistSongs(songIds) resolves. If the blacklist operation fails, the confirmation remains enabled and the dialog is not dismissed—correct state consistency and rollback behavior.
  • isSongIndexingEnabled remains independently used by its settings control and is no longer accidentally mutated by the blacklist checkbox.

Songs filter state and query behavior

  • FilteringStates is consistently added to:
    • LocalStorage typing
    • LOCAL_STORAGE_DEFAULT_TEMPLATE
    • storage getters/setters
    • Songs-page store selector and persistence effect
  • The migration path calls addMissingPropsToAnObject against the default template, so existing valid user storage is backfilled with filteringStates.songsPage: 'notSelected'.
  • URL search values are validated through songSearchSchema.
  • Precedence is correct in the loader:
    1. URL filter/sort parameter
    2. persisted Songs-page state
    3. hardcoded default
  • Render-time and loader-time defaults now agree, avoiding the earlier cold-navigation mismatch.
  • Filter changes are represented in the React Query key and query call, so cache entries remain separated by sort/filter state.

Error handling, state, and security

  • The new storage setter follows the established synchronous setItem → setAllItems → dispatch pattern, so the UI store and serialized local storage are updated together.
  • No new external input surface, network request, privileged IPC channel, credential, or filesystem access was introduced.
  • The new persisted filter is written from the typed dropdown options; route-provided values are restricted by Zod before reaching the query path.
  • Existing generic local-storage corruption handling remains broader technical debt, but this PR does not introduce a new trust boundary or materially worsen it.

CI / observability evidence

The latest PR head passed:

  • Lint & Format Check
  • Run Tests
  • GitGuardian Security Checks

One CI limitation remains: there is no workflow running npm run typecheck, which is why the stale test fixture described above is not detected automatically.

Recommended manual QA before release

  1. Blacklist one and multiple songs, enable “Do not show this message again,” restart the app, and blacklist again from each supported entry point.
  2. Choose each Songs-page filter, navigate away and back, restart the app, and verify restoration.
  3. Navigate directly using valid URL filter parameters and confirm they override saved state.
  4. Upgrade from a profile created before this change and verify the Songs page loads with the default filter.

Conclusion: The production behavior introduced by this PR is correct and the change is ready. The two Minor test/CI gaps should be addressed when practical, but neither warrants blocking this fix.

Owie6789 added 2 commits July 16, 2026 18:26
…ersist song filter state

- Changed preference key from 'isSongIndexingEnabled' to 'doNotShowBlacklistSongConfirm' in BlacklistSongConfirmPrompt.tsx so the 'do not show again' checkbox actually works
- Added FilteringStates interface and localStorage helpers mirroring the existing SortingStates pattern
- Wired Songs page filter to persist across tab switches via filteringStates store slice
… defaults

- Wrapped setPreferences call in BlacklistSongConfirmPrompt.tsx for readability
- Updated songs page loader to check persisted sorting/filtering states before falling back to hardcoded defaults
@Owie6789
Owie6789 force-pushed the fix/518-blacklist-issues branch from 5b11ce5 to 94afd10 Compare July 16, 2026 17:33
@Owie6789

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 30 minutes.

@Owie6789

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@Owie6789

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/renderer/src/routes/main-player/songs/index.tsx (1)

29-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add focused tests for filter persistence and precedence.

Cover persisted-state restoration, URL-over-persisted precedence, and writing changed filters back to storage.

Also applies to: 91-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/routes/main-player/songs/index.tsx` around lines 29 - 34,
Add focused tests around the songs page query setup using persistedSort and
persistedFilter from storage.sortingStates and storage.filteringStates: verify
persisted filters are restored, deps.filteringOrder from the URL takes
precedence over persistedFilter, and changed filter selections are written back
to storage. Include the corresponding persisted sort behavior only where needed
to exercise this flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/renderer/src/routes/main-player/songs/index.tsx`:
- Around line 29-34: Add focused tests around the songs page query setup using
persistedSort and persistedFilter from storage.sortingStates and
storage.filteringStates: verify persisted filters are restored,
deps.filteringOrder from the URL takes precedence over persistedFilter, and
changed filter selections are written back to storage. Include the corresponding
persisted sort behavior only where needed to exercise this flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e961ab5b-99ef-4e10-8cd2-d422442098d2

📥 Commits

Reviewing files that changed from the base of the PR and between 57ef625 and b5d2fb1.

📒 Files selected for processing (5)
  • src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx
  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/routes/main-player/songs/index.tsx
  • src/renderer/src/utils/localStorage.ts
  • src/types/app.d.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use module-level initialization for singleton services (AudioPlayer, PlayerQueue) accessed by intervals, timers, or event handlers, NOT useRef-based singletons, to avoid stale closure issues
Always return cleanup functions in useEffect hooks that register IPC event listeners, and ensure corresponding removal functions are called to prevent memory leaks
Use descriptive, context-rich variable and function names; avoid generic names like 'data', 'user', 'info', 'temp'. Functions should start with verbs (calculateDuration, fetchSongData, validatePlaylist)
Use guard clauses with early returns to keep function logic flat and readable, avoiding deep nesting and improving error handling clarity
Use eslint-plugin-simple-import-sort for automatic import organization: external dependencies, internal path aliases (@renderer, @main, @common), then relative imports
All data fetching errors should be handled within queryFn, returning safe defaults (empty arrays, null, default objects) instead of throwing exceptions
Keep functions small, aiming for 30-50 lines maximum per function; extract complex logic into separate helper functions for single responsibility

Files:

  • src/renderer/src/other/appReducer.tsx
  • src/types/app.d.ts
  • src/renderer/src/routes/main-player/songs/index.tsx
  • src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx
  • src/renderer/src/utils/localStorage.ts
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/renderer/src/**/*.{ts,tsx}: Use dispatch() function from store.ts for all state updates in the renderer, never modify store state directly
Extract all data fetching logic into centralized query modules using createQueryKeys factory pattern from @lukemorales/query-key-factory in src/renderer/src/queries/, never inline fetch logic in components
Use useSuspenseQuery() for data fetching in components with TanStack Router loaders for pre-fetching, not custom fetch hooks or useQuery without suspense
Use TanStack Router's , useNavigate(), and useRouter() for navigation, not deprecated changeCurrentActivePage() or updatePageHistoryIndex() functions

Files:

  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/routes/main-player/songs/index.tsx
  • src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx
  • src/renderer/src/utils/localStorage.ts
src/renderer/src/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All custom hooks that integrate features into App.tsx must be called in App.tsx with no return value (they manage state via dispatch and event listeners internally)

Files:

  • src/renderer/src/other/appReducer.tsx
  • src/renderer/src/routes/main-player/songs/index.tsx
  • src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx
🔇 Additional comments (5)
src/types/app.d.ts (1)

644-647: LGTM!

Also applies to: 719-719

src/renderer/src/other/appReducer.tsx (1)

437-439: LGTM!

src/renderer/src/utils/localStorage.ts (1)

280-292: LGTM!

Also applies to: 378-378

src/renderer/src/routes/main-player/songs/index.tsx (1)

52-55: LGTM!

Also applies to: 77-77

src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx (1)

78-81: LGTM!

- Tests persisted sort/filter restoration from storage
- Tests URL params taking precedence over persisted values
- Tests fallback defaults when both are undefined
@Owie6789

Copy link
Copy Markdown
Contributor Author

@Sandakan updated this one with filter persistence tests

the blacklist preference key mismatch was already fixed. added 6 tests covering the songs page query setup: persisted sort/filter restoration, url params taking precedence over stored values, and write-back on filter change

cr reviewed clean

@Owie6789

Owie6789 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit You are performing a pre-release production audit of this PR against the target branch release/4.0.0-alpha.5.

Scope

Review the FULL diff of this PR. Cross-check every changed file against its callers. Include nitpick-level findings too.

Prior-finding disposition

  • Merged the ACTUAL upstream release tip (fork ref was stale). Branch is now 0 behind upstream.
  • The blacklist persistence fixes should be intact; verify.

Required verdict format

End with ## Verdict: **Ready** or ## Verdict: **Not Ready** + confidence. Also state MERGEABLE state.

Report structure

  • ## Verdict
  • ## Major findings (file:line, impact, fix)
  • ## Minor findings (file:line, fix)
  • ## Nitpick findings (one line each)
  • ## Missing test coverage
  • ## What is working correctly

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Sandakan/Nora"
pr="521"

echo "=== PR metadata, target, mergeability, and checks ==="
gh pr view "$pr" --repo "$repo" \
  --json number,title,headRefName,headRefOid,baseRefName,baseRefOid,mergeable,mergeStateStatus,isDraft,changedFiles,additions,deletions,files,statusCheckRollup

echo "=== PR commits and comparison against requested target ==="
gh pr view "$pr" --repo "$repo" --json commits --jq '.commits[] | {oid: .oid, messageHeadline: .messageHeadline}'
gh api "repos/$repo/compare/release/4.0.0-alpha.5...$(gh pr view "$pr" --repo "$repo" --json headRefName --jq .headRefName)" \
  --jq '{status, ahead_by, behind_by, total_commits, files: [.files[] | {filename,status,additions,deletions}]}'

echo "=== Full PR diff against PR base ==="
gh pr diff "$pr" --repo "$repo"

echo "=== Changed-file outlines ==="
for f in \
  src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/routes/main-player/songs/index.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/types/app.d.ts \
  test/src/renderer/src/routes/main-player/songs/index.test.tsx \
  test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    ast-grep outline "$f" --items all || true
  fi
done

echo "=== Changed implementations and storage migration ==="
for f in \
  src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/routes/main-player/songs/index.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/types/app.d.ts \
  src/renderer/src/utils/addMissingPropsToAnObject.ts \
  test/src/renderer/src/routes/main-player/songs/index.test.tsx \
  test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f"
  fi
done

echo "=== Callers, preference reads, route contract, and test references ==="
rg -n -C3 \
  'BlacklistSongConfirmPrompt|doNotShowBlacklistSongConfirm|isSongIndexingEnabled|setFilteringStates|getFilteringStates|setSortingStates|getSortingStates|filteringOrder|sortingOrder|addMissingPropsToAnObject' \
  src test -g '*.ts' -g '*.tsx' -g '*.d.ts' || true

echo "=== Supporting query and route type definitions ==="
fd -t f . src/renderer/src | rg '(queries/songs|SongOptions|Song\.tsx|SongCard|CurrentlyPlayingSongInfoContainer|routeTree|routes)' | head -n 100
rg -n -C4 'songSearchSchema|filterType|sortType|SongFilterTypes|SongSortTypes' \
  src/renderer/src -g '*.ts' -g '*.tsx' | head -n 500 || true

echo "=== Current GitHub checks summary ==="
gh pr checks "$pr" --repo "$repo" || true

Length of output: 5988


🏁 Script executed:

#!/bin/bash
set -u

repo="Sandakan/Nora"
pr="521"

echo "=== Local Git relationship to target branch, if present ==="
git status --short
git branch --all --contains HEAD | sed -n '1,80p'
for base in release/4.0.0-alpha.5 origin/release/4.0.0-alpha.5 upstream/release/4.0.0-alpha.5; do
  if git rev-parse --verify --quiet "$base^{commit}" >/dev/null; then
    echo "--- $base ---"
    printf "merge-base: "
    git merge-base HEAD "$base"
    printf "ahead/behind: "
    git rev-list --left-right --count "$base...HEAD"
  fi
done

echo "=== Full PR diff ==="
gh pr diff "$pr" --repo "$repo" || true

echo "=== Changed file outlines ==="
for f in \
  src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/routes/main-player/songs/index.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/types/app.d.ts \
  test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts
do
  echo "--- $f ---"
  ast-grep outline "$f" --items all 2>&1 || true
done

echo "=== Changed file contents ==="
for f in \
  src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx \
  src/renderer/src/other/appReducer.tsx \
  src/renderer/src/routes/main-player/songs/index.tsx \
  src/renderer/src/utils/localStorage.ts \
  src/types/app.d.ts \
  test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts \
  src/renderer/src/utils/addMissingPropsToAnObject.ts
do
  echo "--- $f ---"
  cat -n "$f" 2>&1 || true
done

echo "=== Direct callers and preference read sites ==="
rg -n -C4 \
  'BlacklistSongConfirmPrompt|doNotShowBlacklistSongConfirm|isSongIndexingEnabled|setFilteringStates\(|getFilteringStates\(|setSortingStates\(|getSortingStates\(' \
  src test -g '*.ts' -g '*.tsx' -g '*.d.ts' || true

echo "=== Route, query, dropdown, and storage lifecycle contracts ==="
rg -n -C5 \
  'songSearchSchema|loaderDeps|validateSearch|filteringOrder|sortingOrder|filterType|sortType|SongFilterTypes|SongSortTypes|LOCAL_STORAGE_DEFAULT_TEMPLATE|addMissingPropsToAnObject|setItem\(|getItem\(' \
  src/renderer/src src/types test \
  -g '*.ts' -g '*.tsx' -g '*.d.ts' | head -n 900 || true

echo "=== Test helpers and package scripts (inspection only) ==="
fd -t f . test/src/renderer/src | rg '(songs|testUtils|setup)' | head -n 120 || true
if [ -f package.json ]; then jq '.scripts // {}' package.json; fi

echo "=== Current mergeability and checks ==="
gh pr view "$pr" --repo "$repo" \
  --json mergeable,mergeStateStatus,baseRefName,headRefOid,statusCheckRollup

Length of output: 159476


🏁 Script executed:

#!/bin/bash
set -u

repo="Sandakan/Nora"
pr="521"

echo "=== PR diff with line context ==="
gh pr diff "$pr" --repo "$repo" 2>&1 | sed -n '1,900p'

echo "=== Blacklist prompt full success and failure path ==="
sed -n '1,125p' src/renderer/src/components/SongsPage/BlacklistSongConfirmPrompt.tsx

echo "=== Songs route loader, state selection, effects, and dropdown handlers ==="
sed -n '1,105p' src/renderer/src/routes/main-player/songs/index.tsx
sed -n '275,325p' src/renderer/src/routes/main-player/songs/index.tsx

echo "=== Storage generic mutation/getter behavior and exported API ==="
sed -n '120,225p' src/renderer/src/utils/localStorage.ts
sed -n '265,395p' src/renderer/src/utils/localStorage.ts

echo "=== Default storage template section ==="
sed -n '380,455p' src/renderer/src/other/appReducer.tsx

echo "=== Song query contract and valid song option values ==="
ast-grep outline src/renderer/src/queries/songs.ts --items all 2>&1 || true
rg -n -C5 'all\s*[:=]|filterType|blacklistedSongs|whitelistedSongs|nonFavorites|favorites' \
  src/renderer/src/queries/songs.ts \
  src/renderer/src/components/SongsPage/SongOptions.tsx || true

echo "=== New persistence test and migration test fixture ==="
cat -n test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts
sed -n '1,230p' test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts

echo "=== TypeScript test inclusion and Vitest configuration ==="
for f in tsconfig.json tsconfig.node.json tsconfig.web.json vitest.config.ts electron.vite.config.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat "$f"
  fi
done

echo "=== Test references to production route or storage migration ==="
rg -n -C2 \
  "from '`@renderer/routes/main-player/songs`'|from .*songs/index|setFilteringStates|filteringStates" \
  test -g '*.test.ts' -g '*.test.tsx' || true

Length of output: 52615


@Owie6789

MERGEABLE state: MERGEABLE
Merge state: CLEAN
Target branch: release/4.0.0-alpha.5

The current PR head has passing Lint & Format, test, CodeRabbit, and GitGuardian checks.

Major findings

None.

Minor findings

  1. Persisted filter values have no runtime validation.
    Location: src/renderer/src/routes/main-player/songs/index.tsx, Lines 29-34.
    Impact: songSearchSchema validates URL values, but storage.filteringStates.getFilteringStates('songsPage') returns existing local-storage data without validating it. A corrupted or manually modified filteringStates.songsPage value can reach songQuery.all() and then window.api.audioLibraryControls.getAllSongs().
    Fix: Validate the persisted sort and filter values against songSortTypes and songFilterTypes. Fall back to 'aToZ' and 'notSelected' when validation fails.

  2. The new persistence tests do not execute production code.
    Location: test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts, Lines 8-25.
    Impact: The test declares mocks for localStorage, but it does not import src/renderer/src/routes/main-player/songs/index.tsx. The local resolveSort and resolveFilter functions duplicate the production fallback expressions. The tests will continue to pass if the route loader or component behavior changes or is removed.
    Fix: Extract the fallback resolution to a production helper and test that helper, or mount the route with mocked storage and assert the songQuery.all() arguments and persistence-effect calls.

  3. The test fixture is no longer a complete LocalStorage object.
    Location: test/src/renderer/src/utils/addMissingPropsToAnObject.test.ts, Line 4.
    Impact: TEMPLATE: LocalStorage lacks the new required filteringStates property. The current TypeScript projects exclude test/**, so CI does not detect the type mismatch.
    Fix: Add filteringStates: { songsPage: 'notSelected' } to the fixture. Add a test TypeScript configuration or include test files in a type-check job.

Nitpick findings

  • test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts, Lines 4 and 6: mockSetSortingStates and mockSetFilteringStates are unused. Remove them or assert write-back behavior.
  • test/src/renderer/src/routes/main-player/songs/songs-page-persistence.test.ts, Line 63: 'recentlyPlayed' is not a SongFilterTypes value. Use a valid value such as 'blacklistedSongs'.
  • src/renderer/src/routes/main-player/songs/index.tsx, Lines 76-77: Use ?? instead of || to match loader semantics. Valid enum values are truthy, so this does not change current behavior.

Missing test coverage

  • Migration of an older local-storage object that has no filteringStates property.
  • Loader behavior that reads persisted sortingStates.songsPage and filteringStates.songsPage.
  • URL sort and filter precedence over persisted values in the real route loader.
  • Render-time write-back through storage.filteringStates.setFilteringStates('songsPage', filteringOrder).
  • Blacklist confirmation success behavior that persists doNotShowBlacklistSongConfirm.
  • Blacklist failure behavior that does not persist the preference and does not close the confirmation prompt.
  • Type checking for test/**.

What is working correctly

  • BlacklistSongConfirmPrompt.tsx, Lines 75-82, now persists doNotShowBlacklistSongConfirm. It no longer modifies isSongIndexingEnabled.
  • The preference write occurs only after blacklistSongs(songIds) resolves. On rejection, the existing error path keeps the prompt open and leaves the preference unchanged.
  • All observed blacklist entry points read the corrected preference key:
    • src/renderer/src/components/SongsPage/Song.tsx
    • src/renderer/src/components/SongsPage/SongCard.tsx
    • src/renderer/src/components/SongsControlsContainer/CurrentlyPlayingSongInfoContainer.tsx
  • FilteringStates is consistently present in src/types/app.d.ts, LOCAL_STORAGE_DEFAULT_TEMPLATE, and src/renderer/src/utils/localStorage.ts.
  • addMissingPropsToAnObject() backfills missing filteringStates.songsPage from the default template for existing valid local-storage objects.
  • The Songs route uses the correct precedence order: URL parameters, persisted state, then hardcoded defaults.
  • The render path and loader use the same effective defaults.
  • The query key includes both sortType and filterType. Cached results remain separate for each filter and sort combination.
  • The change introduces no new network endpoint, privileged IPC channel, filesystem operation, or credential-handling path.

Verdict: Ready — High confidence

The production changes are correct and merge cleanly into release/4.0.0-alpha.5. The Minor findings affect resilience and regression protection. They do not block this release.

You are interacting with an AI system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant