- 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>
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
TAKEONE (Play) is a Laravel 10 video-sharing platform with sports match annotation, HLS adaptive streaming, GPU-accelerated video processing, playlists, threaded comments, and a super-admin panel. Live at https://video.takeone.bh.
Tech stack: PHP 8.1+, Laravel 10, Blade templating, Vite 5, Axios, FFmpeg/FFProbe with NVIDIA NVENC, SQLite (dev) / MySQL (prod), Laravel Sanctum.
Essential Commands
Local Development
php artisan serve # Backend on http://localhost:8000
npm run dev # Vite dev server with HMR
npm run build # Production frontend build → public/build/
Database
php artisan migrate
php artisan db:seed
php artisan tinker
Background Workers
# Video processing (CompressVideoJob, GenerateHlsJob)
php artisan queue:work --queue=video-processing
# Orphaned file cleanup scheduler (every CLEANUP_INTERVAL_MINUTES, default 30)
php artisan schedule:run # run this every minute via cron
php artisan cleanup:orphaned-videos --dry-run # preview only
php artisan cleanup:orphaned-videos --force # delete orphans
Testing
./vendor/bin/phpunit
./vendor/bin/phpunit --filter "TestName"
./vendor/bin/phpunit tests/Feature/ExampleTest.php
Tests use in-memory SQLite, array mail/cache/session drivers, sync queue, and BCRYPT_ROUNDS=4. Set APP_ENV=testing.
Architecture
Role System
Three roles stored on users.role: super_admin, admin, user (null = user). The IsSuperAdmin middleware guards all /admin/* routes. Helper methods on the User model: isSuperAdmin(), isAdmin(), isUser().
Video Lifecycle
VideoController@storevalidates upload, stores file, extracts metadata via FFProbe (duration, dimensions, orientation).- Status column transitions:
pending → processing → ready(orfailed). CompressVideoJobre-encodes with NVIDIA NVENC (h264_nvenc, CRF 23), replaces original if smaller.GenerateHlsJobproduces 480p / 720p / 1080p HLS variants (h264_nvenc, preset p4) as.m3u8+.tsfiles.- Streaming served at
GET /videos/{video}/hls/{file?}(master playlist → segments).
Queue connection is sync by default (runs inline); switch QUEUE_CONNECTION=database or redis for true async.
Shorts Auto-Detection
A video is a Short when duration ≤ 60 s and portrait orientation. The is_shorts DB column allows manual override. Use model scopes Video::shorts() / Video::notShorts().
Trending Algorithm
Video::scopeTrending($hours=48, $limit=50) — weighted score: 70% recent views, 15% view velocity, 10% recency bonus, 5% likes. Only applies to videos < 10 days old with ≥ 5 recent views.
Playlist System
playlist_videospivot carriesposition,watched_seconds,watched,added_at,last_watched_at.- Each user has one
is_default = trueplaylist ("Watch Later"). Playlistmodel methods:addVideo(),removeVideo(),reorder(),getNextVideo(),getPreviousVideo(),updateWatchProgress(),canView(),canEdit().
Comment Threading & Mentions
Comments have a parent_id for one-level threading (replies). The parsed_body accessor converts @username syntax into clickable profile links.
Comment Timestamp Badges
The enhanceBody() function in components/video-comments.blade.php converts timestamp syntax written in comments into clickable ._comment-time-badge spans at render time (client-side). Supported formats:
@mm:ss— single timestamp, jumps to that point (colon separator)@mm.ss— same, dot separator (legacy, still supported)@mm:ss-mm:ssor@mm.ss-mm.ss— time range; plays from start to end then pauses
Clicking a badge: scrolls to #ytpWrap (smooth), waits 500 ms, seeks #videoPlayer to start, calls .play(), and if a range is specified stops at end via a timeupdate listener. @username mention detection requires the first char to be a letter so it never collides with numeric timestamps.
Sports Match Annotation
Videos of type match support three related models:
MatchRound— round number, name,start_time_secondsMatchPoint—timestamp_seconds, action, competitor (blue/red), running scoreCoachReview— time-range segment with coach note and emoji
All managed via MatchEventController under authenticated routes.
Key Model Scopes & Accessors
Video scopes: public(), visibleTo($user), shorts(), notShorts(), trending().
Video accessors: url, thumbnail_url, like_count, view_count, formatted_duration, iso_duration, open_graph_image.
Playlist accessors: thumbnail_url, video_count, total_duration, formatted_duration.
Rules
Database changes require confirmation — if any task requires a migration, schema change, or new column, always ask before proceeding.
Never use alert(), confirm(), or prompt() — use toast notifications or inline UI feedback instead.
All buttons must use the global .action-btn system — never add custom button CSS. Use these classes:
.action-btn— default (bordered, bg-secondary).action-btn.action-btn-primaryor.action-btn.primary— brand red, for primary/submit actions.action-btn.action-btn-dangeror.action-btn.danger— red border/text, for destructive actions.action-btn.icon-only— square padding, for icon-only buttons
Structure: <button class="action-btn"><i class="bi bi-..."></i> <span>Label</span></button>. The global CSS lives in layouts/app.blade.php.
Mobile layout uses a native-app scroll model — on max-width: 768px, html and body are locked (overflow: hidden; position: fixed) so the window never scrolls. .yt-main is position: fixed spanning top: 56px to bottom: calc(56px + env(safe-area-inset-bottom)) with overflow-y: auto; -webkit-overflow-scrolling: touch. This keeps the header and bottom nav truly fixed without any JavaScript. Consequences to remember:
- Never use
position: stickyinside.yt-mainon mobile — sticky elements float over content because the scroll container changed. Override withposition: relative !importantin the mobile media query. - Never rely on
window.scrollYorwindow.scrollevents on mobile — the window doesn't scroll; listen ondocument.getElementById('main')instead. - Bottom nav needs no JS transform — since the window is locked, browser chrome animation never shifts fixed elements.
Upload modal and upload page must always stay in sync — the desktop upload UI lives in resources/views/layouts/partials/upload-modal.blade.php and the mobile upload UI lives in resources/views/videos/create.blade.php. On mobile, openUploadModal() redirects to the create page instead of showing the modal. Any change made to one must be applied to the other immediately in the same task.
Edit modal and edit page must always stay in sync — the desktop edit UI lives in resources/views/layouts/partials/edit-video-modal.blade.php and the mobile edit UI lives in resources/views/videos/edit.blade.php. On mobile (< 992px), openEditVideoModal(videoId) redirects to /videos/{id}/edit instead of opening the modal. Any change made to one must be applied to the other immediately in the same task. — the desktop upload UI lives in resources/views/layouts/partials/upload-modal.blade.php and the mobile upload UI lives in resources/views/videos/create.blade.php. On mobile, openUploadModal() redirects to the create page instead of showing the modal. Any change made to one must be applied to the other immediately in the same task — this includes new fields, validation logic, file-type support, JS behaviour, labels, and error handling. Never update only one side.
Never build custom dropdowns for country, nationality, phone code, timezone, or currency — reusable Blade components already exist for these. Always use them; never roll a new <select>, inline list, or custom picker:
| Need | Component | Stored value |
|---|---|---|
| Country / nationality picker | <x-country-select name="…" /> |
ISO2 code e.g. "BH" |
| Phone / dial-code picker | <x-phone-code-select name="…" /> |
`"+973 |
| Timezone picker | <x-timezone-select name="…" /> |
IANA string e.g. "Asia/Bahrain" |
| Currency | read from App\Data\Countries::all()[$iso2]['currency'] |
ISO 4217 code e.g. "BHD" |
All three components accept name, id, value, label, placeholder, required, class, and style props. The full dataset lives in app/Data/Countries.php. Usage is tracked in .claude/component-usage.md — add a row to the relevant table whenever you place one of these components in a view.
Component usage tracker is mandatory and must always be kept current — the tracker lives at .claude/component-usage.md. These rules apply without exception:
- Creating a new reusable component → add a new section to the tracker listing the component file path, its props, and an empty usage table.
- Placing a component in any view → immediately add a row to the relevant tracker table (view file path, field/slot name, any relevant notes). Do this in the same task, not later.
- Modifying a component (props, markup, CSS, JS, behaviour) → open the tracker first, read every row in that component's usage table, then apply the necessary follow-up changes to every listed view before marking the task done. Never modify a component without checking its tracker entries.
- Removing a component from a view → delete its row from the tracker table in the same task.
- Deleting a component entirely → remove its full section from the tracker and clean up every view that was still referencing it.
The tracker is the source of truth for blast radius. If the tracker is out of date and a change breaks an unlisted page, that is a process failure — always keep it accurate.
Match highlights sidebar must always match the video player height — use a ResizeObserver on #ytpWrap to write --sidebar-height to document.documentElement and bind .events-sidebar { height: var(--sidebar-height) }. Never hardcode a pixel or viewport height for the sidebar. The pattern lives in videos/types/match.blade.php (initSidebarHeightSync).
Infrastructure Notes
- Cloudflare proxy:
AppServiceProviderforces HTTPS and trusts Cloudflare headers viaTrustProxies. - FFmpeg config:
/config/ffmpeg.php— binaries at/usr/bin/ffmpegand/usr/bin/ffprobe, GPU device 0, 3600 s timeout. - Broadcasting: Pusher is configured but
BroadcastServiceProvideris commented out — not active. - Timezone:
Asia/Bahrain(set inconfig/app.php). - App name constant:
config('app.name')returnsTAKEONE.
Route Structure Summary
- Public:
/,/videos,/trending,/shorts,/videos/search,/videos/{video}, stream/hls/download - Auth-required: video CRUD, likes, comments, profile, settings, history, playlists, match events
- Admin (
/admin/*,super_adminmiddleware): dashboard, user CRUD, video CRUD, orphan cleanup - API:
GET /api/user(Sanctum token auth)