Commit Graph

770 Commits

Author SHA1 Message Date
chevron7 e110b6d77e fix(audiobook): checkpoint progress on a real timer, not the throttled timeupdate
Progress was saved every 30s off the "timeupdate" event, but iOS throttles and
suspends that event when the PWA is backgrounded (screen off) -- the normal way
people listen to audiobooks. So a long screen-off session was never
checkpointed, and an app update (or crash) reverted to the moment the screen was
locked. The saved data was never lost; it just stopped advancing in the
background.

Replace the timeupdate-driven save with a 15s wall-clock setInterval that runs
while playing (started on "play", stopped on "pause"/"ended"), independent of
the media event iOS throttles. saveAudiobookProgress already de-dupes an
unchanged position and the tick is gated on isPlaying(), so paused/stalled ticks
are no-ops. Applies to podcasts too.
2026-06-08 07:23:36 -05:00
chevron7 b1daaaccf9 fix(ios): auto-resume audio after an OS interruption + repair the trace upload
Playback that an iOS interruption (call/notification) pauses now resumes when
the interruption ends, the behaviour other apps have.

- Track play intent separately from audio.paused: set on play/tryResume/
  swapAndPlay, cleared only by explicit pause/stop/cleanup. The native "pause"
  event an interruption fires does NOT clear it.
- The AudioContext statechange listener resumes on an interrupted -> running
  transition when intent is set and the element is paused. Gated hard: only that
  transition (not the initial bridge resume or a background suspend), never
  within 1.5s of an audio-route change (the v1.7.12 unplug-to-speaker
  regression), and never while a stall reload owns the resume.
- Repair the trace auto-upload: it POSTed to a requireAuth route without the
  Bearer token and swallowed the 401, so no iOS trace was ever captured. It now
  sends the token, so device testing finally yields real event data.

Reviewed by Opus/Sonnet passes. Known limits to confirm on-device: only fires if
WebKit returns the context to "running" (a context stuck "interrupted" -- the
force-quit symptom -- is not addressed here).
2026-06-06 12:29:58 -05:00
chevron7 b490ae771b fix(podcast): bound preview fetch so a slow feed can't hang the UI (#168)
The preview hook only stops spinning when the request resolves or rejects. The
RSS parse had a 30s timeout and the client had none, so a slow/dead feed left
the spinner up 30s+ with no error -- the "infinite loading" in #168 (the v1.7.13
fix only handled the error path, not the hang).

- Frontend: previewPodcast aborts after 20s, surfacing the existing error UI.
- Backend: the two preview RSS parses are bounded to 8s (non-critical, already
  falls through to partial data), so a slow feed returns the podcast quickly.
2026-06-06 12:29:58 -05:00
chevron7 fb25b0823e chore(release): v1.8.1
Optional slskd backend for Soulseek (#205, by gossip31) and the audio-analyzer
crash-loop fix (#204, by gossip31, plus a retry-count backstop).
v1.8.1
2026-06-05 20:19:05 -05:00
Silly Susan b6046a3601 feat(soulseek): configurable slskd backend via soulseekMode (#205)
Adds a soulseekMode (p2p|slskd) setting to route Soulseek through an external slskd REST instance, so slskd mode needs no Kima-side Soulseek credentials. Includes the review fixes: https transport, reconnect on backend change, slskdUrl validation, mode-aware connection test, queue position, bounded size cache. Closes #164. By gossip31.
2026-06-05 20:02:39 -05:00
chevron7 8fb792d213 fix(audio-analyzer): count worker crashes as retries so any crash quarantines
Complements #204 (gossip31's pre-decode ffmpeg gate). The pre-decode gate
catches corrupt files that SIGSEGV the decoder, but a worker that dies on any
other native fault (e.g. an Essentia analysis crash after a clean decode) still
left the track in 'processing' and got re-queued by the stale-cleanup sweep
WITHOUT incrementing analysisRetryCount -- so it could loop forever and never
reach the mark-failed/quarantine path.

_cleanup_stale_processing now increments analysisRetryCount when it resets a
crashed track, and marks tracks that have passed MAX_RETRIES as 'failed' (with a
reason) so they quarantine and surface in the permanently-failed accounting
instead of sitting in 'processing' limbo. Defense in depth behind the gate.
2026-06-05 14:58:31 -05:00
Silly Susan be1519822d fix(audio-analyzer): pre-decode ffmpeg gate to stop Essentia segfault crash-loop (#204)
Adds an ffmpeg integrity probe before MonoLoader so corrupt files that SIGSEGV Essentia become a normal load failure (and flow into the existing retry/quarantine) instead of crash-looping the worker. By gossip31.
2026-06-05 14:56:39 -05:00
chevron7 fdc7b3cfa1 fix(build): harden ML model downloads against curl timeouts
The model-download layer failed three recent builds (06-04 x2, 06-05) with curl
exit 28: --max-time caps the whole operation including retries, so a slow
GitHub-runner transfer trips it, and --retry does not retry a timeout. Switched
all 12 downloads to --retry-all-errors (retries timeouts/transient HTTP),
stall-based abort (--speed-limit 1024 --speed-time 60) instead of a hard total
cap, 5 retries, and -f so a bad HTTP response fails fast instead of saving a
corrupt model. The transformers==5.8.1 pin is unaffected and confirmed building.
v1.8.0
2026-06-05 10:34:48 -05:00
chevron7 545a488c67 docs(changelog): drop refuted Redis-memory caveat from #197
The reporter's redis INFO shows a healthy instance (33MB used, no maxmemory
limit, noeviction, zero evictions/rejected connections), ruling out the
memory-pressure hypothesis. The connection-readiness race the fix addresses is
the actual cause, so the hedge is removed.
2026-06-05 08:51:19 -05:00
chevron7 eb5883f13d chore(release): v1.8.0
iOS playback reliability rebuilt from a stable baseline, Vibe search Redis
hardening (#197), Discover Weekly correctness Phase 1, transformers pin, and iOS
audio diagnostics groundwork. Folds the never-released 1.7.16 into 1.8.0.
2026-06-04 19:38:26 -05:00
chevron7 12ebc8c9a5 fix(vibe): harden text-embedding bridge against busy/reconnecting Redis (#197)
The first #197 fix only hardened the pub/sub subscriber; a 3-model review panel
found it incomplete. This closes the rest:

- publish() now runs on a dedicated soft-options connection (enableOfflineQueue,
  infinite retries) instead of the strict shared client -- that strict publish
  was still throwing the same "Stream isn't writeable" error under load.
- subscriber lifecycle: terminal "end" drops the cache, a failed psubscribe
  disconnects the half-open socket instead of leaking it; transient drops
  self-heal via auto-reconnect.
- both subscribe and publish are time-bounded so an unreachable Redis fails the
  request instead of hanging indefinitely.
- analyzer failures ({success:false, embedding:null}, no error field) are now
  rejected cleanly instead of passing null into the pgvector cast (500).
- the analyzer publishes a failure response on internal exceptions so the caller
  fails fast instead of waiting out the full 15s timeout.

Reviewed by Opus/Sonnet/Haiku panels twice (original confirmed INCOMPLETE,
rewrite SHIP-WITH-CHANGES); surviving findings applied, two rejected with reason
(no publisher churn on transient error; keep setMaxListeners(0) to not re-trigger
the warning flood).

The reporter's 200k-track failure may also involve Redis memory pressure or
Python-analyzer saturation, which this makes tolerable but does not itself
resolve -- pending their redis INFO.
2026-06-04 17:42:40 -05:00
chevron7 a9cf54d0f2 fix(build): pin transformers==5.8.1 to stop nightly breaking against torch 2.5.1
The scheduled nightly off main failed (2026-06-04, and 2026-06-02 the same
way): a transformers release newer than 5.8.1 references torch.float8_e8m0fnu
(a dtype added in torch 2.7) at import time, so `from transformers import
BertModel` crashes against the pinned torch==2.5.1 and the Dockerfile
fail-fast check exits 1. The unpinned `transformers>=4.30.0` let pip resolve
to that bad release. Recent branch builds only passed because BuildKit reused
a cached pip layer from before it published.

Pinned to 5.8.1 -- the exact version running in prod against torch 2.5.1+cpu.
Bump only alongside a torch bump.
2026-06-04 08:35:50 -05:00
chevron7 4fa327aa8e fix(ios): re-claim audio session on resume + AudioContext statechange recovery
On the clean 439fa68 bridge baseline (band-aids reverted), add the two
high-confidence stability fixes the resume bug actually needs:

- setAudioSessionPlayback gains a `force` arg; play() now re-claims the
  iOS "playback" session category on every explicit resume, not just the
  first. The one-time latch was why iOS, after an earbud/Control-Center
  interruption, left the session with whatever app grabbed it (a
  sleep-sounds app started playing through it).
- A statechange listener on the bridge AudioContext re-claims the session
  when the OS ends an interruption and the context returns to running. It
  never calls play() -- auto-resume on a route change is the v1.7.12
  earbud-unplug-to-speaker regression.

Reviewed by two independent passes; their findings fixed here: play() now
actually passes force=true (the reclaim was a no-op without it); the
statechange listener + AudioContext are torn down in destroy() (no leak);
em-dash normalized.

Deliberately NOT re-adding the silent-playback watchdog (part of the
reverted band-aid stack) -- the debug instrumentation will show whether an
interrupted-context resume is still silent, and any further recovery will
be a minimal targeted fix on evidence, not another speculative layer.
2026-06-03 20:06:17 -05:00
chevron7 a2dc14a1b0 revert(ios): strip resume band-aids back to AudioContext bridge baseline
Reverts the daf6210 -> 7be3322 -> 1a9f6f4 cascade that piled onto the
bridge. Root regression was daf6210: it awaited setupAudioContextBridge
and bailed play()/tryResume with needs-resume whenever the context was
not "running" -- which forfeited the iOS user-gesture token AND returned
before audio.play() ever ran. So earbud/lock-screen resume went silent
or dead-ended on a Tap-to-resume prompt the lock screen cannot show, and
iOS eventually handed the audio session to another app. 7be3322 and
1a9f6f4 were band-aids on that regression.

Keeps 439fa68 (the bridge) so backgrounded/screen-off playback still
survives, and keeps the debug ring-buffer instrumentation. play() and
tryResume return to the baseline: fire the context resume in parallel,
always attempt audio.play(), preserve the gesture.
2026-06-03 18:30:04 -05:00
chevron7 a7e3a85803 debug(ios): auto-capture + auto-upload the audio ring buffer in standalone PWA
Temporary diagnostic for the earbud-resume bug: the installed iOS PWA has no URL
bar to set ?ios_debug=1 or reach /debug/ios-log, so capture is enabled
unconditionally on iOS standalone and the buffer auto-POSTs (debounced 3s) to
/api/debug/ios-log after each event burst. Revert once the resume bug is fixed.
2026-06-03 16:30:45 -05:00
chevron7 eaeb0d3588 chore: release v1.7.16
iOS earbud/MediaSession resume fix, Vibe text-search Redis subscriber fix (#197),
and Discover Weekly Phase-1 correctness rework. Bump frontend+backend to 1.7.16.
2026-06-02 23:24:37 -05:00
chevron7 81ac1b5c17 fix(vibe): text-embedding Redis subscriber survives a cold/slow connection (#197)
ensureSubscriber duplicated the parent Redis client, inheriting
enableOfflineQueue:false + maxRetriesPerRequest:0, so psubscribe threw 'Stream
isn't writeable' when the subscriber socket wasn't connected yet -- and the
rejected promise was cached, breaking vibe text search permanently until restart
(worsens with library size). The subscriber now gets its own offline queue +
retries, resets the cached promise on rejection, and drops it on 'end' so the
next request reconnects.
2026-06-02 23:23:08 -05:00
chevron7 e9e1176eb7 Merge fix/ios-earbud-resume: iOS earbud/MediaSession resume + smoke hardening (v1.7.16) 2026-06-02 23:22:06 -05:00
chevron7 3db809c3b8 Merge discovery-weekly-overhaul: Phase 1 correctness fixes (v1.7.16) 2026-06-02 23:22:06 -05:00
chevron7 6b435e4167 test(e2e): wait for playback to start before asserting transport in smoke
The smoke spec asserted the play/pause button state immediately after Play all,
racing the first audio load on a cold container (player stayed 'Not Playing').
Poll audio currentTime > 0 first. Surfaced while running the suite pre-v1.7.16.
2026-06-02 18:44:29 -05:00
chevron7 1a9f6f418a fix(ios): earbud/MediaSession resume preserves the user-gesture grant
The MediaSession 'play' action called controller.play(), which awaits the
AudioContext bridge BEFORE audio.play(). That await forfeits the iOS
user-activation token from the earbud click, so an interrupted/suspended
AudioContext never resumes -- and play() then returns (not throws) on
ctx-not-running, so the handler's reloadAndPlay() fallback never fired. Result:
earbud resume produced no audio, no native 'playing' event, no playbackState
update, and after repeated no-audio play actions iOS reassigned the audio
session to the next app.

Adds resumeFromGesture(): fires the context resume without awaiting it, calls
audio.play() synchronously in the gesture tail (mirrors swapAndPlay), and on any
rejection reloads the source to re-grab the hardware session instead of a silent
needs-resume. Wired only into the explicit MediaSession 'play' action, so it
cannot auto-resume on an ambiguous pause/route-change (the v1.7.12 earbud-unplug
-> speaker regression stays fixed). play()/tryResume()/pause/silent-watchdog
untouched. Diagnosed via 4-lens + adversary review (SHIP-AS-IS).

Requires on-device confirmation (?ios_debug=1); cannot be unit-verified.
2026-06-02 18:30:50 -05:00
chevron7 2032de9e3c Merge frontend-quality-audit -> v1.7.15 v1.7.15 2026-06-01 13:04:31 -05:00
chevron7 de21f4d862 revert(player): remove mobile mini-player gesture hint
The one-time swipe hint crowded the bottom edge above the mini-player and read
as clutter; the swipe behavior is intentional and discoverable enough without it.
Removes the hint state, markup, the markHintSeen calls (swipe behavior unchanged),
the now-unused useCallback import, and the hint-in keyframe.
2026-06-01 11:46:58 -05:00
chevron7 099a58da53 chore: release v1.7.15
Bump frontend and backend to 1.7.15. Backfill CHANGELOG with the previously
unlogged 1.7.13 (iOS audio overhaul) and 1.7.14 (#81 podcast refresh) releases,
and add 1.7.15 (frontend quality + UX overhaul, iOS backgrounded-playback fix,
desktop settings-panel fix).
2026-06-01 08:04:05 -05:00
chevron7 c715cee7b1 fix(panel): render registered settings content in the desktop UnifiedPanel
The desktop sidebar was rebuilt as UnifiedPanel but the externally-registered
settings content (discover settings gear, lyrics) was never ported -- clicking
the discover gear opened the panel to the activity feed instead of the settings,
because UnifiedPanel never read settingsContent or handled set-activity-panel-tab.
It now listens for that event, renders the registered settingsContent (which
carries its own header + back button), and resets to the feed on collapse. Fixes
discover settings and lyrics on desktop. Pre-existing since the sidebar rewrite.
2026-05-31 23:01:52 -05:00
chevron7 ea363d677e fix(a11y): bump Refine panel sort/per-page buttons to 44px touch targets
Completes the audit's deferred item -- the sort and per-page option buttons were
py-2 (~32px), below the 44px standard the rest of the sweep adopted.
2026-05-31 20:46:44 -05:00
chevron7 274f784ec8 fix(playlist): re-measure virtualizer scrollMargin on reflow
The scrollMargin useLayoutEffect only ran on [rows.length], so the offset went
stale when layout above the list reflowed (e.g. the responsive hero crossing the
md breakpoint, ~52px). Masked today by the 12-row overscan, but wrong and fragile
if overscan is tuned. Added a ResizeObserver re-measure. (ultrareview bug_003)
2026-05-31 20:10:51 -05:00
chevron7 206cd4e018 fix(a11y): focus-visible ring no longer squares off rounded elements
The global :focus-visible rule set border-radius:2px on the focused ELEMENT
(not the outline), and being unlayered it overrode every Tailwind rounded-*,
collapsing circular play buttons, pills, and rounded modals to near-square
corners on keyboard focus. Removed the line; modern browsers already round the
outline via outline-offset. (ultrareview bug_005)
2026-05-31 20:10:51 -05:00
chevron7 7be332287e fix(ios): silent-playback watchdog no longer false-pauses backgrounded playback
Regression from daf6210: the silent-playback watchdog was armed on the
song-to-song swapAndPlay transition and decided 'silent' purely from whether a
timeupdate EVENT arrived within 2.5s. iOS throttles that event when the PWA is
backgrounded, so after almost every song the watchdog wrongly paused healthy
playback and surfaced a Tap-to-resume error.

Fix: judge liveness by audio.currentTime advancement (the decode clock keeps
moving under event throttling, and is what the stall watchdog already trusts),
not the timeupdate event or AudioContext.state (which lies in both directions on
iOS). Never tear down while document is hidden -- defer to foreground. Disarm on
native playing/pause and on foreground. To keep daf6210's genuine deep-suspension
prompt (which tryResume misses because it short-circuits on !paused), add
isPlayingButContextSuspended() and prompt from handleForeground.

Affects installed iOS PWA only (isIosStandalone gate). Requires on-device
confirmation (phone backgrounded/pocketed); not reproducible at a desk.
2026-05-31 19:38:08 -05:00
chevron7 8182b8e159 fix(discover): close like-vs-cleanup race, torn deletes, cancel status, silent job-drop (Tasks 7-9)
T7: deleteRejectedAlbum and the clear endpoint do an atomic claim-then-delete
(updateMany where status=ACTIVE inside a transaction) so a concurrent /like
cannot lose its album; /like is symmetric and returns 409 on a lost claim. Multi
-step deletes are transaction-wrapped (torn-state fix), a pre-check guards files
before the out-of-tx Lidarr delete, and the owned-Album lookup is filtered to
location=DISCOVER so a same-rgMbid LIBRARY album is never deleted.
T8: cancelled batch marked failed, not completed, so /current does not treat it
as a successful empty week.
T9: /generate and the cron drop a completed/failed BullMQ job hash before
re-enqueue (silent-drop fix), and the cron enqueue takes the distributed lock.
2026-05-31 13:08:57 -05:00
chevron7 7413b93733 fix(discover): retry-unavailable routes through checkBatchCompletion so the playlist builds
The retry IIFE force-completed the batch and queued a discover-retry-unavailable
scan that scanProcessor ignores, so retried albums downloaded but never entered
the playlist. Now hands off to checkBatchCompletion (Lidarr wait, completion
scan, buildFinalPlaylist + reconcile, final status) and adds a top-level catch
that marks the batch failed on a background crash.
2026-05-31 12:16:01 -05:00
chevron7 d8b1fadfbb fix(discover): mark batch failed when playlist build transaction throws
The buildFinalPlaylist catch logged the error but never updated the batch row,
leaving it stuck in scanning until the 30-min sweep. Now sets status=failed with
a 'Playlist build failed' errorMessage (distinct from the no-tracks
short-circuit). Test asserts the catch specifically fires via that discriminator.
2026-05-31 12:16:01 -05:00
chevron7 6213d9e2eb fix(discover): bounded latest-batch read, stale flag, terminal batch-status, Monday cron (Tasks 3-4)
/current and /retry resolve the view week from the latest completed batch
(bounded, with a stale flag) so records whose weekStart drifted are no longer
invisible; /batch-status reports the last terminal batch so the client can
detect a completion it missed. Cron moves to Monday 05:00 and both cron and
manual /generate derive the BullMQ dedup key from resolveGenerationWeekStart so
the batch week and dedup key cannot diverge. Adds a supertest route test for the
data-loss fallback path.
2026-05-31 11:46:14 -05:00
chevron7 1ffe4e7d96 feat(discover): centralized week-date helper; generation tags the upcoming week
Adds lib/discoveryWeek.ts (resolveGenerationWeekStart, resolveViewWeek,
weekStartKey) as the single source of truth for week boundaries, and points
generation at it so a Sunday run tags the upcoming week instead of the ending
one. Pins TZ=UTC in jest.config so the date tests are host-independent.
2026-05-31 11:16:23 -05:00
chevron7 70d81d5198 fix(discover): pin TZ=UTC for deterministic week-boundary math 2026-05-31 11:16:23 -05:00
chevron7 3ff43cea2a refactor(theme): switch redundant tokens to Tailwind utilities (Wave 2b) 2026-05-29 15:52:40 -05:00
chevron7 aab23dd483 refactor(theme): brand gradient stops use from-brand/to-brand/via-brand utilities 2026-05-29 14:06:22 -05:00
chevron7 d1f80cf966 refactor(theme): migrate exact-match hex classes to tokens (Wave 2a, zero visual change)
Replace 289 hard-coded hex color classes across 89 files with CSS custom property
token references. bg-[#0a0a0a/0f0f0f/141414/1a1a1a] → bg-[var(--bg-*)] forms;
border-[#1c1c1c/262626] → border-[var(--border-*)]; all #fca200/#fca208 variants
(typo unification) → bg/text/border/ring/fill-brand. Opacity modifiers preserved.
2026-05-29 14:03:03 -05:00
chevron7 a6635bb9b6 feat(a11y): app-wide sweep on queue/import/device/browse/podcasts/mix/home (audit Wave 1) 2026-05-29 13:56:09 -05:00
chevron7 4377f2a1ab fix(bugs): invisible refresh button, dead link param, fake queue drag handle, silent failures (audit Wave 0)
- import/playlist: fix invalid Tailwind class bg-#0a0a0a -> bg-[#0a0a0a] so Refresh button is visible
- audiobooks: strip dead ?tab=system query param from settings link, keep #audiobookshelf anchor
- queue: remove GripVertical false affordance and unused import -- Up/Down buttons are the real reorder mechanism
- radio: add isError + Retry states for genre and decade station queries; sections no longer silently disappear on failure
- podcasts: add toast.error on handleRefreshAll catch using existing useToast pattern
2026-05-29 13:50:51 -05:00
chevron7 6fa5a51954 feat(discover): clearer CTA + pre-generation disk disclosure, two-phase progress, consistent styling
- Rename primary CTA: "Initialize Generation" -> "Build This Week's Playlist"
- Add pre-generation disclosure grid (Finds / Downloads / Cleans up) surfacing
  the /music/discovery download behaviour before the user triggers generation
- Two-phase progress labels mapped to real backend status values: "scanning"
  -> "Finding artists..." and "downloading" -> "Downloading albums (N / M)..."
- Move disk-impact context cards into the empty state in the page's own visual
  style; HowItWorks kept as fuller reference detail below the playlist
- Settings gear: add aria-label, title, and visible "Settings" text label;
  min-h-[44px] touch target; focus-visible ring
- Load-error retry: expose loadError from useDiscoverData, show a Retry button
  when the initial fetch fails instead of silently landing on an empty state
- HowItWorks styling: bg-[var(--bg-secondary)] border-[var(--border-subtle)]
  replacing hardcoded #111/50 and border-white/5
- Token cleanup: hardcoded #eab308/#f59e0b replaced with --color-brand tokens;
  #a855f7 replaced with --color-discover throughout the empty state
2026-05-29 12:26:49 -05:00
chevron7 3d3cd68816 chore(settings): prefix unused onError rollback params to clear unused-var warnings 2026-05-29 12:22:46 -05:00
chevron7 9ee9cb9965 feat(settings): confirm dialogs on destructive resets, accurate section labels, maintenance grouping
- All five destructive reset ops (artists, mood tags, audio analysis, vibe
  embeddings, full enrichment reset) now gate behind ConfirmDialog with
  op-specific titles and cost warnings; window.confirm removed
- ConfirmDialog upgraded with Escape dismiss, focus trap, role=dialog,
  aria-modal, aria-labelledby/describedby, and auto-focus on Cancel
- Sidebar nav + section title: "Cache & Automation" -> "Library Enrichment";
  "Artwork" -> "AI & Artwork"; AIServicesSection title "Artwork Services"
  -> "AI & Artwork" -- scroll-anchor ids and component names unchanged
- Destructive maintenance buttons corralled into a collapsible "Maintenance
  Operations" disclosure within CacheSection (no new sidebar entry)
- Enrichment status text wrapped in aria-live="polite" aria-atomic="true"
- Re-run buttons and all action buttons raised to min-h-[44px] / py-2.5
  for 44px touch targets (was ~28px py-1)
- Non-admin sidebar already filtered by SettingsSidebar via isAdmin; no
  additional plumbing required
2026-05-29 12:21:18 -05:00
chevron7 37f218b285 feat(onboarding): per-integration test state, clearer admin/integration copy, sync-page visual alignment 2026-05-29 12:12:07 -05:00
chevron7 c692d7f372 feat(player): one-time gesture hint on mobile mini-player (behavior unchanged)
Shows a dismissible pill hint above the mini-player bar the first time a
user sees it with media loaded. Persists seen state in localStorage under
kima_miniplayer_hint_seen. Hint uses pointer-events:none so all swipe
gestures pass through unobstructed; only the dismiss button captures
pointer events. markHintSeen() is called additively in handleTouchEnd
after the existing swipe logic -- no thresholds or behaviors altered.
2026-05-29 11:18:42 -05:00
chevron7 5e2af131e7 feat(collection): consolidate filter/sort/per-page into a Refine panel, clarify owned vs discovery 2026-05-29 11:15:51 -05:00
chevron7 9c13a867b7 feat(vibe): surface vibe/similar/song-path track operations on mobile via panel sheet 2026-05-29 11:12:55 -05:00
chevron7 f25b9fff7c fix(search): render all library track results instead of silently capping at 10 2026-05-29 11:09:29 -05:00
chevron7 0bab9498d3 feat(vibe): first-run map hint + rename Drift->Song Path with outcome-focused tooltips (critique P1) 2026-05-28 23:03:23 -05:00
chevron7 6f3d5885dd fix(playlist): correct window-virtualizer scrollMargin/total-size so full list is scrollable
Switch from useWindowVirtualizer to useVirtualizer targeting the #main-content
scroll container (which is overflow-y-auto / flex-1, not the window). Measure
the list container's offset within that element after data loads via
useLayoutEffect so scrollMargin is accurate and all 1000 rows are reachable.
2026-05-28 22:41:40 -05:00