Repository navigation
feat(frontend): show stale progress when music SSE lags - #1867
LucasSantana-Dev merged 4 commits into
Conversation
This comment was marked as low quality.
This comment was marked as low quality.
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Auto-approved: Display-only stale indicator for progress bar when SSE lags; no behavioral or operational changes, bounded frontend enhancement verified by tests.
Re-trigger cubic
8e5d925 to
7fb45c7
Compare
Kimi review (
|
There was a problem hiding this comment.
I like the shape of this. Funnelling both the SSE and REST paths through a single applyState so there's exactly one place that stamps the timestamp is the right call, and gating the re-tick interval on state.isPlaying avoids leaving a timer running forever.
But I went and read the server side, and I think the warning will fire constantly on healthy connections. I'd like to sort that out before this goes in.
The staleness signal will show up when nothing is wrong
lastStateUpdate only advances when a payload parses as JSON state. Everything else lands in the catch:
try {
applyState(JSON.parse(event.data))
} catch {
/* heartbeat or malformed data */
}So it depends entirely on how often the server pushes real state. Looking at packages/backend/src/routes/music/stateRoutes.ts:
- it writes the full state once on connect (line 27);
- after that the only periodic write is
res.write(': heartbeat\n\n')on a 30 second interval (lines 44-59); - actual state payloads only go out when something broadcasts to
sseClients, i.e. on a real change.
Two problems with that. The heartbeat is an SSE comment (: prefix), so it carries no data: field and won't even reach onmessage, let alone refresh the timestamp. And the interval is 30s against STALE_AFTER_MS = 5_000, so it's six times too slow to help even if it did.
Net effect: start a track, don't touch anything, and 5 seconds later the bar dims to opacity-50, flips to bg-lucky-warning and says "may be outdated" while the connection is perfectly fine. It then stays that way until someone edits the queue. That's the opposite of the signal you want, and it'll teach people to ignore the warning.
A few ways out, your call which fits best:
- derive staleness from
isConnected(the hook already tracks it) rather than payload recency; - give the heartbeat a real
data:payload so it parses and stamps the timestamp, which also makes it a genuine liveness signal; - or keep the recency approach but set the threshold above the real push interval, which means >30s and coupling this constant to the server's heartbeat.
I'd lean toward the second, since a heartbeat that the client can't observe isn't doing much for us today anyway.
Smaller thing: the aria-label doesn't do anything
<div className='flex-1 h-1 ...' aria-label={isStale ? t('music.progressMayBeOutdatedAria') : undefined}>A div has no implicit role, and aria-label on a roleless generic element is ignored by most screen readers. It's also redundant, since you already add the same information as visible text just below, which assistive tech picks up for free. Either drop it, or give the element role='progressbar' with aria-valuenow/aria-valuemin/aria-valuemax — which is what this control actually is, and would be a real improvement beyond the scope of this PR. Right now it's just dead markup.
Nit: putting the label between the position and duration spans turns that row from a two-item justify-between into three, so the timestamps stop sitting at the extremes and jump inward whenever the warning appears. Reserving the space or absolutely positioning the notice would avoid the shift.
Merge order: #1866 rewrites the same useMusicPlayer.ts region and also touches pages/Music.tsx and both locale files; #1864 touches Music.tsx too. I'll sequence the three rather than letting them collide.
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.
|
Heads-up: the CI blockers I mentioned are fixed on What that clears:
Please rebase onto The review feedback above is separate and still stands. |
1bbe6a6 to
51317bf
Compare
|
Addressed in b53707f (squashed, rebased onto main).
|
51317bf to
b53707f
Compare
LucasSantana-Dev
left a comment
There was a problem hiding this comment.
Review: approve with nits
Correct, well-scoped, and fixes a real protocol bug. Remaining items are a misleading PR description, one semantic question, and minor polish.
P2 — PR body vs code: the description says "if no state update for 5s, dim the bar", but STALE_AFTER_MS = 45_000 (Music.tsx:144). The code is the right call (5s would flap against the 30s server heartbeat), so the description is stale, not the code. Please update the PR body so reviewers and future git blame readers aren't chasing a 5s threshold that never existed.
P2 — question on the staleness definition: heartbeats stamp lastStateUpdate, so the stale indicator only fires when BOTH state broadcasts and heartbeats stop for 45s. If the #1772 failure mode is the backend stopping state publishes while the SSE connection (and its heartbeats) stays alive, the bar stays frozen with no indicator. Fine if the intended definition is "connection liveness" (per the code comment); if it's position-data freshness, heartbeats mask it.
P3 (nits): progressMayBeOutdatedAria is added to both locales but never referenced (dead key — drop it or wire it up); the stale flip is conveyed via title on a non-interactive div, which is keyboard/touch-inaccessible and not announced — the ConnectionBadge pattern (role='status') is the in-repo precedent; hook-level tests cover lastStateUpdate but nothing exercises the NowPlayingHero stale UI.
What's good: the protocol fix is real and correct — SSE comment lines (: heartbeat) never fire onmessage, so the new data: {"type":"heartbeat"} payload plus the payload?.type === 'heartbeat' branch is the only way the client can observe liveness, and QueueState has no type field so there's no collision with real broadcasts. Optimistic updates correctly bypass applyState, lastStateUpdate resets on guild change, and the 1s re-tick interval only runs while playing and is cleaned up.
Track lastStateUpdate from SSE/REST state and from real heartbeat data payloads. While playing, mark the bar stale after 45s without a stamp (1.5x the 30s server heartbeat). Drop dead aria-label markup and keep timestamps from shifting when the outdated notice shows.
b53707f to
cb93285
Compare
|
Addressed the remaining nits.
Rebased onto main. |
## 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. -->
|
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. 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:
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. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/hooks/useMusicPlayer.ts`:
- Around line 99-107: Validate parsed WebSocket payloads in useMusicPlayer
before applyState, accepting only heartbeat messages or complete QueueState
objects and ignoring all other valid JSON values. In
packages/frontend/src/hooks/useMusicPlayer.ts lines 99-107, add the runtime
validation while preserving heartbeat handling; in
packages/frontend/src/hooks/useMusicPlayer.test.ts lines 200-204, add a
syntactically valid invalid payload such as {} and assert the previous queue
state remains unchanged.
- Around line 187-189: Update the successful optimistic command path in
useMusicPlayer so it applies the returned state through the same
liveness-stamping mechanism as the rollback refresh, rather than only calling
setState. Ensure the existing isLiveCommand guard and stale-progress behavior
remain intact while refreshing freshness immediately after a successful command.
🪄 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: c474b2f6-f2b5-4991-a562-9762a625a4da
📒 Files selected for processing (6)
packages/backend/src/routes/music/stateRoutes.tspackages/frontend/src/hooks/useMusicPlayer.test.tspackages/frontend/src/hooks/useMusicPlayer.tspackages/frontend/src/locales/en.jsonpackages/frontend/src/locales/pt-BR.jsonpackages/frontend/src/pages/Music.tsx
🤖 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).
Description
The dashboard progress bar uses
state.positionfrom SSE. If the stream lags, the bar freezes mid-track with no indication that the position is stale.Fix
useMusicPlayer: recordlastStateUpdate(wall clock) on every successful SSE or REST state payload.Verification
lastStateUpdateis set from SSE and initial REST state.Checklist
Destructive / irreversible interaction (Tier A)
Not applicable. Display-only.
Feature-removal sweep
Not applicable.
Fixes #1772
Summary by cubic
Show a stale indicator on the now-playing progress bar when SSE updates or heartbeats lag. Marks progress outdated after 45s and announces it via role="status", addressing Linear #1772.
New Features
Bug Fixes
Written for commit 6823487. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes