removeBotStatus (botRemoved WS frame or admin deleting the scoped bot) left
scopedBotId dangling: isScoped stayed true, displayedBots went empty, and activeBot
silently fell back to bots[0], locking the UI onto a phantom bot. Clear the scope
when the scoped bot disappears.
Capability flags (player.control/player.queue/bot.manage/platform.auth/quality) + per-member bot allow-list, layered under the existing member role; admin is super-user. Backend-enforced via requirePermission/requireBotAccess; existing members backfilled to full on upgrade, new members get a basic tier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
随机循环 (rloop) used true random-with-replacement, so some songs repeated constantly while others were starved (issue #70). Both random modes now draw from a shuffle bag: every song plays exactly once per cycle in random order. They differ only at cycle end — 随机 (random) stops, 随机循环 (rloop) reshuffles and continues, excluding the just-played song from the first pick of the new cycle to avoid a back-to-back repeat across the boundary. Songs added mid-cycle stay eligible within the current cycle.
随机's visible behavior is unchanged (it already avoided in-cycle repeats); the two branches now share one selection path. Adds shuffle-bag tests (per-cycle permutation, even distribution, no cross-boundary repeat, mid-cycle add).
Closes#70
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
no-referrer downgraded the Origin header to the literal "null" on same-origin non-GET requests (per the WHATWG Fetch "Append a request Origin header" algorithm), which the /api/* csrfOriginCheck then rejected with 403 "bad origin" — silently breaking QR login, cookie save, and every other WebUI POST/PUT/DELETE/PATCH (playback, bot management, user admin). /api/session/* was unaffected because it mounts before the CSRF gate, which is why WebUI login still worked.
same-origin keeps the real Origin on same-origin requests (CSRF passes) while still sending no Referer cross-origin, so B站/NetEase/QQ CDN cover thumbnails keep loading. Adds referrer-policy.test.ts pinning the policy and a csrf.test.ts case for the Origin: "null" rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bilibili's CDN (i*.hdslb.com) returns 403 with `x-error-info:
RefererWhite` for image requests whose Referer is not on their
whitelist. `CoverArt.vue` already sets `referrerpolicy="no-referrer"`
on its `<img>` tag, BUT the `.cover-shadow` div renders the same URL
as a CSS `background-image`, which ignores the img attribute and uses
the document default policy (`strict-origin-when-cross-origin` in
modern Firefox/Chrome) — that sends `Referer: http://localhost:3000/`
and triggers the block.
Setting `<meta name="referrer" content="no-referrer">` in index.html
applies no-referrer site-wide: covers <img> tags, CSS background-image
fetches, and anywhere else CDNs check referer. Doesn't affect our
/api/* CSRF middleware because that uses Origin (still sent by the
browser), not Referer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous commit misidentified the second bug. Reverting the
Settings.vue `resize: vertical` → `resize: none` change — that
wasn't the issue.
Real fix: `.daily-card` (used by B站热门 and 每日推荐 sections in
Home.vue) is a CSS Grid cell with default `min-width: auto`, which
refuses to shrink below its content. A long Bilibili video title
inside `.daily-name` expanded the cell past its 1fr column, breaking
the 6-column grid and creating empty/black space on the right. The
existing `text-overflow: ellipsis` on `.daily-name` couldn't engage.
Adding `min-width: 0` to `.daily-card` lets the cell shrink to the
1fr grid track size, and the ellipsis truncation now works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Player.vue: wrap artist text in a span with ellipsis. The previous
text node sat directly inside the flex `.song-artist` container with
no overflow handling, so a long author name expanded the container
past its 240px parent and broke the bottom Player bar layout. Also
add `min-width: 0 + overflow: hidden` to `.song-info` and
`.song-artist`, and a `:title` attribute for the full text on hover.
- Settings.vue: change `resize: vertical` on the cookie textareas
to `resize: none`. The browser's resize grip rendered as a stray
black triangle at the bottom-right corner in dark theme, and
dragging it caused visual artifacts on the right edge. The
textareas keep their `rows="3"` default height.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every response now carries:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Prevents the WebUI from being embedded in a third-party iframe.
Combined with the existing CSRF Origin-host check, this closes the
last meaningful UI-redress surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>