Skip to content

feat(frontend): per-action loading and connection gating on music controls - #1866

Merged
LucasSantana-Dev merged 4 commits into
LucasSantana-Dev:mainfrom
Adolanium:feat/1771-music-control-action-state
Jul 27, 2026
Merged

LucasSantana-Dev merged 4 commits into
LucasSantana-Dev:mainfrom
Adolanium:feat/1771-music-control-action-state

Conversation

@Adolanium

@Adolanium Adolanium commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

Dashboard music controls stayed clickable when the SSE stream was down, and failures only set a generic bottom-of-page error with no idea which action failed.

Fix

  • useMusicPlayer / useMusicCommands: each command passes an actionKey; pendingAction tracks the in-flight action and errors are prefixed with that key.
  • Music page controls: disabled when disconnected or busy; spinner on the control that is working; dismissible error alert.
  • PlaybackControls: optional isConnected + pendingAction so the shared component matches the same rules (tests updated).

Verification

  • Unit test: PlaybackControls disables when isConnected={false}.
  • Manual: disconnect SSE / fail a command and confirm the matching control shows busy then the error names the action.

Checklist

  • PlaybackControls test for disconnected state
  • CHANGELOG.md updated (if user-facing)
  • TypeScript clean for touched hooks/components

Destructive / irreversible interaction (Tier A)

Not applicable. Frontend control UX only.

Feature-removal sweep

Not applicable.

Fixes #1771


Summary by cubic

Add per-action loading and connection gating to music controls. Controls disable when disconnected or while a command is in flight, show which control is busy, and prevent stale rollbacks and cross-guild races. Also fixes the hero track selection. Fixes #1771.

  • New Features

    • PlaybackControls: added Previous, per-action spinners with aria-busy, and global disable via isConnected/pendingAction; shared BusyIcon; tests cover disconnected, pending, and single-spinner states.
    • Page gating: disable hero controls (play/pause, repeat, shuffle, previous/next, volume), SearchBar, ImportPlaylist, and QueueList (drag/remove/clear) when not connected or busy; localized “working”/“player not connected”; dismissible error via clearError.
    • Hooks: useMusicCommands passes actionKey; useMusicPlayer tracks pendingAction with FIFO spinner selection, enforces global lockout during any command, resets loading/pending/error on guild switch, and exposes stable clearError.
  • Bug Fixes

    • Scope command guards to the current visit using a guild ref and an active command map cleared on guild changes; prevent older commands from clearing newer pendingAction; surface rollback queue refresh failures in the action-scoped error.
    • Hero now selects the current track from state.currentTrack or falls back to state.tracks[0] for older payloads.

Written for commit 5b4f8af. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Music controls are now disabled when the player is disconnected or a command is in progress.
    • Progress indicators and accessibility status are shown for active playback commands.
    • Search, playlist import, and queue actions now respect player availability.
    • Added dismissible music-player error messages and connection status feedback.
    • Added Portuguese (Brazilian) translations for new music-player messages.
  • Bug Fixes

    • Prevented commands from being triggered while unavailable or already processing.
    • Improved handling of concurrent music actions and command failures.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Music commands now carry typed action keys, useMusicPlayer tracks concurrent pending actions and stale commands, and music controls are disabled or show busy indicators based on connection and command state. The music page adds localized progress, connection, and error-dismissal UI with corresponding tests.

Changes

Music control state and interaction flow

Layer / File(s) Summary
Command action-key contract
packages/frontend/src/hooks/useMusicCommands.ts, packages/frontend/src/hooks/useMusicCommands.test.ts
Commands now pass typed action keys, while optimistic state expectations remain explicitly verified.
Player command concurrency lifecycle
packages/frontend/src/hooks/useMusicPlayer.ts, packages/frontend/src/hooks/useMusicPlayer.test.ts
The player tracks active commands by visit, derives FIFO pending state, handles stale failures and queue refresh failures, and tests concurrent command resolution.
Reusable control gating and busy indicators
packages/frontend/src/components/Music/*
Playback, queue, search, and import controls now respect disabled state and render action-specific busy indicators.
Music page state integration
packages/frontend/src/pages/Music.tsx, packages/frontend/src/pages/Music.test.tsx, packages/frontend/src/locales/*.json
The page passes connection and pending state through its controls, displays localized status messages, and supports dismissing errors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: enhancement, size/xl

Suggested reviewers: lucassantana-dev

Sequence Diagram(s)

sequenceDiagram
  participant MusicPage
  participant MusicControls
  participant useMusicPlayer
  participant useMusicCommands
  MusicPage->>MusicControls: render connection and pending state
  MusicControls->>useMusicPlayer: invoke action callback
  useMusicPlayer->>useMusicCommands: send command with actionKey
  useMusicPlayer-->>MusicPage: update pendingAction and isLoading
  MusicPage-->>MusicControls: disable controls and show busy indicator
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement per-action pending state and disable music controls when disconnected, matching issue #1771's requirements.
Out of Scope Changes check ✅ Passed The touched components, hooks, locales, and tests all support music-control loading and connection gating, with no clear unrelated changes.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main frontend change: per-action loading and connection-based gating for music controls.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/frontend/src/hooks/useMusicPlayer.ts Outdated
Comment thread packages/frontend/src/pages/Music.tsx Outdated
Comment thread packages/frontend/src/components/Music/PlaybackControls.test.tsx
Comment thread packages/frontend/src/hooks/useMusicCommands.ts
Comment thread packages/frontend/src/components/Music/PlaybackControls.tsx
Comment thread packages/frontend/src/pages/Music.tsx Outdated
Comment thread packages/frontend/src/components/Music/PlaybackControls.tsx
@Adolanium
Adolanium force-pushed the feat/1771-music-control-action-state branch from d11ddba to ad9b1f1 Compare July 21, 2026 03:56
cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Jul 21, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 10 files (changes from recent commits).

Auto-approved: Frontend-only UX improvement adding per-action loading and connection gating to music controls; bounded, clearly beneficial, with no operational or security tradeoffs.

Re-trigger cubic

@LucasSantana-Dev

Copy link
Copy Markdown
Owner

Kimi review (kimi-code/kimi-for-coding, via local subscription)

• VERDICT: ISSUES

  • [medium] useMusicPlayer.ts:753-758,785 — Guild switch resets pendingAction/isLoading, but an in-flight command from the previous guild can still complete and write stale state or an error to the new guild; finally also unconditionally clears pendingAction, which can overwrite a newer in-flight action. Fix: track the current guildId in a ref and ignore state/error/pending updates in sendCommand when the ref no longer matches; only clear pendingAction if it still equals the finishing action's key.

  • [medium] useMusicPlayer.ts:777 — The queue refresh after a command failure is silently swallowed (.catch(() => {})). Fix: surface the refresh failure, e.g. log it and/or include it in the displayed error so users know the queue may be stale.

  • [medium] PlaybackControls.test.tsx:57-79 — The new test covers isConnected=false but does not verify that pendingAction disables controls or renders the busy Loader2 icon. Fix: add tests asserting controls are disabled while a pendingAction is set and the matching button shows the spinner.


Head ad9b1f1. Posted by kimi-review-watch (launchd).

@Adolanium

Copy link
Copy Markdown
Contributor Author

Addressed in 465a532. Guild changes now use commit-scoped tokens, in-flight commands keep their own pending state, and rollback refresh failures are shown to the user. I also added coverage for disabled controls, busy icons, both command completion orders, and the Previous action path. The full frontend suite passes with 1,029 tests. The branch is up to date with main.

@LucasSantana-Dev LucasSantana-Dev left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Correcting myself first: I posted an earlier version of this review claiming a hung request would lock the UI forever. That was wrong and I've removed it. packages/frontend/src/services/api.ts:86 sets timeout: 10000 on the axios instance, so a stalled request rejects after 10s, the finally runs, and pendingAction clears. I should have checked before writing it. Sorry for the noise.

What stands after a closer read:

The test coverage here is genuinely good. Asserting exactly one .animate-spin inside the toolbar is the sort of thing that actually catches a regression, and the disconnected/pending matrix is thorough. Guarding against stale responses landing after a guild switch is a real bug fix, not busywork.

My one substantive concern is the primitive that guard is built on.

useMemo isn't an identity guarantee

const guildToken = useMemo(() => Symbol('guild'), [guildId])

React's docs are explicit that useMemo is a performance optimisation and not a semantic guarantee: React reserves the right to discard a cached value and recompute it even when the dependencies haven't changed. Here the Symbol's identity isn't an optimisation, it's load-bearing for correctness.

If it ever does recompute without guildId changing:

  • the useLayoutEffect keyed on guildToken re-runs, calling activeCommandsRef.current.clear(), setState(EMPTY_STATE) and setPendingAction(null), so the visible queue empties mid-session for no reason;
  • every in-flight command captured the old token, so isCurrentGuild() goes false and those commands drop their results including the finally that clears pendingAction.

To be straight about the confidence: I'm not aware of stable React discarding a memo in this situation today, so this is a "relying on a documented non-guarantee" problem rather than a bug I can reproduce right now. But the failure mode if it ever bites is silent and would look like "the UI randomly emptied and then froze", which is horrible to debug, and the fix is cheap. The captured guildId in the closure already tells you what you need:

const guildRef = useRef(guildId)
useLayoutEffect(() => { guildRef.current = guildId /* ...reset... */ }, [guildId])
const isCurrentGuild = () => guildRef.current === guildId

That drops the Symbol, drops guildToken from the useCallback deps, and stops sendCommand's identity churning through useMusicCommands into every memoised child.

"per-action" in the title, global lockout in the behaviour

const canPlay  = (hasTrack || isPaused) && isConnected && !pendingAction
const canTrack = hasTrack && isConnected && !pendingAction
const canAux   = isConnected && !pendingAction

Only the spinner is per-action; the disabling is all-or-nothing. A slow volume or seek blocks skip, previous, stop, shuffle and repeat, and those don't actually conflict with each other. Serialising every mutation is a defensible choice and I'm not going to insist otherwise, but I want it to be a decision rather than a side effect. If it is intended, the prop name pendingAction undersells it.

Two nits:

  • clearError: () => setError(null) is returned without useCallback, so it's a new identity every render and defeats memoisation in any consumer that takes it as a prop or dep. Stands out because the rest of this hook is careful about exactly that.
  • activeCommands.slice().reverse().find(c => c.actionKey) means with two commands in flight the most recently registered wins the spinner, so finishing command A can move the spinner onto command B's control even though B started first. Cosmetic, and it mostly goes away if the point above changes.

Merge order: this rewrites the same useMusicPlayer.ts region as #1867 and touches Music.tsx, QueueList.tsx and the locale files, overlapping #1867 and #1864. I'll sequence the three.

On the red checks: not yours. Security was an unpassable repo-wide gate, kimi-review fails on every PR because an API key isn't set (#1877), and the npm ci errors come from a stale lockfile on main. Fixed in #1876; rebase once it lands.

@LucasSantana-Dev

Copy link
Copy Markdown
Owner

Heads-up: the CI blockers I mentioned are fixed on main now (#1876, merged as ca410d2c).

What that clears:

  • Security — it ran npm audit --audit-level=high across devDependencies and could never pass, which is why it was red on every PR including this one. It now audits production deps only, with an explicit allowlist pinned to individual advisory ids.
  • npm ci / Build — shared / Quality Gates — main's lockfile was stale and npm@12's stricter ci rejected it (lock file's eslint@10.7.0 does not satisfy eslint@10.8.0). Resynced.
  • compressed-size — it installs the base branch too, so it was failing on main's lockfile rather than on your changes.
  • kimi-review — removed in ci: remove kimi-review until an api key is set #1884; the API key was never set, so it failed on every PR in the repo.

Please rebase onto main and push. The auto-update workflow only touches branches whose authors enabled auto-merge, so it deliberately won't rewrite yours. A rebase should turn the checks green without any change to your code.

The review feedback above is separate and still stands.

@Adolanium
Adolanium force-pushed the feat/1771-music-control-action-state branch from 9a32603 to 33aa7da Compare July 27, 2026 03:48
@Adolanium

Adolanium commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed in 4132c62 (squashed, rebased onto main).

  • Replaced the useMemo + Symbol token with a guildRef updated in useLayoutEffect on guildId. In-flight commands capture the guild they started on and compare against the ref, so correctness does not depend on memo identity.
  • clearError is a stable useCallback.
  • Spinner selection is FIFO (oldest actionKey) when more than one command is in flight.
  • Global disable while any action is pending is intentional: concurrent mutations on the same queue race too easily. Comment in the hook calls that out. Happy to rename the prop if you want the name to match the behaviour more closely.

@Adolanium
Adolanium force-pushed the feat/1771-music-control-action-state branch from 33aa7da to 4132c62 Compare July 27, 2026 03:51

@LucasSantana-Dev LucasSantana-Dev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review: changes-required

The cross-guild race guard is unsound for the navigate-away-and-back case, the PR's own new regression test for that exact case appears to fail against the implementation, and frontend CI never ran on this branch to catch it.

P1 — stale rollback can overwrite a fresh visit: useMusicPlayer.ts (the isCurrentGuild guard in sendCommand) compares guild IDs by string equality, so it cannot distinguish a re-visit to the same guild. Trace: on guild A, a command's optimistic update rejects and the rollback api.music.getState() is slow; user switches A → B → A; the layout effect resets state and guildRef.current is 'A' again; when the stale rollback resolves, isCurrentGuild() is true, so setState(staleData) and setError('volume: command failed') run, overwriting the fresh state and showing a phantom error. This is exactly what the new test does not let an old guild rollback update a new visit to that guild asserts against (expect(state.volume).toBe(35), expect(error).toBeNull()), so that test appears to fail as written. Fix: gate on activeCommandsRef.current.has(commandId) (the map is cleared per guild change in the useLayoutEffect, so membership means "issued during the current visit"), or use a per-visit epoch counter.

P1 — CI never executed the frontend tests: the check rollup on 4132c622 shows only label/cubic/CodeRabbit/GitGuardian/Socket; the CI/CD Pipeline frontend job (which runs npm run test --workspace=packages/frontend) is absent, likely pending maintainer approval for an external contributor. ~300 lines of new/changed tests are unverified, and per the finding above the branch is likely red. A maintainer should approve the workflow run before merge.

P2 — tested path is not the shipped path: PlaybackControls.tsx is referenced only by itself and its test (repo-wide grep); the controls users interact with are the inline duplicates in Music.tsx's NowPlayingHero. The page-level gating (hero spinners, controlsEnabled wiring) got no behavioral tests. Fix: render PlaybackControls from the page, or add page-level assertions for disconnected/busy states.

P3 (nits): hardcoded English dismiss label and new Error('Player is not connected') on a page otherwise fully localized through t(); music.progressMayBeOutdated* locale keys added but referenced nowhere; pendingAction?: string | null is an untyped string across the hook/component boundary — a union of the action keys would catch mismatches.

What's good: the rollback path now awaits the queue refresh and surfaces Queue refresh failed: ... instead of fire-and-forget; the FIFO pendingAction recompute from the insertion-ordered Map is correct for both completion orders (and both are tested); gating is thorough at the interaction layer (spacebar, drag/drop, remove, clear all respect disabled, with aria-busy/aria-disabled).

…trols

Show a spinner on the control in flight and disable controls while
disconnected or busy. Guard in-flight commands with a guildId ref so guild
switches drop stale results without relying on useMemo identity. Stable
clearError, FIFO spinner selection, intentional global lockout while any
command is pending.
@Adolanium
Adolanium force-pushed the feat/1771-music-control-action-state branch from 4132c62 to c0445b4 Compare July 27, 2026 16:17
@Adolanium

Copy link
Copy Markdown
Contributor Author

Addressed the changes-requested review.

P1 — visit-scoped guard: in-flight results now gate on activeCommandsRef.current.has(commandId). The map is cleared on every guild change, so A→B→A cannot revive a stale rollback. The existing does not let an old guild rollback update a new visit to that guild test is green.

Also:

  • MusicActionKey union for pendingAction across hook/component boundary
  • Localized dismiss / not-connected strings
  • Dropped the unused staleness locale keys that had leaked into this PR
  • Page-level tests for disconnected and busy hero controls (the path users actually hit)
  • Global lockout stays intentional: spinner is per-action, disable is all-or-nothing so concurrent queue mutations cannot race

Rebased onto main. 62 frontend music tests green locally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/frontend/src/pages/Music.tsx (1)

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

Duplicate disabled-check logic instead of reusing controlsEnabled.

handlePlayPause and handleRepeatCycle re-derive !player.isConnected || player.isLoading inline, while every other handler in this component (onPrevious, onSkip, onShuffle, onVolumeChange) uses the already-computed controlsEnabled. If the definition of controlsEnabled changes later, these two call sites are easy to miss.

♻️ Proposed fix
     const handlePlayPause = useCallback(() => {
-        if (!player.isConnected || player.isLoading) return
+        if (!controlsEnabled) return
         if (player.state.isPlaying) player.pause()
         else player.resume()
-    }, [player])
+    }, [player, controlsEnabled])

     const handleRepeatCycle = useCallback(() => {
-        if (!player.isConnected || player.isLoading) return
+        if (!controlsEnabled) return
         const modes: Array<'off' | 'track' | 'queue' | 'autoplay'> = [
             'off',
             'track',
             'queue',
             'autoplay',
         ]
         const idx = modes.indexOf(player.state.repeatMode)
         player.setRepeatMode(modes[(idx + 1) % modes.length])
-    }, [player])
+    }, [player, controlsEnabled])
🤖 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 `@packages/frontend/src/pages/Music.tsx` around lines 34 - 50, Update
handlePlayPause and handleRepeatCycle to reuse the existing controlsEnabled
value for their early-return checks instead of duplicating player.isConnected
and player.isLoading logic, while preserving the current disabled behavior and
callback dependencies.
packages/frontend/src/components/Music/PlaybackControls.tsx (2)

68-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Toolbar aria-disabled ignores pendingAction.

aria-disabled={!isConnected} only reflects connection state, even though pendingAction also globally locks every control (canPlay/canTrack/canAux all require !pendingAction). Individual buttons already carry disabled, so impact is limited, but the toolbar-level state could be made consistent.

♻️ Suggested tweak
-            aria-disabled={!isConnected}
+            aria-disabled={!isConnected || Boolean(pendingAction)}
🤖 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 `@packages/frontend/src/components/Music/PlaybackControls.tsx` around lines 68
- 73, Update the playback controls toolbar’s aria-disabled value to reflect both
disconnected and pending-action states, matching the locking conditions used by
canPlay, canTrack, and canAux while preserving the existing individual button
disabled behavior.

31-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared PlaybackControls component in NowPlayingHero.

Music.tsx renders its own control row inside NowPlayingHero and does not import PlaybackControls, so the new component supports isConnected/pendingAction only in definition. Point NowPlayingHero at the shared component instead of maintaining duplicate control markup and icon/busy logic.

🤖 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 `@packages/frontend/src/components/Music/PlaybackControls.tsx` around lines 31
- 49, Update NowPlayingHero in Music.tsx to render the shared PlaybackControls
component instead of its duplicated control row, icon markup, and busy-state
logic. Import PlaybackControls and pass the existing playback state,
isConnected, pendingAction, and action callbacks through its defined props;
remove the redundant local controls implementation.
packages/frontend/src/hooks/useMusicCommands.test.ts (1)

105-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Incomplete actionKey coverage for play/skip/shuffle/move/import.

Assertions verify the new sendCommand actionKey argument for pause/resume/stop/volume/repeat/seek/remove/clear, but not for play, skip, shuffle, moveTrack, or importPlaylist. Since this file is the dedicated test surface for the new action-key contract, consider adding the missing sendCommand assertions (with actionKey) for completeness.

🤖 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 `@packages/frontend/src/hooks/useMusicCommands.test.ts` around lines 105 - 165,
The useMusicCommands test assertions cover actionKey values for several commands
but omit play, skip, shuffle, moveTrack, and importPlaylist. Extend the relevant
test cases around sendCommand to assert each command is called with its expected
payload and actionKey, matching the existing assertion style.
packages/frontend/src/components/Music/QueueList.tsx (1)

222-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Grip handle keeps cursor-grab styling even when disabled.

The item container now gets opacity-60 and draggable={!disabled}, but the adjacent GripVertical handle still always shows cursor-grab on hover, implying draggability even though dragging is disabled.

🤖 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 `@packages/frontend/src/components/Music/QueueList.tsx` around lines 222 - 229,
Update the GripVertical handle styling in the queue item component to
conditionally remove its cursor-grab hover behavior when disabled, matching the
container’s disabled state and draggable={!disabled} behavior; preserve the grab
cursor for enabled items.
🤖 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.

Inline comments:
In `@packages/frontend/src/pages/Music.tsx`:
- Around line 380-386: Update the disconnected, not-busy message in the
controls-disabled branch to use the existing music.playerNotConnected
translation key instead of music.notConnectedToVoiceChannel, preserving the
commandInProgress key for busy states. Add or extend a test for the disconnected
and not-busy case if the relevant test setup is available.

---

Nitpick comments:
In `@packages/frontend/src/components/Music/PlaybackControls.tsx`:
- Around line 68-73: Update the playback controls toolbar’s aria-disabled value
to reflect both disconnected and pending-action states, matching the locking
conditions used by canPlay, canTrack, and canAux while preserving the existing
individual button disabled behavior.
- Around line 31-49: Update NowPlayingHero in Music.tsx to render the shared
PlaybackControls component instead of its duplicated control row, icon markup,
and busy-state logic. Import PlaybackControls and pass the existing playback
state, isConnected, pendingAction, and action callbacks through its defined
props; remove the redundant local controls implementation.

In `@packages/frontend/src/components/Music/QueueList.tsx`:
- Around line 222-229: Update the GripVertical handle styling in the queue item
component to conditionally remove its cursor-grab hover behavior when disabled,
matching the container’s disabled state and draggable={!disabled} behavior;
preserve the grab cursor for enabled items.

In `@packages/frontend/src/hooks/useMusicCommands.test.ts`:
- Around line 105-165: The useMusicCommands test assertions cover actionKey
values for several commands but omit play, skip, shuffle, moveTrack, and
importPlaylist. Extend the relevant test cases around sendCommand to assert each
command is called with its expected payload and actionKey, matching the existing
assertion style.

In `@packages/frontend/src/pages/Music.tsx`:
- Around line 34-50: Update handlePlayPause and handleRepeatCycle to reuse the
existing controlsEnabled value for their early-return checks instead of
duplicating player.isConnected and player.isLoading logic, while preserving the
current disabled behavior and callback dependencies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c7e35f4e-e121-4ff8-a082-1d0a40528c12

📥 Commits

Reviewing files that changed from the base of the PR and between 0528f66 and c0445b4.

📒 Files selected for processing (13)
  • packages/frontend/src/components/Music/ImportPlaylist.tsx
  • packages/frontend/src/components/Music/PlaybackControls.test.tsx
  • packages/frontend/src/components/Music/PlaybackControls.tsx
  • packages/frontend/src/components/Music/QueueList.tsx
  • packages/frontend/src/components/Music/SearchBar.tsx
  • packages/frontend/src/hooks/useMusicCommands.test.ts
  • packages/frontend/src/hooks/useMusicCommands.ts
  • packages/frontend/src/hooks/useMusicPlayer.test.ts
  • packages/frontend/src/hooks/useMusicPlayer.ts
  • packages/frontend/src/locales/en.json
  • packages/frontend/src/locales/pt-BR.json
  • packages/frontend/src/pages/Music.test.tsx
  • packages/frontend/src/pages/Music.tsx

Comment thread packages/frontend/src/pages/Music.tsx
LucasSantana-Dev added a commit that referenced this pull request Jul 27, 2026
## Summary

Two structural failures block every fork PR's required checks (seen on
#1863, #1864, #1865, #1866, #1867, #1674 after their CI was approved):

- **SonarCloud Scan (required) hard-fails on forks**: fork PRs get no
secrets, so `SONAR_TOKEN` is never present and the token-policy step
exits 1. Now the sonar job is skipped for fork PRs (a skipped required
check counts as passing). Same pattern deploy-staging already uses.
- **danger 403s on forks**: `review-tools.yml` ran on `pull_request`,
where the fork token is forced read-only and the comment POST fails with
403. Switched to `pull_request_target`; the reusable workflow checks out
and executes base-repo code only (documented in the file header, same
safety rule as the other target workflows).

## Test plan
- [x] actionlint clean on both files
- [ ] Next push to an external contributor PR: SonarCloud Scan shows
skipped, danger posts its comment

After merge I will update the seven open contributor branches to main so
they pick this up.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Unblocks fork PRs by fixing CI gates for SonarCloud and `danger`. Fork
PRs now pass required checks without secrets and get review comments.

- Bug Fixes
- Skip SonarCloud Scan on fork PRs to avoid failing when `SONAR_TOKEN`
is unavailable (skipped required check counts as passing).
- Run review tools on `pull_request_target` so `danger` can comment on
forks; workflow executes base-repo code only.

<sup>Written for commit 316f567.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/LucasSantana-Dev/Lucky/pull/1898?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Warnings
⚠️

Big PR — 1088 lines changed across 13 files. Consider splitting into smaller, reviewable chunks.

⚠️

User-facing change without a CHANGELOG.md update. Add a line under ## [Unreleased] if this should appear in release notes. (Or apply the skip-changelog label if this PR does not affect end users.)

Generated by 🚫 dangerJS against 5b4f8af

@LucasSantana-Dev
LucasSantana-Dev dismissed their stale review July 27, 2026 20:15

All findings addressed and verified in the follow-up commits. Thanks for the quick turnaround.

@LucasSantana-Dev

Copy link
Copy Markdown
Owner

A note from the maintainer side: sorry this PR waited as long as it did for a proper review, and sorry for the rounds of branch updates and re-running checks today. The churn was on our side, not yours. Also thank you for the quick turnaround on the review feedback: every finding was addressed correctly, and I have dismissed the earlier change request.

Your PRs exposed real gaps in how this repo handled external contributions: CI runs sat in a silent approval queue, some gates could never pass on fork PRs (SonarCloud, danger), and the team had no notification when external PRs arrived. Those are all fixed as of today:

  • CI auto-approves for returning contributors, no more waiting on a manual click.
  • All gates now pass on fork PRs (SonarCloud skips gracefully without a token, danger runs and comments correctly).
  • The team gets notified the moment an external PR is opened.

Your branch is up to date and the full suite is green. Thanks for the patience and for the contribution. External contributors are very welcome here.

@LucasSantana-Dev
LucasSantana-Dev merged commit 2dda60f into LucasSantana-Dev:main Jul 27, 2026
41 checks passed
LucasSantana-Dev added a commit that referenced this pull request Jul 27, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>2.38.0</summary>

##
[2.38.0](v2.37.3...v2.38.0)
(2026-07-27)


### Features

* **bot:** add /ticket-setup for support category and agent role
([#1863](#1863))
([3f4af39](3f4af39))
* **frontend:** per-action loading and connection gating on music
controls
([#1866](#1866))
([2dda60f](2dda60f))
* **frontend:** show stale progress when music SSE lags
([#1867](#1867))
([4952e73](4952e73))
* **music:** surface recommendationReason in nowplaying and queue
([#1864](#1864))
([960fd62](960fd62))
* **ops:** blue/green zero-downtime deploys — Phase 1 web tier
([#1786](#1786))
([f5f7597](f5f7597))


### Bug Fixes

* **docker:** make compose stack boot from a fresh .env
([#1674](#1674))
([babe0ef](babe0ef))
* **docker:** treat an empty db password as missing in compose guards
([#1881](#1881))
([718c0ad](718c0ad))
* **frontend:** make landing page usable at mobile widths
([#1865](#1865))
([6190350](6190350))
* **frontend:** stop hero grid columns overflowing on narrow viewports
([#1874](#1874))
([ce5cea0](ce5cea0))
* **invite:** add /invite where cloudflare pages reads it
([#1895](#1895))
([0528f66](0528f66))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(frontend): per-action loading/error state and connection-status gating on music controls

2 participants