- SportsMatchController syncs Video.title with SportsMatch.title on
store/update so the "Match title" field drives the video's display
title everywhere.
- headerData(): "championship" now prefers event_name (event title)
and adds a dedicated "stage" key sourced from SportsMatch.title
(shown between the yellow lines above the weight class in both the
in-player VS overlay and the vs-mini gallery card).
- Video cards render match titles with flags + corner colours
("FINALS – 🇧🇭 Blue vs 🇧🇭 Red (-61 kg)"), matching the video
page header, and clip long titles to one line.
- Video gallery grid uses repeat(N, minmax(0, 1fr)) + grid-auto-rows:1fr
so every card thumbnail renders at exactly the same size; hover-preview
videos use object-fit:cover to stop portrait sources looking smaller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match player + preview
- Rebuild VS intro as a 1920×1080 scaled canvas (arena design from drafts/):
angled RED/BLUE panels with clip-paths, slam-in VS with pulse+shine,
event/stage/weight header, Match/Court/Referee chips, per-fighter
Record/Rank/Stats chip row. Every placeholder from the artifact is
always in the DOM; empty ones are hidden by `.vs-nullable:empty`
and `.vs-nullable-hide` so the layout adapts without losing the
design's HTML shape. Country codes always render as full country
names (via new App\Data\Countries::name()).
- Real-time 6s countdown Skip button; video pinned at t=0 while
the intro runs, released to play at zero or on Skip.
- Standalone preview page at /videos/{video}/vs-preview.
Video cards (home + everywhere)
- Match cards get a `vs-mini` overlay of the same arena design
(fixed canvas, per-card ResizeObserver, animations replay every
time the mouse leaves the thumb). On hover the mini explodes
outward (panels fly out, VS scales+blurs+fades) and the hover
video plays over the cleared thumb.
- Match cards also render a `scorebar-mini` overlay (verbatim of
the full-player msb-scale scorebar + msb-feed) that shows
during hover playback. Live-scoring is wired per-card via a
data-sbm-state payload of rounds+points; RED/BLUE score, round
label, per-round clock, and the top-right Live Scoring ticker
(4 entries, sport-aware Yuko/Waza-ari/Ippon or Punch/Body
kick/… labels) all update from the mini video's timeupdate.
Bug fixes
- Hover video wasn't playing sound: dropped the `muted` attribute
I'd added while debugging match previews.
- White flash strobing on the left of match cards: removed the
`.vs-mini-flash` element (intro-only flourish that fired on
every mouseleave replay).
- Sports-match modal was showing "Please complete the required
fields" when saving basic info for match videos that had no
SportsMatch row yet: the submit gate only checked matchId, not
the attached videoId; now it correctly skips uploadVideoFirst()
in both edit and attach-existing-video modes.
Country names project-wide
- New helper App\Data\Countries::name($iso2) → full country name.
- Swapped `?? country` fallbacks to `?? Countries::name(country)`
in admin dashboard, video-analytics, video-insights payloads
(VideoController + SuperAdminController), and the VS partials.
Now user-visible country labels always show "Bahrain" instead
of "BH", including chart tooltips and modal titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove max-height clamps and override portrait/square/ultrawide/theater
variants so the .ytp-wrap box shape never distorts. Portrait content
letterboxes inside the 16:9 frame via object-fit: contain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Scoreboard scale observes #videoContainer (the .ytp inner box) instead of
#ytpWrap so the overlay tracks the squeezed container when the fullscreen
highlights drawer opens. Score bar + live-scoring feed shift left with the
video and stay fully visible next to the drawer.
- msb-small (auto-hide identity + feed under 700 px) now skipped while
document.fullscreenElement is set — the drawer squeeze on smaller viewports
was dropping the container under 700 and hiding the live feed.
- Player settings panel (#ytpSettingsPanel) is now vertical-scroll capable:
max-height: calc(100vh - 120px) + overflow-y: auto + slim on-brand
scrollbar + overscroll-behavior: contain. Top items (Mini player, Playback
speed) stay reachable when the injected Scoreboard toggles push the
list past the visible area.
- Removed the slow-mo (.slowmo-picker) 1/4x/1/2x/3/4x selector from the
match highlights tab bar.
- Disabled the scoreboard scrim (.msb-scrim { display: none }). The two
linear-gradients were tinting the top and bottom of the video for text
legibility, but every scoreboard text element already carries its own
text-shadow — the tint was showing as permanent shadows over portrait
and landscape videos alike.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove VS intro from player and thumbnail VS card from match video cards.
All references (`msb-vs*`, `initVsIntro`, `setVsImg`, `HAS_VS`, `$vs`,
`$msbThumbVs`, `$hasMatchMeta`) are stripped from scoreboard partials,
video-card component, and scoreboard script. Related CSS keyframes
(`msbFadeIn/Out`, `msbDriftA/B`, `msbSweep`) removed.
- Image cropper (resources/views/components/image-cropper.blade.php):
replace unreliable third-party Cropme integration with a DIY cropper.
Pan/zoom/rotate wired directly with pointer + wheel events; on save,
the visible viewport is rendered to an offscreen canvas at the configured
output-width (with circular clip for `shape="circle"`). Also reparent the
overlay to <body> on open to escape any transformed Bootstrap-modal
ancestor's stacking context that clipped it.
- Sports match modal (resources/views/layouts/partials/sports-match-modal.blade.php):
set fighter photo croppers to 3:4 portrait (285x380) to match the VS
card's 228x304 frame; club logos + referee stay 1:1 square.
- SportsMatchController::fillFromRequest (line 174): coerce
`$this->clean($request->input('media', []))` to `[]` when it returns null
so array_merge doesn't crash under PHP 8's stricter types.
- SportsMatch::headerData(): read fighter headshots + club logos from the
canonical media.* keys (participant1_photo / participant2_photo /
club1_logo / club2_logo) that the uploader modal actually saves, with the
older p1_* / p2_* names kept as a secondary fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frame-accurate timestamps (end-to-end)
- Migration 2026_08_08_000002 promotes match_rounds.start_time_seconds,
match_points.timestamp_seconds, coach_reviews.start_time_seconds and
coach_reviews.end_time_seconds from INTEGER to decimal(10,3).
- Model $casts updated (float). MatchEventController validators relaxed
from `integer` to `numeric|min:0` on all four fields.
- Blade SSR data-time-{start,end} attributes cast to (float) so the
first paint carries frame precision.
- Point + review capture strips: slider step = 1/fps, snapToFrame()
applied to every seek/nudge/input commit. Clock now shows SMPTE
MM:SS.FF; parsePcbTimeInput accepts MM:SS.FF, MM:SS, MM.SS or seconds.
- Default fps is 30; override via window.matchFPS.
- confirmPointCapture no longer Math.round()s currentTime — sends the
exact frame boundary.
Point + review UX
- Video pauses aggressively on scrubber grab (mousedown/pointerdown/
touchstart) AND on every input, with a 30ms follow-up to beat HLS.js
race conditions.
- Zoom buttons now scope by data-pcs-zoom / data-rcb-zoom so clicks on
the review-strip zoom don't clobber the point-strip zoom (root cause
of the "resets while button still on 20x" bug).
- Point cards now play a preroll/postroll clip at 1× then replay at
the chosen slow-mo rate, then resume normal playback.
- REPLAY ×N badge shown in the top-left of the video during replays,
swapping palette when slow-mo kicks in.
- Slow-mo picker (¼× ½× ¾×) in the tab-header — window.slowmoRate is
the source of truth for both point and review replays.
- Clicking a card hides the player chrome (controls-hidden re-applied
across 0/10/60/200/500ms to beat pause/seek/play showControls races);
mousemove over the video brings them back instantly.
- Review-tools slow-mo button removed — the card click IS the trigger.
Coach review specific
- Dual-thumb scrubber on a shared track with a red fill between the
thumbs (visualises the note's range). Start clamps to ≤ end and
vice-versa; nudges act on the last-touched thumb.
- Note overlay live-previews as user types and is drag-to-position;
overlay position persists (position_x/position_y decimal(6,4)).
- Overlay uses container-query units (cqi) so text scales with the
player width and position stays proportional on any resize.
- Timeline card design (chapter-style time label + subtle card with
emoji, note, coach name, tools) — nothing else selected.
Sidebar
- "Rounds & points" / "Private notes" section labels removed; only
the owner-only + Add Round / + Add Note button remains.
- Points that share a timestamp within a round collapse into ONE card
with a chip per side (equal-width Blue/Red pills), one edit/delete
pair, and a coloured score line (yellow ROUND N, blue/red score
badges — background pills with white numerals).
- On mobile the highlights sheet only closes via its own toggle now.
Backdrop is pointer-events: none so taps pass through to the video.
Mobile capture strip
- Renders below the video on mobile portrait so the paused frame is
visible while typing. Wider inner padding (10px 20px 14px) so
content doesn't sit on the edges.
- .pcs-entry uses flex-wrap: nowrap and Action input shrinks to
fit — the row stays on one line at any width.
Collapsed action menu (video-actions component)
- Every viewport now shows the single ⚡ Action dropdown; individual
desktop-action buttons are hidden by CSS across the board.
SPA navigation fixes
- window.videoId + isOwner now live on window so navigation to a new
match video via Up Next can update them via reloadMatchVideoState.
- Tabs, event-item clicks, highlights toggle and sidebar-height sync
re-hydrate on each SPA swap.
- Blade point-time regression: "{{ '@' . $fmtTime(...) }}" instead of
the misused "@{{ ... }}" escape directive.
Backend
- Point-score recompute (recomputeRoundScores) already sums by
(timestamp, id) order — works fine with the new decimal columns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capture strip
- Twin of the point-capture strip, embedded inside #ytpWrap. Two time slots
(Start + optional End) with the scrubber driving whichever is "active".
Editable mm:ss inputs, zoom (1x/5x/20x), inline note + coach name + emoji
chip picker. Cancel / Save on the right; Delete in edit mode.
- ▶ preview button plays Start → End and auto-pauses at End; click again to
stop early. Playback uses the live overlay so previewing shows what the
viewer will see.
- Removed the #reviewModal popup; openAddReviewModal(), setReviewEmoji(),
saveReview() and the reviewForm submit binding are gone.
Live-preview overlay + drag-to-position
- As the user types, the coach-note overlay updates on the video in real
time. In capture mode the overlay is grab-able and draggable to any spot
over the video (mouse + touch), with a red dashed outline + "↕ drag me"
hint.
- Overlay position is stored as normalized 0..1 CENTER coords of the video
wrap and applied via `left: X%; top: Y%; transform: translate(-50%,-50%)`
so the overlay stays at the same relative spot on any resize / fullscreen.
- Font-size, padding and border-radius scale with the player width via CSS
container queries (`container-type: inline-size` on #ytpWrap + `clamp(...cqi)`),
so the note grows/shrinks proportionally.
- Draggable overlay lives above the strip (z-index 70 vs 60) so it can be
dropped anywhere including over the strip.
Persistence (schema change)
- Migration 2026_08_08_000001_add_position_to_coach_reviews: adds nullable
decimal(6,4) position_x / position_y to coach_reviews.
- CoachReview fillable + casts updated. store/updateReview validate
position_x / position_y as numeric between 0 and 1 and persist them.
- showCoachNoteOverlay(text, pos) applies the saved position on playback.
Cards: timeline design (Option B from the mockup)
- Vertical spine with red dots on the left; each review is a chapter-style
time label above a subtle card. Card body: emoji as the leading indicator,
note text, then coach name + tools (▶⏳ / ✏️ / 🗑).
- ▶⏳ = playReviewSlowmo(id): plays Start → End once at 1×, then loops back
and plays again at 0.5×, then pauses and hides the overlay. Only rendered
when the review has an end_time > start_time.
- Card body still clickable for the classic "jump-and-play-once" behaviour.
- Fixes a latent bug where the primary Save button stayed disabled after a
successful save, silently swallowing the next click. Both the review and
point capture strips now re-enable the button in their cancel handlers.
Also
- Design mockup file at public/coach-review-mockups.html shows the four
candidate layouts side by side (kept for future design iterations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CompressVideoJob: rewrite to exec ffmpeg directly. php-ffmpeg's X264
format class validates the codec name and only accepts libx264, so
every job dispatched with h264_nvenc had been throwing within ~1s and
leaving the source untouched. The job now honours the GPU encoder
settings, and always replaces the source when the codec is HEVC/H.265
so downloads and the MP4 fallback stay playable.
- VideoController + video-player component: point HLS URLs at
playlist.m3u8 (the actual master file GenerateHlsJob writes) instead
of the non-existent master.m3u8, which was 404-ing and forcing the
player back to the raw MP4.
- VideoController@store: on the NAS-success branch, flip status to
'ready' and populate duration/is_shorts before dispatching
GenerateHlsJob, so the job no longer silently no-ops.
- Upload chooser: render as a bottom sheet on mobile, and forward the
picked type to /videos/create?type= so the create page pre-selects it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds one chip per language currently present in public content —
counting both primary language and any secondary audio track, sorted
by video count, capped at 12. Clicking a chip lists every video whose
primary is that language or that has a matching secondary track; cards
surfaced only via a secondary track link straight to /videos/{key}?track=<id>
so the pinned playback wiring loads the right audio, get a small flag
badge on the thumbnail, and their hover-preview streams the track file
instead of the primary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundled pre-existing changes on the branch:
- Video merge: controller, service, admin merge modal, and a repair
console command; new video_redirects table (with source_folder)
so merged/moved videos keep resolving under their old keys.
- Default avatar SVG for users without a profile picture.
- Assorted view tweaks across profile, video actions/comments/insights,
admin layout, video-details, and the app layout.
- Small touch-ups in SuperAdminController, User model, NasSyncService.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cropper's onAvatarSaved/onBannerSaved callback used the url from
image.upload, which points at the temp buffer file. updateAvatar/updateBanner
then move that temp file to the NAS profile dir and delete the local copy, so
the url is dead the moment it's used — the avatar/banner rendered broken until
the next page load (the file itself was saved fine).
- updateAvatar/updateBanner now return the canonical media route URL
(route('media.avatar'|'media.banner', $relPath)), matching the model's
avatar_url/banner_url accessors and the project's no-asset('storage/...') rule.
- The image-cropper prefers the update endpoint's returned url over the temp
image.upload url; falls back to the old behavior when none is returned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The base card CSS (.yt-video-card/.yt-video-thumb/.yt-video-info) lived inside
video-card's @once <style> block, so it only emitted when a video card rendered.
Playlist cards reuse those classes, so on playlist-only pages (no video cards)
they rendered completely unstyled — a mess.
Extract the shared card CSS into resources/views/components/partials/card-styles
(kept @once) and @include it from BOTH video-card and playlist-card. Now the CSS
loads whenever either card renders, and @once still dedupes it to one copy on
pages that show both (home feed verified: emitted exactly once).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Player/UI fixes:
- Sports (match) Highlights pane: move the toggle click-handler script out of
the always-false @if(false ...) legacy block so the "Highlights" button opens,
and emit videoId/isOwner with @json instead of {{ json_encode() }} (escaped
quotes crashed the data/tab/CRUD block once route keys became hashid strings).
- Progress scrubber knob (video + audio players): center on the track line with
translate(-50%, -50%); drop the margin-top hack that floated it above the line.
- Music player volume: give the slider wrap height so overflow:hidden no longer
crops the knob; add a subtle thumb shadow for visibility.
- Music language menu: cap height to available space above the header, sticky
header, internal scroll; keep controls from auto-hiding while the menu is open.
- Description box: re-check "Show more" overflow at every settle point (fonts,
images, rAF, tab reopen) so SPA autonext keeps the expand button.
Also bundles pending work: save-to-playlist button component, playlist/channel
views, PlaylistController + NasSyncService, and component-usage tracker updates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lyrics pipeline (Whisper + Demucs + description alignment):
- New GenerateLyricsJob runs WhisperX with VAD filtering and forced word
alignment, writes per-track JSON to NAS.
- New DecorateLyricsJob calls the active LLM provider to bake one to
several emojis into each line (heavy decoration prompt).
- LyricsDescriptionParser strips heading content, section markers, and
emoji-decoration from a song's description while preserving every
language verbatim.
- correct_whisper_with_description aligner: strong-match anchors only,
vocal-region-aware gap-fill so missing verses land on actual singing.
- Owner UI for generate/regenerate/edit/delete in the player gear.
Admin pages:
- /admin/lyrics toggles for VAD, vocal gap-fill, Demucs, master
- /admin/gpu extracted GPU section, encoder picker, FFmpeg path
- /admin/backup extracted users-and-settings export/import
- /admin/settings now AI/LLM only with provider list and Test button
- /admin/nas-storage hosts NAS settings, repair, disable flow, browser
- Shared partials/settings-styles for a uniform look across pages.
Playlist view tracking:
- Migration adds playlists.view_count and playlist_views dedup table.
- Playlist::bumpViewIfNew increments per device with a one-hour window.
- Tracked from /playlists/{id}, /playlists/share/{token}, /ps/{token},
and /videos/{id}?playlist={token}. Dispatched after-response so it
never blocks the page render.
- Loading a playlist on the video page now runs one query instead of
the four the old getNextVideo/getPreviousVideo path triggered.
- View counts shown on every playlist card and the playlist hero.
Player polish:
- Floating mini-player is draggable, persists its position in
localStorage, clamps to viewport on resize.
- Mini disabled entirely on mobile (less than 768px).
- New gear-menu Mini Player toggle (persists in localStorage) lets the
user disable both scroll-activation and SPA-nav-activation.
- Close button keeps media playing when used on the player's own page.
- SPA navigator now swaps a #page-scripts container so per-page JS
(channel tabs, etc.) gets re-executed after content swaps.
Storage layout:
- Runtime data moved from /storage/* to /data/* and gitignored.
- /ml/venv, /ml/cache, /ml/__pycache__ excluded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New POST /videos/{video}/share/email route (auth + throttled) handled by
VideoController@shareByEmail, sending the VideoShared mailable rendered from
emails/video-shared.blade.php. Wired into the share modal and video-actions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).
Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
is carried through share links and auto-selects audio + title + flag + about +
OG/meta on open.
GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
frequency bars; loop-filter rebuild makes generation ~25x faster.
Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce per-video language support and multiple audio tracks
(VideoAudioTrack model + migrations for language, description, title),
a reusable language-select component, and a track-editor form. Bundle
the self-hosted flag-icons v7.2.3 library and a NAS auto-sync command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On mobile, entering fullscreen now also locks the screen orientation to
landscape via the Screen Orientation API. Exiting fullscreen unlocks it,
allowing the device to return to portrait. Applied to both the video
player and audio player. Gracefully ignored on browsers that don't
support screen.orientation.lock (e.g. iOS Safari).
Also includes the playlist auto-scroll fix (committed separately).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The MANIFEST_PARSED listener was registered after loadSource/attachMedia,
so cached manifests could fire the event before the listener was set —
meaning tryAutoplay never ran. Fix: register the listener first, then
call loadSource. Also switch MP4 and native-HLS paths to play on
loadedmetadata (not immediately after load()) which is the earliest
safe moment to call play(). Belt-and-suspenders canplay fallback now
also explicitly mutes before calling play().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, play() was only called from the canplay listener which can
fire too late or be missed with HLS.js. Now also triggers on
Hls.Events.MANIFEST_PARSED (the earliest reliable point), and calls
play() directly after video.load() for MP4/native-HLS paths. Also
fixes _ytpLoadSource (SPA transitions) to use the same pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _ytpLoadSource(hlsUrl, mp4Url) to video-player component:
destroys old HLS instance, creates a new one with the new source,
then plays — browser retains autoplay permission since the <video>
element never leaves the page
- Add _ytpNavOverride hook: playlist overlay can replace navigateNext/
navigatePrev without modifying the component internals
- Add _plOnVideoEnd hook to 'ended' handler so the playlist overlay
can control autoplay/loop behavior independently
- Expose window._ytpHls for HLS instance lifecycle management
- Add hls_url + has_hls to /videos/{video}/player-data JSON endpoint
- Replace generic.blade.php playlist controls with full SPA system
identical in structure to music type: plTransitionTo, plSwapContent,
plAdj, plRender, plHighlight — no page refresh on track change
- Sidebar shows all playlist tracks; current track highlighted in red
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Controls were only added to show.blade.php, but music/generic/match videos
render their own complete layouts with their own sidebars. Added the
pl-controls-bar to all three type views and the global CSS to app.blade.php.
- music: full standalone JS with shuffle/loop/autoplay + _plOnTrackEnd hook
- generic/match: syncs with video-player's existing ytpShuffleRow/ytpAutoplayRow toggles
- audio-player: ended handler now calls window._plOnTrackEnd if defined
- video-player: exposes window._ytpNav.next/prev for sidebar prev/next buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- video-player: add userSeeking flag so mid-seek pause events are
suppressed; force-resume on 'seeked' if video was playing before seek;
guard player click handler against progCont clicks; e.preventDefault()
on touchend to stop synthetic click toggling play
- audio-player: apply identical seek fixes (same four changes)
- app layout: add floating mini-player that saves video state to
sessionStorage when bottom nav is tapped while a video is playing,
then restores playback on the next page via a floating overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
settings panel, country/phone/timezone components, posts, slideshow,
playlist shares, video downloads/shares, comment likes, notifications,
social links, and more
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>