ghassan c160242dbc WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:15:20 +03:00

1084 lines
58 KiB
PHP

@extends('admin.layout')
@section('title', 'Admin Dashboard')
@section('page_title', 'Dashboard')
@section('extra_styles')
<style>
/* ── Stat cards ── */
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 20px 22px;
display: flex;
flex-direction: column;
gap: 6px;
position: relative;
overflow: hidden;
transition: border-color .2s;
}
.stat-card:hover { border-color: #444; }
.stat-card-accent {
position: absolute; top: 0; left: 0; right: 0; height: 3px; border-radius: 14px 14px 0 0;
}
.stat-card-top { display: flex; align-items: center; justify-content: space-between; }
.stat-icon {
width: 42px; height: 42px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 20px;
}
.stat-value { font-size: 28px; font-weight: 700; color: var(--text-primary); line-height: 1; }
.stat-label { font-size: 13px; color: var(--text-secondary); font-weight: 500; }
.stat-growth {
display: inline-flex; align-items: center; gap: 4px;
font-size: 12px; font-weight: 600; padding: 2px 8px;
border-radius: 20px; width: fit-content;
}
.stat-growth.up { background: rgba(34,197,94,.15); color: #4ade80; }
.stat-growth.down { background: rgba(239,68,68,.15); color: #f87171; }
.stat-growth.flat { background: rgba(156,163,175,.15); color: #9ca3af; }
.stat-week { font-size: 12px; color: var(--text-secondary); }
/* ── Alert banner ── */
.alert-banner {
border-radius: 12px; padding: 14px 18px;
display: flex; align-items: center; gap: 12px;
margin-bottom: 24px; font-size: 14px;
}
.alert-banner.danger { background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); color: #f87171; }
.alert-banner.warn { background: rgba(234,179,8,.12); border: 1px solid rgba(234,179,8,.3); color: #facc15; }
/* ── Section card ── */
.dash-card {
background: var(--bg-secondary); border: 1px solid var(--border-color);
border-radius: 14px; padding: 20px; height: 100%;
}
.dash-card-title {
font-size: 15px; font-weight: 600; color: var(--text-primary);
margin: 0 0 16px; display: flex; align-items: center; gap: 8px;
}
.dash-card-title i { color: var(--brand-red); }
/* ── Chart container ── */
.chart-wrap { position: relative; height: 220px; }
/* ── Top content table ── */
.top-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.top-table th { color: var(--text-secondary); font-weight: 500; padding: 0 10px 10px; text-align: left; border-bottom: 1px solid var(--border-color); }
.top-table td { padding: 10px; border-bottom: 1px solid rgba(255,255,255,.04); color: var(--text-primary); vertical-align: middle; }
.top-table tr:last-child td { border-bottom: none; }
.top-table tr:hover td { background: rgba(255,255,255,.03); }
.top-thumb { width: 52px; height: 34px; object-fit: cover; border-radius: 5px; background: #333; }
.top-thumb-placeholder { width: 52px; height: 34px; border-radius: 5px; background: #2a2a2a; display:flex; align-items:center; justify-content:center; color:#555; }
/* ── Status progress bars ── */
.status-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.status-label { width: 90px; font-size: 13px; color: var(--text-secondary); flex-shrink: 0; }
.status-bar-wrap { flex: 1; height: 8px; background: #2a2a2a; border-radius: 4px; overflow: hidden; }
.status-bar { height: 100%; border-radius: 4px; transition: width .4s ease; }
.status-count { width: 36px; text-align: right; font-size: 13px; font-weight: 600; color: var(--text-primary); flex-shrink: 0; }
/* ── Engagement metrics ── */
.engage-item {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,.05);
}
.engage-item:last-child { border-bottom: none; padding-bottom: 0; }
.engage-name { font-size: 13px; color: var(--text-secondary); }
.engage-val { font-size: 18px; font-weight: 700; color: var(--text-primary); }
/* ── Storage breakdown ── */
.storage-item { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.storage-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.storage-name { font-size: 13px; color: var(--text-secondary); flex: 1; }
.storage-bar-wrap { width: 120px; height: 6px; background: #2a2a2a; border-radius: 3px; overflow: hidden; }
.storage-bar { height: 100%; border-radius: 3px; }
.storage-mb { font-size: 13px; font-weight: 600; color: var(--text-primary); width: 60px; text-align: right; }
/* ── Recent table ── */
.recent-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.recent-table th { color: var(--text-secondary); font-weight: 500; padding: 0 8px 10px; text-align: left; border-bottom: 1px solid var(--border-color); }
.recent-table td { padding: 10px 8px; border-bottom: 1px solid rgba(255,255,255,.04); }
.recent-table tr:last-child td { border-bottom: none; }
.user-avatar-sm { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
/* ── Type doughnut legend ── */
.type-legend { display: flex; flex-direction: column; gap: 10px; justify-content: center; }
.type-leg-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.type-leg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
/* ── Country list ── */
.country-row {
display: flex; align-items: center; gap: 10px;
padding: 9px 0; border-bottom: 1px solid rgba(255,255,255,.04);
}
.country-row:last-child { border-bottom: none; }
.country-rank { width: 22px; font-size: 12px; font-weight: 700; color: var(--text-secondary); text-align: right; flex-shrink: 0; }
.country-flag { font-size: 22px; line-height: 1; flex-shrink: 0; width: 28px; text-align: center; }
.country-name { flex: 1; font-size: 13px; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.country-bar-wrap { width: 80px; height: 5px; background: #2a2a2a; border-radius: 3px; overflow: hidden; flex-shrink: 0; }
.country-bar { height: 100%; border-radius: 3px; background: #22d3ee; }
.country-count { width: 48px; text-align: right; font-size: 13px; font-weight: 600; color: var(--text-primary); flex-shrink: 0; }
/* ── Clickable rows ── */
.clickable-row { cursor: pointer; transition: background .15s; }
.clickable-row:hover td, .clickable-row:hover { background: rgba(255,255,255,.04) !important; }
.clickable-card { cursor: pointer; transition: border-color .2s, transform .15s; }
.clickable-card:hover { border-color: #555 !important; transform: translateY(-2px); }
.clickable-seg { cursor: pointer; }
/* ── Dash Modal ── */
.dm-overlay {
position: fixed; inset: 0; z-index: 9000;
background: rgba(0,0,0,.65); backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .2s;
}
.dm-overlay.open { opacity: 1; pointer-events: auto; }
.dm-sheet {
background: var(--bg-secondary); border: 1px solid var(--border-color);
border-radius: 16px; width: min(760px, 95vw); max-height: 80vh;
display: flex; flex-direction: column;
transform: translateY(24px) scale(.97); transition: transform .25s cubic-bezier(.34,1.56,.64,1);
box-shadow: 0 32px 80px rgba(0,0,0,.6);
}
.dm-overlay.open .dm-sheet { transform: translateY(0) scale(1); }
.dm-header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 22px 14px; border-bottom: 1px solid var(--border-color); flex-shrink: 0;
}
.dm-title { font-size: 15px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 10px; }
.dm-title i { color: var(--brand-red); font-size: 16px; }
.dm-close {
width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border-color);
background: transparent; color: var(--text-secondary); cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 16px; transition: all .15s;
}
.dm-close:hover { background: var(--border-color); color: var(--text-primary); }
.dm-body { overflow-y: auto; padding: 18px 22px; flex: 1; }
.dm-spinner {
width: 32px; height: 32px; border: 3px solid var(--border-color);
border-top-color: var(--brand-red); border-radius: 50%;
animation: dm-spin .7s linear infinite; margin: 0 auto;
}
@keyframes dm-spin { to { transform: rotate(360deg); } }
.dm-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.dm-table th { color: var(--text-secondary); font-weight: 500; padding: 0 10px 10px; text-align: left; border-bottom: 1px solid var(--border-color); white-space: nowrap; }
.dm-table td { padding: 10px; border-bottom: 1px solid rgba(255,255,255,.04); color: var(--text-primary); vertical-align: middle; }
.dm-table tr:last-child td { border-bottom: none; }
.dm-table tr[onclick]:hover td { background: rgba(255,255,255,.03); }
.dm-badge { padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; }
/* ── Quick actions ── */
.quick-btn {
display: flex; align-items: center; gap: 10px; padding: 12px 16px;
background: rgba(255,255,255,.04); border: 1px solid var(--border-color);
border-radius: 10px; color: var(--text-primary); font-size: 13px; font-weight: 500;
text-decoration: none; cursor: pointer; transition: background .2s, border-color .2s; width: 100%;
}
.quick-btn:hover { background: rgba(255,255,255,.08); border-color: #555; color: var(--text-primary); }
.quick-btn i { font-size: 18px; }
</style>
@endsection
@section('content')
{{-- ── Alert Banner ── --}}
@if($failedCount > 0)
<div class="alert-banner danger">
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
<div>
<strong>{{ $failedCount }} video{{ $failedCount > 1 ? 's' : '' }} failed processing</strong>
these uploads need attention.
<a href="{{ route('admin.videos') }}?status=failed" class="ms-2 text-danger fw-bold">Review now </a>
</div>
</div>
@endif
@if($processingCount > 0 || $pendingCount > 0)
<div class="alert-banner warn">
<i class="bi bi-hourglass-split fs-5"></i>
<div>
<strong>{{ $processingCount + $pendingCount }} video{{ ($processingCount + $pendingCount) > 1 ? 's' : '' }} in queue</strong>
{{ $processingCount }} processing, {{ $pendingCount }} pending.
</div>
</div>
@endif
{{-- ── Stat Cards ── --}}
<div class="row g-3 mb-4">
{{-- Users --}}
<div class="col-6 col-lg-4 col-xl-2">
<div class="stat-card clickable-card" onclick="openDashModal('All Users','users',{},'bi-people-fill')" title="Click to view users">
<div class="stat-card-accent" style="background: linear-gradient(90deg,#6366f1,#818cf8);"></div>
<div class="stat-card-top">
<div class="stat-icon" style="background:rgba(99,102,241,.15);color:#818cf8;"><i class="bi bi-people-fill"></i></div>
</div>
<div class="stat-value">{{ number_format($stats['totalUsers']) }}</div>
<div class="stat-label">Total Users</div>
<div class="stat-growth {{ $stats['growthUsers']['dir'] }}">
<i class="bi bi-arrow-{{ $stats['growthUsers']['dir'] === 'up' ? 'up' : ($stats['growthUsers']['dir'] === 'down' ? 'down' : 'right') }}-short"></i>
{{ $stats['growthUsers']['pct'] }}%
</div>
<div class="stat-week">+{{ $stats['usersThisWeek'] }} this week</div>
</div>
</div>
{{-- Videos --}}
<div class="col-6 col-lg-4 col-xl-2">
<div class="stat-card clickable-card" onclick="openDashModal('All Videos','videos',{},'bi-play-circle-fill')" title="Click to view videos">
<div class="stat-card-accent" style="background: linear-gradient(90deg,#e61e1e,#ff6b6b);"></div>
<div class="stat-card-top">
<div class="stat-icon" style="background:rgba(230,30,30,.15);color:#ff6b6b;"><i class="bi bi-play-circle-fill"></i></div>
</div>
<div class="stat-value">{{ number_format($stats['totalVideos']) }}</div>
<div class="stat-label">Total Videos</div>
<div class="stat-growth {{ $stats['growthVideos']['dir'] }}">
<i class="bi bi-arrow-{{ $stats['growthVideos']['dir'] === 'up' ? 'up' : ($stats['growthVideos']['dir'] === 'down' ? 'down' : 'right') }}-short"></i>
{{ $stats['growthVideos']['pct'] }}%
</div>
<div class="stat-week">+{{ $stats['videosThisWeek'] }} this week</div>
</div>
</div>
{{-- Views --}}
<div class="col-6 col-lg-4 col-xl-2">
<div class="stat-card clickable-card" onclick="openDashModal('Recent Views','views_day',{},'bi-eye-fill')" title="Click to view recent views">
<div class="stat-card-accent" style="background: linear-gradient(90deg,#06b6d4,#22d3ee);"></div>
<div class="stat-card-top">
<div class="stat-icon" style="background:rgba(6,182,212,.15);color:#22d3ee;"><i class="bi bi-eye-fill"></i></div>
</div>
<div class="stat-value">{{ number_format($stats['totalViews']) }}</div>
<div class="stat-label">Total Views</div>
<div class="stat-growth {{ $stats['growthViews']['dir'] }}">
<i class="bi bi-arrow-{{ $stats['growthViews']['dir'] === 'up' ? 'up' : ($stats['growthViews']['dir'] === 'down' ? 'down' : 'right') }}-short"></i>
{{ $stats['growthViews']['pct'] }}%
</div>
<div class="stat-week">+{{ number_format($stats['viewsThisWeek']) }} this week</div>
</div>
</div>
{{-- Likes --}}
<div class="col-6 col-lg-4 col-xl-2">
<div class="stat-card clickable-card" onclick="openDashModal('Most Liked Videos','likes',{},'bi-heart-fill')" title="Click to view most liked">
<div class="stat-card-accent" style="background: linear-gradient(90deg,#ec4899,#f472b6);"></div>
<div class="stat-card-top">
<div class="stat-icon" style="background:rgba(236,72,153,.15);color:#f472b6;"><i class="bi bi-heart-fill"></i></div>
</div>
<div class="stat-value">{{ number_format($stats['totalLikes']) }}</div>
<div class="stat-label">Total Likes</div>
<div class="stat-growth {{ $stats['growthLikes']['dir'] }}">
<i class="bi bi-arrow-{{ $stats['growthLikes']['dir'] === 'up' ? 'up' : ($stats['growthLikes']['dir'] === 'down' ? 'down' : 'right') }}-short"></i>
{{ $stats['growthLikes']['pct'] }}%
</div>
<div class="stat-week">+{{ $stats['likesThisWeek'] }} this week</div>
</div>
</div>
{{-- Comments --}}
<div class="col-6 col-lg-4 col-xl-2">
<div class="stat-card clickable-card" onclick="openDashModal('Recent Comments','comments',{},'bi-chat-dots-fill')" title="Click to view comments">
<div class="stat-card-accent" style="background: linear-gradient(90deg,#f59e0b,#fbbf24);"></div>
<div class="stat-card-top">
<div class="stat-icon" style="background:rgba(245,158,11,.15);color:#fbbf24;"><i class="bi bi-chat-dots-fill"></i></div>
</div>
<div class="stat-value">{{ number_format($stats['totalComments']) }}</div>
<div class="stat-label">Total Comments</div>
<div class="stat-growth flat"><i class="bi bi-arrow-right-short"></i> </div>
<div class="stat-week">+{{ $stats['commentsThisWeek'] }} this week</div>
</div>
</div>
{{-- Ready Videos --}}
<div class="col-6 col-lg-4 col-xl-2">
<div class="stat-card clickable-card" onclick="openDashModal('Live Videos','videos',{status:'ready'},'bi-check-circle-fill')" title="Click to view live videos">
<div class="stat-card-accent" style="background: linear-gradient(90deg,#22c55e,#4ade80);"></div>
<div class="stat-card-top">
<div class="stat-icon" style="background:rgba(34,197,94,.15);color:#4ade80;"><i class="bi bi-check-circle-fill"></i></div>
</div>
<div class="stat-value">{{ number_format($videosByStatus->get('ready', 0)) }}</div>
<div class="stat-label">Live Videos</div>
@php $failPct = $stats['totalVideos'] > 0 ? round(($failedCount/$stats['totalVideos'])*100,1) : 0; @endphp
<div class="stat-growth {{ $failPct > 5 ? 'down' : 'flat' }}">
<i class="bi bi-x-circle"></i> {{ $failedCount }} failed
</div>
<div class="stat-week">{{ $processingCount + $pendingCount }} in queue</div>
</div>
</div>
</div>
{{-- ── Activity Chart ── --}}
<div class="row g-3 mb-4">
<div class="col-lg-8">
<div class="dash-card">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="dash-card-title mb-0"><i class="bi bi-graph-up"></i> Activity Last 30 Days</h6>
<div class="d-flex gap-2" id="chartToggle">
<button class="btn btn-sm btn-outline-secondary active" data-ds="views">Views</button>
<button class="btn btn-sm btn-outline-secondary" data-ds="users">Users</button>
<button class="btn btn-sm btn-outline-secondary" data-ds="videos">Uploads</button>
</div>
</div>
<div class="chart-wrap">
<canvas id="activityChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="dash-card">
<h6 class="dash-card-title"><i class="bi bi-pie-chart-fill"></i> Content by Type</h6>
<div class="d-flex align-items-center gap-4" style="height: 180px;">
<div style="width: 140px; height: 140px; flex-shrink:0;">
<canvas id="typeChart"></canvas>
</div>
<div class="type-legend">
<div class="type-leg-item"><span class="type-leg-dot" style="background:#e61e1e;"></span> Generic &nbsp;<strong>{{ $videosByType->get('generic', 0) }}</strong></div>
<div class="type-leg-item"><span class="type-leg-dot" style="background:#f59e0b;"></span> Music &nbsp;<strong>{{ $videosByType->get('music', 0) }}</strong></div>
<div class="type-leg-item"><span class="type-leg-dot" style="background:#6366f1;"></span> Match &nbsp;<strong>{{ $videosByType->get('match', 0) }}</strong></div>
</div>
</div>
</div>
</div>
</div>
{{-- ── Top Content & Uploaders ── --}}
<div class="row g-3 mb-4">
<div class="col-lg-7">
<div class="dash-card">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="dash-card-title mb-0"><i class="bi bi-trophy-fill"></i> Top Videos by Views</h6>
<a href="{{ route('admin.videos') }}" class="btn btn-outline-secondary btn-sm">View All</a>
</div>
<table class="top-table">
<thead>
<tr>
<th>#</th>
<th colspan="2">Video</th>
<th>Views</th>
<th>Likes</th>
</tr>
</thead>
<tbody>
@forelse($topVideos as $i => $v)
<tr class="clickable-row" onclick="window.open('{{ route('videos.show', \App\Models\Video::encodeId($v->id)) }}','_blank')">
<td style="color:var(--text-secondary);font-weight:700;">{{ $i + 1 }}</td>
<td>
@if($v->thumbnail)
<img src="{{ route('media.thumbnail', $v->thumbnail) }}" class="top-thumb" alt="">
@else
<div class="top-thumb-placeholder"><i class="bi bi-play-fill"></i></div>
@endif
</td>
<td>
<a href="{{ route('videos.show', \App\Models\Video::encodeId($v->id)) }}" target="_blank" class="text-decoration-none text-white fw-500" style="font-size:13px;display:block;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ $v->title }}</a>
<small style="color:var(--text-secondary);">{{ $v->username }}</small>
</td>
<td><span style="color:#22d3ee;font-weight:600;">{{ number_format($v->view_count) }}</span></td>
<td><span style="color:#f472b6;font-weight:600;">{{ number_format($v->like_count) }}</span></td>
</tr>
@empty
<tr><td colspan="5" class="text-center" style="color:var(--text-secondary);padding:20px;">No videos yet</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="col-lg-5">
<div class="dash-card">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="dash-card-title mb-0"><i class="bi bi-person-fill"></i> Top Uploaders</h6>
<a href="{{ route('admin.users') }}" class="btn btn-outline-secondary btn-sm">View All</a>
</div>
<table class="top-table">
<thead>
<tr><th>#</th><th>User</th><th>Videos</th><th>Views</th></tr>
</thead>
<tbody>
@forelse($topUploaders as $i => $u)
<tr class="clickable-row" onclick="openDashModal('Videos by {{ addslashes($u->name) }}','videos',{uploader_id:{{ $u->id }}},'bi-person-fill')">
<td style="color:var(--text-secondary);font-weight:700;">{{ $i + 1 }}</td>
<td>
<div style="font-size:13px;font-weight:500;max-width:130px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ $u->name }}</div>
</td>
<td><span style="color:#818cf8;font-weight:600;">{{ $u->video_count }}</span></td>
<td><span style="color:#22d3ee;font-weight:600;">{{ number_format($u->total_views ?? 0) }}</span></td>
</tr>
@empty
<tr><td colspan="4" class="text-center" style="color:var(--text-secondary);padding:20px;">No users yet</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
{{-- ── Status / Visibility / Engagement ── --}}
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="dash-card">
<h6 class="dash-card-title"><i class="bi bi-sliders"></i> Processing Status</h6>
@php $totalV = max($stats['totalVideos'], 1); @endphp
@foreach([['ready','#22c55e'],['processing','#6366f1'],['pending','#f59e0b'],['failed','#ef4444']] as [$s,$color])
@php $cnt = $videosByStatus->get($s, 0); @endphp
<div class="status-row clickable-seg" style="cursor:pointer" onclick="openDashModal('{{ ucfirst($s) }} Videos','videos',{status:'{{ $s }}'},'bi-sliders')" title="{{ $cnt }} {{ $s }} videos">
<div class="status-label">{{ ucfirst($s) }}</div>
<div class="status-bar-wrap">
<div class="status-bar" style="width:{{ round(($cnt/$totalV)*100,1) }}%;background:{{ $color }};"></div>
</div>
<div class="status-count">{{ $cnt }}</div>
</div>
@endforeach
</div>
</div>
<div class="col-md-4">
<div class="dash-card">
<h6 class="dash-card-title"><i class="bi bi-shield-lock-fill"></i> Visibility</h6>
@foreach([['public','#22c55e'],['unlisted','#f59e0b'],['private','#6b7280']] as [$v,$color])
@php $cnt = $videosByVisibility->get($v, 0); @endphp
<div class="status-row clickable-seg" style="cursor:pointer" onclick="openDashModal('{{ ucfirst($v) }} Videos','videos',{visibility:'{{ $v }}'},'bi-shield-lock-fill')" title="{{ $cnt }} {{ $v }} videos">
<div class="status-label">{{ ucfirst($v) }}</div>
<div class="status-bar-wrap">
<div class="status-bar" style="width:{{ round(($cnt/$totalV)*100,1) }}%;background:{{ $color }};"></div>
</div>
<div class="status-count">{{ $cnt }}</div>
</div>
@endforeach
</div>
</div>
<div class="col-md-4">
<div class="dash-card">
<h6 class="dash-card-title"><i class="bi bi-bar-chart-fill"></i> Engagement Metrics</h6>
<div class="engage-item">
<div class="engage-name">Avg views / video</div>
<div class="engage-val">{{ number_format($avgViewsPerVideo, 1) }}</div>
</div>
<div class="engage-item">
<div class="engage-name">Like-to-view ratio</div>
<div class="engage-val">{{ $likeToViewRatio }}%</div>
</div>
<div class="engage-item">
<div class="engage-name">Avg uploads / user</div>
<div class="engage-val">{{ number_format($avgVideosPerUser, 1) }}</div>
</div>
</div>
</div>
</div>
{{-- ── Viewers by Country ── --}}
<div class="row g-3 mb-4">
<div class="col-lg-5">
<div class="dash-card" style="height:100%;">
<h6 class="dash-card-title"><i class="bi bi-globe2"></i> Viewers by Country</h6>
@if($viewsByCountry->isEmpty())
<div style="text-align:center;padding:40px 0;color:var(--text-secondary);font-size:13px;">
<i class="bi bi-globe" style="font-size:32px;display:block;margin-bottom:8px;"></i>
No geo data yet new views will be tracked automatically.
</div>
@else
@php $maxViews = $viewsByCountry->first()->total; @endphp
@foreach($viewsByCountry as $i => $row)
<div class="country-row clickable-seg" style="cursor:pointer" onclick="openDashModal('Viewers from {{ addslashes($row->country_name ?? $row->country ?? 'Unknown') }}','country_viewers',{country:'{{ $row->country }}'},'bi-globe2')">
<div class="country-rank">{{ $i + 1 }}</div>
<div class="country-flag" title="{{ $row->country }}">{{ $row->country ? countryCodeToFlag($row->country) : '🌍' }}</div>
<div class="country-name">{{ $row->country_name ?? 'Unknown' }}</div>
<div class="country-bar-wrap">
<div class="country-bar" style="width:{{ round(($row->total / $maxViews) * 100) }}%;"></div>
</div>
<div class="country-count">{{ number_format($row->total) }}</div>
</div>
@endforeach
@endif
</div>
</div>
<div class="col-lg-7">
<div class="dash-card" style="height:100%; display:flex; flex-direction:column;">
<h6 class="dash-card-title" style="flex-shrink:0;"><i class="bi bi-bar-chart-horizontal-fill"></i> Top Countries View Distribution</h6>
@if($viewsByCountry->isEmpty())
<div style="text-align:center;padding:60px 0;color:var(--text-secondary);font-size:13px;">Chart will appear once geo data is collected.</div>
@else
<div style="flex:1; min-height:200px; position:relative;">
<canvas id="countryChart"></canvas>
</div>
@endif
</div>
</div>
</div>
{{-- ── Recent Users & Videos ── --}}
<div class="row g-3 mb-4">
<div class="col-lg-6">
<div class="dash-card">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="dash-card-title mb-0"><i class="bi bi-person-plus-fill"></i> Recent Users</h6>
<a href="{{ route('admin.users') }}" class="btn btn-outline-secondary btn-sm">View All</a>
</div>
<table class="recent-table">
<thead><tr><th>User</th><th>Role</th><th>Joined</th></tr></thead>
<tbody>
@forelse($recentUsers as $user)
<tr class="clickable-row" onclick="openDashModal('Videos by {{ addslashes($user->name) }}','videos',{uploader_id:{{ $user->id }}},'bi-person-fill')">
<td>
<div class="d-flex align-items-center gap-2">
<img src="{{ $user->avatar_url }}" class="user-avatar-sm" alt="">
<div>
<div style="font-weight:500;">{{ $user->name }}</div>
<small style="color:var(--text-secondary);">{{ Str::limit($user->email, 22) }}</small>
</div>
</div>
</td>
<td>
@if($user->role === 'super_admin')
<span class="badge-role badge-super-admin">Super Admin</span>
@elseif($user->role === 'admin')
<span class="badge-role badge-admin">Admin</span>
@else
<span class="badge-role badge-user">User</span>
@endif
</td>
<td style="color:var(--text-secondary);font-size:12px;">{{ $user->created_at->diffForHumans() }}</td>
</tr>
@empty
<tr><td colspan="3" class="text-center" style="color:var(--text-secondary);padding:20px;">No users</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="col-lg-6">
<div class="dash-card">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="dash-card-title mb-0"><i class="bi bi-cloud-upload-fill"></i> Recent Uploads</h6>
<a href="{{ route('admin.videos') }}" class="btn btn-outline-secondary btn-sm">View All</a>
</div>
<table class="recent-table">
<thead><tr><th colspan="2">Video</th><th>Status</th><th>When</th></tr></thead>
<tbody>
@forelse($recentVideos as $video)
<tr class="clickable-row" onclick="window.open('{{ route('videos.show', $video) }}','_blank')">
<td>
@if($video->thumbnail)
<img src="{{ route('media.thumbnail', $video->thumbnail) }}" class="top-thumb" alt="">
@else
<div class="top-thumb-placeholder"><i class="bi bi-play-fill"></i></div>
@endif
</td>
<td>
<div style="max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;font-weight:500;">{{ $video->title }}</div>
<small style="color:var(--text-secondary);">{{ $video->user->name }}</small>
</td>
<td>
@switch($video->status)
@case('ready') <span class="badge-status badge-ready">Ready</span> @break
@case('processing') <span class="badge-status badge-processing">Processing</span> @break
@case('pending') <span class="badge-status badge-pending">Pending</span> @break
@case('failed') <span class="badge-status badge-failed">Failed</span> @break
@endswitch
</td>
<td style="color:var(--text-secondary);font-size:12px;">{{ $video->created_at->diffForHumans() }}</td>
</tr>
@empty
<tr><td colspan="4" class="text-center" style="color:var(--text-secondary);padding:20px;">No videos</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
{{-- ── Storage & Quick Actions ── --}}
<div class="row g-3 mb-4">
<div class="col-lg-6">
<div class="dash-card">
<h6 class="dash-card-title"><i class="bi bi-hdd-stack-fill"></i> Storage Breakdown</h6>
@php
$storageItems = [
['Videos', $storage['videos'], '#e61e1e'],
['Thumbnails', $storage['thumbnails'], '#f59e0b'],
['Avatars', $storage['avatars'], '#6366f1'],
['Images', $storage['images'], '#22c55e'],
];
$totalSt = max($storage['total'], 0.01);
@endphp
@foreach($storageItems as [$name, $mb, $color])
<div class="storage-item">
<div class="storage-dot" style="background:{{ $color }};"></div>
<div class="storage-name">{{ $name }}</div>
<div class="storage-bar-wrap">
<div class="storage-bar" style="width:{{ round(($mb/$totalSt)*100,1) }}%;background:{{ $color }};"></div>
</div>
<div class="storage-mb">{{ $mb }} MB</div>
</div>
@endforeach
<div class="d-flex align-items-center justify-content-between mt-3 pt-3" style="border-top:1px solid var(--border-color);">
<span style="color:var(--text-secondary);font-size:13px;">Total used</span>
<span style="font-weight:700;font-size:16px;">{{ $storage['total'] }} MB</span>
</div>
<button id="cleanupBtn" class="btn btn-danger w-100 mt-3">
<i class="bi bi-trash3-fill me-2"></i> Clean Orphaned Files
</button>
<div id="cleanupStatus" class="mt-2"></div>
@if(\App\Models\Setting::get('nas_sync_enabled', 'false') === 'true')
<button id="nasRepairBtn" class="btn btn-warning w-100 mt-2">
<i class="bi bi-arrow-repeat me-2"></i> Repair NAS Storage
</button>
<div id="nasRepairStatus" class="mt-2"></div>
@endif
</div>
</div>
<div class="col-lg-6">
<div class="dash-card">
<h6 class="dash-card-title"><i class="bi bi-lightning-fill"></i> Quick Actions</h6>
<div class="d-flex flex-column gap-2">
<a href="{{ route('admin.users') }}" class="quick-btn">
<i class="bi bi-people" style="color:#818cf8;"></i> Manage Users
</a>
<a href="{{ route('admin.videos') }}" class="quick-btn">
<i class="bi bi-play-circle" style="color:#ff6b6b;"></i> Manage Videos
</a>
@if($failedCount > 0)
<a href="{{ route('admin.videos') }}?status=failed" class="quick-btn" style="border-color:rgba(239,68,68,.4);">
<i class="bi bi-exclamation-triangle" style="color:#f87171;"></i>
Review {{ $failedCount }} Failed Video{{ $failedCount > 1 ? 's' : '' }}
</a>
@endif
<a href="{{ route('admin.logs') }}" class="quick-btn">
<i class="bi bi-terminal" style="color:#22d3ee;"></i> View System Logs
</a>
<a href="{{ route('videos.index') }}" target="_blank" class="quick-btn">
<i class="bi bi-box-arrow-up-right" style="color:#4ade80;"></i> Open Platform
</a>
</div>
</div>
</div>
</div>
{{-- ── Dash Modal ── --}}
<div class="dm-overlay" id="dashModal" onclick="if(event.target===this)closeDashModal()">
<div class="dm-sheet">
<div class="dm-header">
<div class="dm-title">
<i class="bi bi-circle-fill" id="dashModalIcon" style="font-size:10px;"></i>
<span id="dashModalTitle">Loading…</span>
</div>
<button class="dm-close" onclick="closeDashModal()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="dm-body" id="dashModalBody">
<div style="text-align:center;padding:48px;">
<div class="dm-spinner"></div>
<p style="color:var(--text-secondary);margin-top:14px;font-size:13px;">Loading…</p>
</div>
</div>
</div>
</div>
@endsection
@php
function countryCodeToFlag(string $code): string {
$code = strtoupper(trim($code));
if (strlen($code) !== 2) return '🌍';
$chars = '';
foreach (str_split($code) as $c) {
$chars .= mb_chr(0x1F1E6 + ord($c) - ord('A'));
}
return $chars;
}
@endphp
@section('scripts')
<script>
// ── Activity Line Chart ─────────────────────────────────────────
const labels = {!! $chartLabels !!};
const usersData = {!! $chartUsersData !!};
const videosData = {!! $chartVideosData !!};
const viewsData = {!! $chartViewsData !!};
const datasets = {
views: { label: 'Views', data: viewsData, borderColor: '#22d3ee', backgroundColor: 'rgba(34,211,238,.08)', tension: .4, fill: true, pointRadius: 2, pointHoverRadius: 5 },
users: { label: 'Users', data: usersData, borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,.08)', tension: .4, fill: true, pointRadius: 2, pointHoverRadius: 5 },
videos: { label: 'Uploads', data: videosData, borderColor: '#ff6b6b', backgroundColor: 'rgba(255,107,107,.08)', tension: .4, fill: true, pointRadius: 2, pointHoverRadius: 5 },
};
const activityChart = new Chart(document.getElementById('activityChart'), {
type: 'line',
data: { labels, datasets: [datasets.views] },
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, backgroundColor: '#1e1e1e', borderColor: '#303030', borderWidth: 1, titleColor: '#f1f1f1', bodyColor: '#aaa' } },
scales: {
x: { grid: { color: 'rgba(255,255,255,.05)' }, ticks: { color: '#666', maxTicksLimit: 8, font: { size: 11 } } },
y: { grid: { color: 'rgba(255,255,255,.05)' }, ticks: { color: '#666', font: { size: 11 } }, beginAtZero: true },
},
interaction: { mode: 'nearest', axis: 'x', intersect: false },
}
});
// Toggle buttons
document.getElementById('chartToggle').addEventListener('click', e => {
const btn = e.target.closest('button[data-ds]');
if (!btn) return;
document.querySelectorAll('#chartToggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activityChart.data.datasets = [datasets[btn.dataset.ds]];
activityChart.update();
});
// ── Type Doughnut ───────────────────────────────────────────────
new Chart(document.getElementById('typeChart'), {
type: 'doughnut',
data: {
labels: ['Generic','Music','Match'],
datasets: [{
data: [{{ $videosByType->get('generic',0) }}, {{ $videosByType->get('music',0) }}, {{ $videosByType->get('match',0) }}],
backgroundColor: ['#e61e1e','#f59e0b','#6366f1'],
borderColor: '#1e1e1e', borderWidth: 3,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1e1e1e', borderColor: '#303030', borderWidth: 1, titleColor: '#f1f1f1', bodyColor: '#aaa' } },
cutout: '68%',
}
});
// ── Country Chart ───────────────────────────────────────────────
@if($viewsByCountry->isNotEmpty())
(function() {
const countryData = @json($viewsByCountry);
const labels = countryData.map(r => r.country_name || r.country || '');
const values = countryData.map(r => r.total);
const maxVal = Math.max(...values);
const colors = values.map(v => `rgba(34,211,238,${(0.3 + 0.7 * v / maxVal).toFixed(2)})`);
const canvas = document.getElementById('countryChart');
const container = canvas.parentElement; // position:relative — flags overlay here
// Overlay real <img> flags — canvas ctx cannot render emoji flag sequences reliably.
// yAxis.right = left edge of bar area; flags sit in the ticks.padding gap before the bars.
function placeFlagOverlays() {
container.querySelectorAll('.cty-flag').forEach(e => e.remove());
const yAxis = chart.scales.y;
if (!yAxis) return;
const fW = 22, fH = 15;
// Place flag just to the left of the bar area (inside the ticks.padding gap)
const x = Math.round(yAxis.right - fW - 4);
countryData.forEach((row, i) => {
if (!row.country || row.country.length !== 2) return;
const yPx = Math.round(yAxis.getPixelForTick(i));
const img = document.createElement('img');
img.src = `https://flagcdn.com/w40/${row.country.toLowerCase()}.png`;
img.className = 'cty-flag';
img.style.cssText = [
'position:absolute',
`width:${fW}px`,
`height:${fH}px`,
`top:${yPx - Math.round(fH / 2)}px`,
`left:${x}px`,
'pointer-events:none',
'border-radius:3px',
'object-fit:cover',
'box-shadow:0 1px 3px rgba(0,0,0,.4)',
].join(';');
container.appendChild(img);
});
}
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels,
datasets: [{
data: values,
backgroundColor: colors,
borderColor: 'rgba(34,211,238,0.6)',
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
animation: { onComplete: placeFlagOverlays },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#1e1e1e', borderColor: '#303030', borderWidth: 1,
titleColor: '#f1f1f1', bodyColor: '#aaa',
callbacks: { label: ctx => ' ' + ctx.parsed.x.toLocaleString() + ' views' }
}
},
scales: {
x: {
grid: { color: 'rgba(255,255,255,.05)' },
ticks: { color: '#666', font: { size: 11 } },
beginAtZero: true,
},
y: {
grid: { display: false },
// padding must be >= flagWidth + gaps so labels don't overlap flags
ticks: { color: '#ccc', font: { size: 12 }, padding: 32 },
}
},
}
});
// Reposition overlays whenever the canvas resizes (responsive reflow)
new ResizeObserver(() => requestAnimationFrame(placeFlagOverlays)).observe(canvas);
})();
@endif
// ── Dashboard Modal ─────────────────────────────────────────────
const MODAL_URL = '{{ route("admin.dashboard.modal") }}';
const CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
function esc(str) {
if (str == null) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
function statusBadge(s) {
const map = { ready: ['#22c55e','Ready'], processing: ['#6366f1','Processing'], pending: ['#f59e0b','Pending'], failed: ['#ef4444','Failed'] };
const [color, label] = map[s] || ['#6b7280', s];
return `<span class="dm-badge" style="background:${color}22;color:${color};">${label}</span>`;
}
function roleBadge(r) {
const map = { super_admin: ['#f59e0b','Super Admin'], admin: ['#818cf8','Admin'], user: ['#6b7280','User'] };
const [color, label] = map[r] || ['#6b7280', r];
return `<span class="dm-badge" style="background:${color}22;color:${color};">${label}</span>`;
}
function thumb(url, title) {
if (url) return `<img src="${esc(url)}" alt="" style="width:52px;height:34px;object-fit:cover;border-radius:5px;">`;
return `<div style="width:52px;height:34px;border-radius:5px;background:#2a2a2a;display:flex;align-items:center;justify-content:center;color:#555;font-size:14px;"><i class="bi bi-play-fill"></i></div>`;
}
function renderUsersTable(items) {
if (!items.length) return '<p style="color:var(--text-secondary);text-align:center;padding:32px;">No users found.</p>';
const rows = items.map(u => `
<tr style="cursor:pointer;" onclick="window.open('${esc(u.url)}','_blank')">
<td><div style="display:flex;align-items:center;gap:10px;">
<img src="${esc(u.avatar)}" style="width:32px;height:32px;border-radius:50%;object-fit:cover;" alt="">
<div>
<div style="font-weight:500;font-size:13px;">${esc(u.name)}</div>
<div style="font-size:11px;color:var(--text-secondary);">${esc(u.email)}</div>
</div>
</div></td>
<td>${roleBadge(u.role)}</td>
<td style="color:var(--text-secondary);font-size:12px;white-space:nowrap;">${esc(u.joined)}</td>
</tr>`).join('');
return `<table class="dm-table"><thead><tr><th>User</th><th>Role</th><th>Joined</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderVideosTable(items) {
if (!items.length) return '<p style="color:var(--text-secondary);text-align:center;padding:32px;">No videos found.</p>';
const rows = items.map(v => `
<tr style="cursor:pointer;" onclick="window.open('${esc(v.url)}','_blank')">
<td>${thumb(v.thumbnail, v.title)}</td>
<td><div style="max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;font-size:13px;">${esc(v.title)}</div>
<div style="font-size:11px;color:var(--text-secondary);">${esc(v.owner)}</div></td>
<td>${statusBadge(v.status)}</td>
<td style="color:#22d3ee;font-weight:600;white-space:nowrap;">${Number(v.views).toLocaleString()}</td>
<td style="color:var(--text-secondary);font-size:12px;white-space:nowrap;">${esc(v.uploaded)}</td>
</tr>`).join('');
return `<table class="dm-table"><thead><tr><th></th><th>Video</th><th>Status</th><th>Views</th><th>Uploaded</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderTopVideosTable(items) {
if (!items.length) return '<p style="color:var(--text-secondary);text-align:center;padding:32px;">No videos found.</p>';
const rows = items.map((v, i) => `
<tr style="cursor:pointer;" onclick="window.open('${esc(v.url)}','_blank')">
<td style="color:var(--text-secondary);font-weight:700;width:28px;">${i+1}</td>
<td>${thumb(v.thumbnail, v.title)}</td>
<td><div style="max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;font-size:13px;">${esc(v.title)}</div>
<div style="font-size:11px;color:var(--text-secondary);">${esc(v.owner)}</div></td>
<td style="color:#22d3ee;font-weight:600;white-space:nowrap;">${Number(v.views).toLocaleString()}</td>
<td style="color:#f472b6;font-weight:600;white-space:nowrap;">${Number(v.likes).toLocaleString()}</td>
</tr>`).join('');
return `<table class="dm-table"><thead><tr><th>#</th><th></th><th>Video</th><th>Views</th><th>Likes</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderViewsTable(items) {
if (!items.length) return '<p style="color:var(--text-secondary);text-align:center;padding:32px;">No views found.</p>';
const rows = items.map(v => `
<tr style="cursor:pointer;" onclick="window.open('${esc(v.url)}','_blank')">
<td style="max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;font-weight:500;">${esc(v.video)}</td>
<td style="font-size:13px;color:var(--text-secondary);">${esc(v.viewer)}</td>
<td style="font-size:13px;">${esc(v.country)}</td>
<td style="font-size:12px;color:var(--text-secondary);white-space:nowrap;">${esc(v.time)}</td>
</tr>`).join('');
return `<table class="dm-table"><thead><tr><th>Video</th><th>Viewer</th><th>Country</th><th>When</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderCommentsTable(items) {
if (!items.length) return '<p style="color:var(--text-secondary);text-align:center;padding:32px;">No comments found.</p>';
const rows = items.map(c => `
<tr style="cursor:pointer;" onclick="window.open('${esc(c.url)}','_blank')">
<td style="font-size:13px;font-weight:500;white-space:nowrap;">${esc(c.user)}</td>
<td style="font-size:12px;color:var(--text-secondary);max-width:240px;">${esc(c.body)}</td>
<td style="font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;">${esc(c.video)}</td>
<td style="font-size:12px;color:var(--text-secondary);white-space:nowrap;">${esc(c.time)}</td>
</tr>`).join('');
return `<table class="dm-table"><thead><tr><th>User</th><th>Comment</th><th>Video</th><th>When</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderUploadersTable(items) {
if (!items.length) return '<p style="color:var(--text-secondary);text-align:center;padding:32px;">No uploaders found.</p>';
const rows = items.map((u, i) => `
<tr style="cursor:pointer;" onclick="window.open('${esc(u.url)}','_blank')">
<td style="color:var(--text-secondary);font-weight:700;width:28px;">${i+1}</td>
<td><div style="display:flex;align-items:center;gap:8px;">
<img src="${esc(u.avatar)}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;" alt="">
<span style="font-size:13px;font-weight:500;">${esc(u.name)}</span>
</div></td>
<td style="color:#818cf8;font-weight:600;">${Number(u.videos).toLocaleString()}</td>
<td style="color:#22d3ee;font-weight:600;">${Number(u.views).toLocaleString()}</td>
</tr>`).join('');
return `<table class="dm-table"><thead><tr><th>#</th><th>User</th><th>Videos</th><th>Views</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderDashModal(data) {
const body = document.getElementById('dashModalBody');
let html = '';
switch (data.type) {
case 'users': html = renderUsersTable(data.items); break;
case 'videos': html = renderVideosTable(data.items); break;
case 'top_videos': html = renderTopVideosTable(data.items); break;
case 'views': html = renderViewsTable(data.items); break;
case 'comments': html = renderCommentsTable(data.items); break;
case 'uploaders': html = renderUploadersTable(data.items); break;
default: html = '<p style="color:var(--text-secondary);padding:24px;">Unknown response type.</p>';
}
body.innerHTML = html;
}
function openDashModal(title, resource, params, icon) {
const modal = document.getElementById('dashModal');
const titleEl = document.getElementById('dashModalTitle');
const iconEl = document.getElementById('dashModalIcon');
const body = document.getElementById('dashModalBody');
titleEl.textContent = title;
iconEl.className = 'bi ' + (icon || 'bi-circle-fill');
body.innerHTML = '<div style="text-align:center;padding:48px;"><div class="dm-spinner"></div><p style="color:var(--text-secondary);margin-top:14px;font-size:13px;">Loading…</p></div>';
modal.classList.add('open');
document.body.style.overflow = 'hidden';
const qs = new URLSearchParams({ resource, ...params });
fetch(MODAL_URL + '?' + qs.toString(), {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF, 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin',
})
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(data => renderDashModal(data))
.catch(err => {
body.innerHTML = `<p style="color:#f87171;text-align:center;padding:32px;"><i class="bi bi-exclamation-triangle-fill me-2"></i>Failed to load: ${esc(err.message)}</p>`;
});
}
function closeDashModal() {
document.getElementById('dashModal').classList.remove('open');
document.body.style.overflow = '';
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeDashModal(); });
// ── Chart click handlers ─────────────────────────────────────────
const chartDatesRaw = {!! $chartDatesRaw ?? '[]' !!};
activityChart.options.onClick = function(evt, elements) {
if (!elements.length) return;
const idx = elements[0].index;
const date = chartDatesRaw[idx];
if (!date) return;
const activeDs = activityChart.data.datasets[0];
const ds = document.querySelector('#chartToggle button.active')?.dataset.ds ?? 'views';
if (ds === 'users') {
openDashModal('Users joined on ' + date, 'users', { date }, 'bi-people-fill');
} else if (ds === 'videos') {
openDashModal('Uploads on ' + date, 'videos', { date }, 'bi-play-circle-fill');
} else {
openDashModal('Views on ' + date, 'views_day', { date }, 'bi-eye-fill');
}
};
activityChart.update();
// Type doughnut click
const typeChartInst = Chart.getChart('typeChart');
if (typeChartInst) {
typeChartInst.options.onClick = function(evt, elements) {
if (!elements.length) return;
const types = ['generic', 'music', 'match'];
const labels = ['Generic Videos', 'Music Videos', 'Match Videos'];
const icons = ['bi-play-circle-fill', 'bi-music-note-beamed', 'bi-shield-fill'];
const idx = elements[0].index;
openDashModal(labels[idx], 'videos', { type: types[idx] }, icons[idx]);
};
typeChartInst.update();
}
// Country bar chart click
const countryChartInst = Chart.getChart('countryChart');
if (countryChartInst) {
const countryData = @json($viewsByCountry ?? collect());
countryChartInst.options.onClick = function(evt, elements) {
if (!elements.length) return;
const row = countryData[elements[0].index];
if (!row) return;
openDashModal('Viewers from ' + (row.country_name || row.country), 'country_viewers', { country: row.country }, 'bi-globe2');
};
countryChartInst.update();
}
// ── Cleanup ─────────────────────────────────────────────────────
document.getElementById('cleanupBtn')?.addEventListener('click', async function() {
this.disabled = true;
this.innerHTML = '<i class="bi bi-gear-fill me-2"></i> Cleaning...';
const status = document.getElementById('cleanupStatus');
try {
const res = await fetch('{{ url("/admin/cleanup-orphans") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content, 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
if (data.success) {
status.innerHTML = `<div class="alert alert-success mt-2 py-2 px-3" style="font-size:13px;">✅ ${data.message} — Videos: ${data.stats.videos_dir_size_mb} MB | Total: ${data.stats.total_public_size_mb} MB</div>`;
setTimeout(() => location.reload(), 2500);
} else throw new Error(data.message || 'Failed');
} catch(e) {
status.innerHTML = `<div class="alert alert-danger mt-2 py-2 px-3" style="font-size:13px;">❌ ${e.message}</div>`;
}
this.disabled = false;
this.innerHTML = '<i class="bi bi-trash3-fill me-2"></i> Clean Orphaned Files';
});
// ── NAS Repair ──────────────────────────────────────────────────
document.getElementById('nasRepairBtn')?.addEventListener('click', async function() {
this.disabled = true;
this.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i> Scanning…';
const status = document.getElementById('nasRepairStatus');
status.innerHTML = '';
try {
const res = await fetch('{{ url("/admin/nas-repair") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content, 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
const cls = data.success ? 'alert-success' : 'alert-warning';
const icon = data.success ? '✅' : '⚠️';
let html = `<div class="alert ${cls} mt-2 py-2 px-3" style="font-size:13px;">${icon} ${data.message}`;
if (data.details && data.details.length) {
html += `<ul class="mb-0 mt-1 ps-3" style="font-size:12px;">` + data.details.map(d => `<li>${d}</li>`).join('') + `</ul>`;
}
html += `</div>`;
status.innerHTML = html;
} catch(e) {
status.innerHTML = `<div class="alert alert-danger mt-2 py-2 px-3" style="font-size:13px;">❌ ${e.message}</div>`;
}
this.disabled = false;
this.innerHTML = '<i class="bi bi-arrow-repeat me-2"></i> Repair NAS Storage';
});
</script>
@endsection