Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,373 @@
@extends('admin.layouts.app')
@section('title', 'Sağlık Kontrolü')
@section('page-title', 'Sağlık Kontrolü')
@push('styles')
<style>
.health-card {
background:var(--bg2);
border-radius:var(--radius);
margin-bottom:16px;
overflow:hidden;
}
.health-card-header {
display:flex; align-items:center; gap:10px;
padding:14px 20px;
border-bottom:1px solid var(--border);
}
.health-card-header .badge { margin-left:auto; }
.health-card-body { padding:0; }
.health-row {
padding:14px 20px;
border-bottom:1px solid var(--border);
display:flex; align-items:center; flex-wrap:wrap; gap:12px;
}
.health-row:last-child { border-bottom:none; }
.health-ok { padding:14px 20px; color:var(--success); font-size:13.5px; display:flex; align-items:center; gap:7px; }
</style>
@endpush
@section('content')
<div class="page-header">
<div>
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Sağlık Kontrolü</h5>
<div style="font-size:13px;color:var(--text2);margin-top:2px">{{ now()->format('d.m.Y H:i') }}</div>
</div>
</div>
{{-- Özet Kartlar --}}
<div class="row g-3 mb-4">
@php
$checks = [
['icon'=>'bi-copy','label'=>'Duplike Anime','value'=>$malDuplicates->count(),'danger'=>$malDuplicates->count()>0],
['icon'=>'bi-shuffle','label'=>'Karışık Kaynaklı','value'=>$mixedSources->count(),'warn'=>$mixedSources->count()>0],
['icon'=>'bi-collection-play','label'=>'Eksik Bölümlü','value'=>$missingEpisodes->count(),'warn'=>$missingEpisodes->count()>0],
['icon'=>'bi-cloud','label'=>'Harici CDN Bölüm','value'=>number_format($externalCount),'info'=>$externalCount>0],
['icon'=>'bi-trash3','label'=>'0 Bölümlü Anime','value'=>$zeroEpisodeAnimes->count(),'danger'=>$zeroEpisodeAnimes->count()>0],
];
@endphp
@foreach($checks as $c)
<div class="col-6 col-md-4 col-lg">
<div class="stat-card" style="border-color:{{ ($c['danger']??false)?'rgba(239,68,68,.4)':(($c['warn']??false)?'rgba(245,158,11,.4)':(($c['info']??false)?'rgba(6,182,212,.4)':'rgba(16,185,129,.3)')) }}">
<div class="stat-card-top">
<div>
<div class="stat-value" style="color:{{ ($c['danger']??false)?'var(--danger)':(($c['warn']??false)?'var(--warning)':(($c['info']??false)?'var(--info)':'var(--success)')) }}">{{ $c['value'] }}</div>
<div class="stat-label mt-1">{{ $c['label'] }}</div>
</div>
<div class="stat-icon {{ ($c['danger']??false)?'red':(($c['warn']??false)?'orange':(($c['info']??false)?'cyan':'green')) }}">
<i class="bi {{ $c['icon'] }}"></i>
</div>
</div>
</div>
</div>
@endforeach
</div>
{{-- 1. Duplike --}}
<div class="health-card" style="border:1px solid {{ $malDuplicates->count()>0 ? 'rgba(239,68,68,.3)' : 'var(--border)' }}">
<div class="health-card-header">
<i class="bi bi-copy" style="color:var(--danger)"></i>
<span style="font-size:14px;font-weight:700;color:var(--text)">Duplike Animeler (MAL ID)</span>
<span class="badge {{ $malDuplicates->count()>0 ? 'bg-danger' : 'bg-success' }}">{{ $malDuplicates->count() }}</span>
</div>
<div class="health-card-body">
@forelse($malDuplicates as $dup)
<div class="health-row flex-column align-items-start">
<div style="font-size:12.5px;color:var(--text2)">MAL ID: <strong style="color:var(--warning)">{{ $dup['mal_id'] }}</strong></div>
<div class="table-wrap w-100">
<div class="table-responsive">
<table class="table">
<thead><tr><th>ID</th><th>Başlık</th><th style="text-align:center">Bölüm</th><th>Eklenme</th><th></th></tr></thead>
<tbody>
@foreach($dup['animes'] as $anime)
<tr>
<td style="color:var(--text2);font-size:12px">#{{ $anime->id }}</td>
<td>
<a href="{{ route('admin.animes.show',$anime->slug) }}" target="_blank"
style="color:var(--info);text-decoration:none;font-size:13px;font-weight:600">
{{ $anime->title }} <i class="bi bi-box-arrow-up-right" style="font-size:10px"></i>
</a>
</td>
<td style="text-align:center"><span class="badge bg-secondary">{{ $anime->episodes_count }}</span></td>
<td style="font-size:12px;color:var(--text2)">{{ $anime->created_at->format('d.m.Y') }}</td>
<td>
<form method="POST" action="{{ route('admin.health.delete-anime',$anime) }}"
onsubmit="return confirm('{{ addslashes($anime->title) }} silinsin mi?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i></button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@empty
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> Duplike anime yok.</div>
@endforelse
</div>
</div>
{{-- 2. Karışık Kaynak --}}
<div class="health-card" style="border:1px solid {{ $mixedSources->count()>0 ? 'rgba(245,158,11,.3)' : 'var(--border)' }}">
<div class="health-card-header">
<i class="bi bi-shuffle" style="color:var(--warning)"></i>
<span style="font-size:14px;font-weight:700;color:var(--text)">Karışık Kaynaklı Animeler</span>
<span style="font-size:12px;color:var(--text2)">(hem animecix hem anizium)</span>
<span class="badge {{ $mixedSources->count()>0 ? 'bg-warning' : 'bg-success' }}" style="{{ $mixedSources->count()>0 ? 'color:#000' : '' }}">{{ $mixedSources->count() }}</span>
</div>
<div class="health-card-body">
@forelse($mixedSources as $item)
<div class="health-row">
<div style="flex:1;min-width:0">
<a href="{{ route('admin.animes.show',$item['anime']->slug) }}" target="_blank"
style="color:var(--info);font-size:13.5px;font-weight:700;text-decoration:none">
{{ $item['anime']->title }}
</a>
<div class="d-flex gap-2 flex-wrap mt-1">
@foreach($item['sources'] as $src => $cnt)
<span class="badge bg-secondary" style="font-size:11px">{{ $src }}: {{ $cnt }} bölüm</span>
@endforeach
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
@foreach($item['sources'] as $src => $cnt)
<form method="POST" action="{{ route('admin.health.delete-source-episodes',$item['anime']) }}"
onsubmit="return confirm('{{ addslashes($item['anime']->title) }} — {{ $src }} bölümleri ({{ $cnt }} adet) silinsin mi?')">
@csrf
<input type="hidden" name="source" value="{{ $src }}">
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i> {{ $src }}</button>
</form>
@endforeach
</div>
</div>
@empty
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> Karışık kaynaklı anime yok.</div>
@endforelse
</div>
</div>
{{-- 3. Eksik Bölümler --}}
<div class="health-card" style="border:1px solid {{ $missingEpisodes->count()>0 ? 'rgba(245,158,11,.3)' : 'var(--border)' }}">
<div class="health-card-header">
<i class="bi bi-collection-play" style="color:var(--warning)"></i>
<span style="font-size:14px;font-weight:700;color:var(--text)">Eksik Bölümlü Animeler</span>
<span class="badge {{ $missingEpisodes->count()>0 ? 'bg-warning' : 'bg-success' }}" style="{{ $missingEpisodes->count()>0 ? 'color:#000' : '' }}">{{ $missingEpisodes->count() }}</span>
</div>
<div class="health-card-body">
@if($missingEpisodes->count())
<div class="table-responsive">
<table class="table">
<thead><tr><th style="padding-left:20px">Anime</th><th style="text-align:center">Beklenen</th><th style="text-align:center">Mevcut</th><th style="text-align:center">Eksik</th><th style="padding-right:20px"></th></tr></thead>
<tbody>
@foreach($missingEpisodes as $item)
<tr>
<td style="padding-left:20px;font-size:13px;font-weight:700;color:var(--text)">{{ $item['title'] }}</td>
<td style="text-align:center;font-size:13px">{{ $item['expected'] }}</td>
<td style="text-align:center;font-size:13px;color:var(--text2)">{{ $item['actual'] }}</td>
<td style="text-align:center"><span class="badge {{ $item['missing']>5?'bg-danger':'bg-warning' }}" style="{{ $item['missing']>5?'':'color:#000' }}">-{{ $item['missing'] }}</span></td>
<td style="padding-right:20px;text-align:right">
<a href="{{ route('admin.episodes.index',['anime_id'=>$item['id']]) }}" class="btn btn-sm btn-secondary" target="_blank">
<i class="bi bi-list"></i> Bölümler
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> Eksik bölümlü anime yok.</div>
@endif
</div>
</div>
{{-- 4. Sıfır Bölümlü --}}
<div class="health-card" style="border:1px solid {{ $zeroEpisodeAnimes->count()>0 ? 'rgba(239,68,68,.3)' : 'var(--border)' }}">
<div class="health-card-header">
<i class="bi bi-trash3" style="color:var(--danger)"></i>
<span style="font-size:14px;font-weight:700;color:var(--text)">0 Bölümlü Animeler</span>
<span class="badge {{ $zeroEpisodeAnimes->count()>0 ? 'bg-danger' : 'bg-success' }}">{{ $zeroEpisodeAnimes->count() }}</span>
@if($zeroEpisodeAnimes->count() > 0)
<button class="btn btn-sm btn-danger" onclick="destroyZero()" id="btn-zero-health" style="margin-left:auto">
<i class="bi bi-trash3"></i> Tümünü Sil
</button>
@endif
</div>
<div class="health-card-body">
@if($zeroEpisodeAnimes->count())
<div class="table-responsive">
<table class="table">
<thead><tr><th style="padding-left:20px">Anime</th><th>Eklenme</th><th style="padding-right:20px;text-align:right">İşlem</th></tr></thead>
<tbody>
@foreach($zeroEpisodeAnimes as $anime)
<tr>
<td style="padding-left:20px;font-size:13px;font-weight:700;color:var(--text)">{{ $anime->title }}</td>
<td style="font-size:12.5px;color:var(--text2)">{{ $anime->created_at->format('d.m.Y') }}</td>
<td style="padding-right:20px;text-align:right">
<form method="POST" action="{{ route('admin.animes.destroy',$anime) }}"
onsubmit="return confirm('{{ addslashes($anime->title) }} silinsin mi?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i></button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> 0 bölümlü anime yok.</div>
@endif
</div>
</div>
{{-- ── Depolama & İnode Yönetimi ── --}}
<div class="card mt-4">
<div class="card-header d-flex align-items-center gap-2">
<i class="bi bi-hdd-fill" style="color:var(--warning)"></i>
<h6 class="mb-0">Depolama & İnode Temizliği</h6>
<button class="btn btn-sm btn-outline-secondary ms-auto" onclick="loadStorageStats()">
<i class="bi bi-arrow-clockwise"></i> Yenile
</button>
</div>
<div class="card-body">
<div class="alert alert-warning mb-3" style="font-size:13px">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>İnode uyarısı:</strong> Sunucuda 188.000+ inode kullanılıyor. En büyük tüketiciler:
HLS segment cache, file session'lar, Laravel cache dosyaları.
</div>
<div id="storage-stats-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px">
<div class="text-center p-2" style="background:var(--bg);border:1px solid var(--border);border-radius:8px">
<div style="font-size:20px;font-weight:700" id="ss-seg"></div>
<div style="font-size:11px;color:var(--text2)">HLS Segment Cache</div>
<div style="font-size:10px;color:var(--text3)" id="ss-seg-size"></div>
</div>
<div class="text-center p-2" style="background:var(--bg);border:1px solid var(--border);border-radius:8px">
<div style="font-size:20px;font-weight:700" id="ss-sessions"></div>
<div style="font-size:11px;color:var(--text2)">Session Dosyaları</div>
<div style="font-size:10px;color:var(--text3)" id="ss-driver"></div>
</div>
<div class="text-center p-2" style="background:var(--bg);border:1px solid var(--border);border-radius:8px">
<div style="font-size:20px;font-weight:700" id="ss-cache"></div>
<div style="font-size:11px;color:var(--text2)">Laravel Cache</div>
<div style="font-size:10px;color:var(--text3)" id="ss-views"> views</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">
<button class="btn btn-sm btn-outline-danger" onclick="cleanStorage('seg_cache')">
<i class="bi bi-trash3 me-1"></i>HLS Cache Temizle
<small class="d-block" style="font-size:10px;opacity:.7">storage/app/seg_cache/*</small>
</button>
<button class="btn btn-sm btn-outline-warning" onclick="cleanStorage('sessions')">
<i class="bi bi-person-x me-1"></i>Session'ları Temizle
<small class="d-block" style="font-size:10px;opacity:.7">storage/framework/sessions/*</small>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="cleanStorage('cache_data')">
<i class="bi bi-database-x me-1"></i>Laravel Cache Temizle
<small class="d-block" style="font-size:10px;opacity:.7">storage/framework/cache/data/*</small>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="cleanStorage('views')">
<i class="bi bi-file-code me-1"></i>Derlenmiş View Temizle
<small class="d-block" style="font-size:10px;opacity:.7">storage/framework/views/*</small>
</button>
</div>
<div id="cleanup-result" style="display:none;margin-top:10px;font-size:12px;padding:8px 12px;border-radius:6px;background:var(--bg);border:1px solid var(--border)"></div>
<div class="mt-3 p-3" style="background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:12px">
<strong>Session sürücüsünü değiştir .env</strong>
<div style="color:var(--text2);margin-top:4px">
File session'lar her ziyaretçi için 1 inode kullanır. Değiştirmek için <code>.env</code> dosyasında:
</div>
<code style="display:block;margin-top:6px;padding:6px;background:var(--bg2);border-radius:4px">SESSION_DRIVER=cookie</code>
<div style="color:var(--text3);margin-top:4px">Cookie driver: 0 sunucu dosyası, anında inode tasarrufu. Sonra cache temizle.</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
async function loadStorageStats() {
try {
const d = await (await fetch('{{ route("admin.health.storage-stats") }}')).json();
const s = d.stats;
const fmt = (n) => n >= 1000 ? (n/1000).toFixed(1) + 'K' : n;
const fmtSize = (b) => b > 1048576 ? (b/1048576).toFixed(1) + ' MB' : b > 1024 ? (b/1024).toFixed(1) + ' KB' : b + ' B';
document.getElementById('ss-seg').textContent = fmt(s.seg_cache?.count || 0) + ' dosya';
document.getElementById('ss-seg-size').textContent = fmtSize(s.seg_cache?.size || 0);
const sessions = s.sessions?.count || 0;
document.getElementById('ss-sessions').textContent = fmt(sessions) + ' dosya';
document.getElementById('ss-sessions').style.color = sessions > 1000 ? 'var(--danger)' : sessions > 100 ? 'var(--warning)' : 'inherit';
const cache = s.cache_data?.count || 0;
document.getElementById('ss-cache').textContent = fmt(cache) + ' dosya';
const views = s.views?.count || 0;
document.getElementById('ss-views').textContent = views + ' view dosyası';
// Session driver
fetch('{{ route("admin.health.session-info") }}').then(r=>r.json()).then(sd => {
document.getElementById('ss-driver').textContent = 'Driver: ' + sd.driver;
if (sd.driver === 'file') document.getElementById('ss-driver').style.color = 'var(--danger)';
}).catch(()=>{});
} catch(e) {}
}
loadStorageStats();
async function cleanStorage(target) {
const labels = {
seg_cache: 'HLS segment cache',
sessions: 'session dosyaları',
cache_data: 'Laravel cache',
views: 'derlenmiş view dosyaları',
};
if (!confirm(`${labels[target] || target} temizlensin mi?`)) return;
const res = document.getElementById('cleanup-result');
res.style.display = 'block';
res.textContent = 'Temizleniyor...';
try {
const r = await fetch('{{ route("admin.health.cleanup-storage") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
body: JSON.stringify({ target }),
});
const d = await r.json();
res.innerHTML = `<span style="color:var(--success)">✓ ${d.message}</span>`;
loadStorageStats();
} catch(e) {
res.innerHTML = `<span style="color:var(--danger)">Hata: ${e.message}</span>`;
}
}
async function destroyZero() {
if (!confirm('0 bölümlü tüm animeler silinsin mi? Bu işlem geri alınamaz!')) return;
const btn = document.getElementById('btn-zero-health');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:12px;height:12px"></span>';
try {
const res = await fetch('{{ route("admin.animes.destroy-zero-episodes") }}', {
method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Accept':'application/json'}
});
const d = await res.json();
if (d.success) { axToast(`${d.count} anime silindi.`, 'success'); setTimeout(()=>location.reload(),1200); }
else { axToast('Hata oluştu.', 'error'); btn.disabled=false; }
} catch(e) { axToast('Hata: '+e.message, 'error'); btn.disabled=false; }
}
</script>
@endpush