Skip to content

feat(frontend): show stale progress when music SSE lags - #1867

Merged
LucasSantana-Dev merged 4 commits into
LucasSantana-Dev:mainfrom
Adolanium:feat/1772-progress-staleness
Jul 27, 2026
Merged

LucasSantana-Dev merged 4 commits into
LucasSantana-Dev:mainfrom
Adolanium:feat/1772-progress-staleness

Conversation

@Adolanium

@Adolanium Adolanium commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

The dashboard progress bar uses state.position from SSE. If the stream lags, the bar freezes mid-track with no indication that the position is stale.

Fix

  • useMusicPlayer: record lastStateUpdate (wall clock) on every successful SSE or REST state payload.
  • Now-playing hero: while playing, re-tick every second; if no state update for 5s, dim the bar, switch it to warning color, and show a "may be outdated" label.

Verification

  • Unit tests assert lastStateUpdate is set from SSE and initial REST state.

Checklist

  • useMusicPlayer tests for lastStateUpdate
  • CHANGELOG.md updated (if user-facing)
  • TypeScript clean for touched files

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

    • Track lastStateUpdate from SSE, initial REST, and heartbeat in useMusicPlayer. While playing, tick every second; after 45s without a stamp, dim the bar, switch to warning, and show a centered, localized “may be outdated” label without shifting timestamps.
  • Bug Fixes

    • Use shared applyState for SSE/REST and optimistic rollbacks so staleness clears on refresh; ignore malformed SSE. Server heartbeats now send data: {"type":"heartbeat"} and are treated as liveness only. Removed unused aria markup; stale notice is announced via role="status".

Written for commit 6823487. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added detection for outdated music playback progress.
    • Progress indicators are dimmed and display a warning when updates have not been received recently.
    • Heartbeat updates now refresh playback freshness without changing the current queue state.
    • Added English and Portuguese translations for the outdated-progress message.
  • Bug Fixes

    • Improved handling of malformed streaming messages without overwriting player state.

@coderabbitai

This comment was marked as low quality.

@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 3 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
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 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

@Adolanium
Adolanium force-pushed the feat/1772-progress-staleness branch from 8e5d925 to 7fb45c7 Compare July 21, 2026 03:56
@LucasSantana-Dev

Copy link
Copy Markdown
Owner

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

• VERDICT: CLEAN

No substantive concerns: state-update timestamping is straightforward, the interval effect is correctly cleaned up, dependency arrays are updated, and tests cover the new behavior. Existing swallowed .catch(() => {}) patterns are unchanged by this PR.


Head 7fb45c7. Posted by kimi-review-watch (launchd).

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

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.

@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 commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed in b53707f (squashed, rebased onto main).

  • Stream heartbeat is now a real SSE data payload ({ type: 'heartbeat' }) instead of a comment, so it reaches onmessage.
  • The client stamps lastStateUpdate on that heartbeat without touching queue state.
  • STALE_AFTER_MS is 45s (1.5x the 30s server interval) so a healthy idle connection no longer flips to warning after 5s.
  • Dropped the roleless aria-label on the progress div. Visible text still covers the stale case.
  • Stale notice is absolutely centered so the position/duration timestamps stay at the edges.

@Adolanium
Adolanium force-pushed the feat/1772-progress-staleness branch from 51317bf to b53707f 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: 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.
@Adolanium
Adolanium force-pushed the feat/1772-progress-staleness branch from b53707f to cb93285 Compare July 27, 2026 16:18
@Adolanium

Copy link
Copy Markdown
Contributor Author

Addressed the remaining nits.

  • PR body now matches the code (STALE_AFTER_MS = 45_000, not 5s).
  • Dropped the unused progressMayBeOutdatedAria / title keys.
  • Stale notice uses role="status" so assistive tech picks it up without a dead title on a non-interactive div.
  • Heartbeat stamping stays as connection liveness (per the code comment). Position freshness without heartbeats would need a different signal.

Rebased onto main.

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
⚠️

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 6823487

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

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.

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2dda60f and 6823487.

📒 Files selected for processing (6)
  • packages/backend/src/routes/music/stateRoutes.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.tsx

Comment thread packages/frontend/src/hooks/useMusicPlayer.ts
Comment thread packages/frontend/src/hooks/useMusicPlayer.ts
@LucasSantana-Dev
LucasSantana-Dev merged commit 4952e73 into LucasSantana-Dev:main Jul 27, 2026
46 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): staleness indicator on music progress bar when SSE lags

2 participants