- 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>
157 lines
11 KiB
Markdown
157 lines
11 KiB
Markdown
# 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
|
|
```bash
|
|
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
|
|
```bash
|
|
php artisan migrate
|
|
php artisan db:seed
|
|
php artisan tinker
|
|
```
|
|
|
|
### Background Workers
|
|
```bash
|
|
# 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
|
|
```bash
|
|
./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
|
|
1. `VideoController@store` validates upload, stores file, extracts metadata via FFProbe (duration, dimensions, orientation).
|
|
2. Status column transitions: `pending → processing → ready` (or `failed`).
|
|
3. `CompressVideoJob` re-encodes with NVIDIA NVENC (`h264_nvenc`, CRF 23), replaces original if smaller.
|
|
4. `GenerateHlsJob` produces 480p / 720p / 1080p HLS variants (`h264_nvenc`, preset p4) as `.m3u8` + `.ts` files.
|
|
5. 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_videos` pivot carries `position`, `watched_seconds`, `watched`, `added_at`, `last_watched_at`.
|
|
- Each user has one `is_default = true` playlist ("Watch Later").
|
|
- `Playlist` model 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:ss` or `@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_seconds`
|
|
- `MatchPoint` — `timestamp_seconds`, action, competitor (blue/red), running score
|
|
- `CoachReview` — 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-primary` or `.action-btn.primary` — brand red, for primary/submit actions
|
|
- `.action-btn.action-btn-danger` or `.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: sticky` inside `.yt-main` on mobile** — sticky elements float over content because the scroll container changed. Override with `position: relative !important` in the mobile media query.
|
|
- **Never rely on `window.scrollY` or `window.scroll` events on mobile** — the window doesn't scroll; listen on `document.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|BH"` — split on `|` to get code alone |
|
|
| 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:
|
|
|
|
1. **Creating a new reusable component** → add a new section to the tracker listing the component file path, its props, and an empty usage table.
|
|
2. **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.
|
|
3. **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.
|
|
4. **Removing a component from a view** → delete its row from the tracker table in the same task.
|
|
5. **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**: `AppServiceProvider` forces HTTPS and trusts Cloudflare headers via `TrustProxies`.
|
|
- **FFmpeg config**: `/config/ffmpeg.php` — binaries at `/usr/bin/ffmpeg` and `/usr/bin/ffprobe`, GPU device 0, 3600 s timeout.
|
|
- **Broadcasting**: Pusher is configured but `BroadcastServiceProvider` is commented out — not active.
|
|
- **Timezone**: `Asia/Bahrain` (set in `config/app.php`).
|
|
- **App name constant**: `config('app.name')` returns `TAKEONE`.
|
|
|
|
### 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_admin` middleware): dashboard, user CRUD, video CRUD, orphan cleanup
|
|
- API: `GET /api/user` (Sanctum token auth)
|