Files
2026-07-14 00:01:48 +03:00

374 lines
19 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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