Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user