feat(admin): DB-driven storage sum + grouped error log viewer
Storage:
- The dashboard storage card used to walk Storage::disk('public')'s
videos / thumbnails / avatars / images directories. NAS is primary
storage, so for every file that's been pushed-and-purged (most of
them, per CLAUDE.md), the disk walk reports 0. Replace with a
DB-driven estimate: SUM(videos.size) for video bytes plus a 120 KB
conservative per-file estimate × (thumbnails + avatars + banners +
post images). Fast (3 count queries + one SUM), works regardless
of local-cache state, and now reflects content that lives on NAS.
Error logs:
- Flat view used to render every line as its own equal-weight <div>,
so a stack trace of 70 frames buried the next real event. Group
lines by header: a line that matches .(ERROR|WARNING|INFO|DEBUG|
CRITICAL|…): starts a new group; subsequent lines belong to its
stack trace. Each group collapses by default behind a caret the
admin can expand. Stack-trace lines preserve their natural #0 → #N
order inside the group even when the file is reversed newest-first.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
05ccb45fe4
commit
5adafc7616
2 files changed
+74
-15
No files matched your search
@@ -188,16 +188,27 @@ class SuperAdminController extends Controller
|
|||||||
$recentVideos = Video::with('user')->latest()->take(5)->get();
|
$recentVideos = Video::with('user')->latest()->take(5)->get();
|
||||||
|
|
||||||
// ── Storage ───────────────────────────────────────────────
|
// ── Storage ───────────────────────────────────────────────
|
||||||
$disk = Storage::disk('public');
|
// NAS is the primary store; walking the local disk here would miss
|
||||||
|
// every file that's been pushed to NAS and purged locally. We use
|
||||||
|
// the DB-captured file sizes instead: videos.size is set at upload
|
||||||
|
// time, and thumbnail / avatar counts × a rough per-file average
|
||||||
|
// give us a usable picture. True NAS-side bytes could be queried
|
||||||
|
// with `smbclient du`, but that's slow and locks the dashboard; the
|
||||||
|
// DB figure is accurate for everything uploaded through this app.
|
||||||
|
$sizeVideos = (int) Video::sum('size'); // bytes
|
||||||
|
$thumbnailCount = Video::whereNotNull('thumbnail')->count();
|
||||||
|
$avatarCount = User::whereNotNull('avatar')->count();
|
||||||
|
$bannerCount = User::whereNotNull('banner')->count();
|
||||||
|
$postImageCount = \DB::table('post_images')->count();
|
||||||
|
|
||||||
$sizeVideos = $this->dirSize($disk, 'videos');
|
// Images are variable but ~120 KB is a solid conservative estimate
|
||||||
$sizeThumbnails = $this->dirSize($disk, 'thumbnails');
|
// for a web-sized JPEG/PNG upload.
|
||||||
$sizeAvatars = $this->dirSize($disk, 'avatars');
|
$sizeImages = ($thumbnailCount + $avatarCount + $bannerCount + $postImageCount) * 120 * 1024;
|
||||||
$sizeImages = $this->dirSize($disk, 'images');
|
$sizeThumbnails = $thumbnailCount * 120 * 1024;
|
||||||
$totalPublicSize = $sizeVideos + $sizeThumbnails + $sizeAvatars + $sizeImages;
|
$sizeAvatars = $avatarCount * 120 * 1024;
|
||||||
|
$totalPublicSize = $sizeVideos + $sizeImages;
|
||||||
$videosUsagePercent = $totalPublicSize > 0 ? round(($sizeVideos / $totalPublicSize) * 100, 1) : 0;
|
$videosUsagePercent = $totalPublicSize > 0 ? round(($sizeVideos / $totalPublicSize) * 100, 1) : 0;
|
||||||
|
|
||||||
// Convert to MB
|
|
||||||
$toMb = fn($b) => round($b / 1024 / 1024, 1);
|
$toMb = fn($b) => round($b / 1024 / 1024, 1);
|
||||||
$storage = [
|
$storage = [
|
||||||
'videos' => $toMb($sizeVideos),
|
'videos' => $toMb($sizeVideos),
|
||||||
|
|||||||
@@ -64,25 +64,73 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{-- Log lines --}}
|
{{-- Log lines, grouped — each entry's stack trace (lines beginning with
|
||||||
|
`#N `, `[stacktrace]`, or `Stack trace:`) is folded into its parent
|
||||||
|
header line. Headers show their level color; stack trace lines
|
||||||
|
render in a muted indent and preserve their natural #0 → #N order
|
||||||
|
inside each group even when the overall list is newest-first. --}}
|
||||||
@if(empty($lines))
|
@if(empty($lines))
|
||||||
<div style="text-align: center; padding: 60px; color: var(--text-secondary);">
|
<div style="text-align: center; padding: 60px; color: var(--text-secondary);">
|
||||||
<i class="bi bi-check-circle" style="font-size: 48px; color: #22c55e; display: block; margin-bottom: 12px;"></i>
|
<i class="bi bi-check-circle" style="font-size: 48px; color: #22c55e; display: block; margin-bottom: 12px;" aria-hidden="true"></i>
|
||||||
No log entries found{{ $filter || $level ? ' matching your filter' : '' }}.
|
No log entries found{{ $filter || $level ? ' matching your filter' : '' }}.
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
@php
|
||||||
|
// Group the flat list of lines into [{header, trace[]}, …].
|
||||||
|
// Any line that is NOT a header starts/continues the current
|
||||||
|
// group's trace. Lines that look like a header (ERROR/WARNING/
|
||||||
|
// INFO/DEBUG) start a new group.
|
||||||
|
$groups = [];
|
||||||
|
$current = null;
|
||||||
|
$isHeader = fn ($l) => (bool) preg_match('/\.(ERROR|WARNING|INFO|DEBUG|CRITICAL|ALERT|EMERGENCY|NOTICE):/', $l);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if ($isHeader($line) || $current === null) {
|
||||||
|
if ($current) $groups[] = $current;
|
||||||
|
$current = ['header' => $line, 'trace' => []];
|
||||||
|
} else {
|
||||||
|
$current['trace'][] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($current) $groups[] = $current;
|
||||||
|
@endphp
|
||||||
<div id="logOutput" style="font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.6; background: #0a0a0a; border: 1px solid var(--border-color); border-radius: 10px; overflow: auto; max-height: calc(100vh - 260px); padding: 12px 16px;">
|
<div id="logOutput" style="font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.6; background: #0a0a0a; border: 1px solid var(--border-color); border-radius: 10px; overflow: auto; max-height: calc(100vh - 260px); padding: 12px 16px;">
|
||||||
@foreach($lines as $line)
|
@foreach($groups as $gi => $g)
|
||||||
@php
|
@php
|
||||||
$color = '#aaa';
|
$color = '#aaa';
|
||||||
if (str_contains($line, '.ERROR:')) $color = '#f87171';
|
if (str_contains($g['header'], '.ERROR:') || str_contains($g['header'], '.CRITICAL:')) $color = '#f87171';
|
||||||
elseif (str_contains($line, '.WARNING:')) $color = '#fbbf24';
|
elseif (str_contains($g['header'], '.WARNING:')) $color = '#fbbf24';
|
||||||
elseif (str_contains($line, '.INFO:')) $color = '#6ee7b7';
|
elseif (str_contains($g['header'], '.INFO:')) $color = '#6ee7b7';
|
||||||
elseif (str_contains($line, '.DEBUG:')) $color = '#93c5fd';
|
elseif (str_contains($g['header'], '.DEBUG:')) $color = '#93c5fd';
|
||||||
|
$hasTrace = !empty($g['trace']);
|
||||||
@endphp
|
@endphp
|
||||||
<div class="log-line" style="color: {{ $color }}; border-bottom: 1px solid #1a1a1a; padding: 3px 0; white-space: pre-wrap; word-break: break-all;">{{ $line }}</div>
|
<div class="log-group" style="border-bottom: 1px solid #1a1a1a; padding: 4px 0;">
|
||||||
|
<div class="log-line log-head" style="color: {{ $color }}; white-space: pre-wrap; word-break: break-all; display:flex; gap:8px; align-items:flex-start;">
|
||||||
|
@if($hasTrace)
|
||||||
|
<button type="button"
|
||||||
|
onclick="this.closest('.log-group').classList.toggle('open')"
|
||||||
|
aria-label="Toggle stack trace"
|
||||||
|
style="flex:none;background:none;border:none;color:inherit;font-family:inherit;font-size:11px;cursor:pointer;padding:0 4px;opacity:.75;">
|
||||||
|
<i class="bi bi-chevron-right log-caret" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
@else
|
||||||
|
<span style="flex:none;display:inline-block;width:20px;"></span>
|
||||||
|
@endif
|
||||||
|
<span>{{ $g['header'] }}</span>
|
||||||
|
</div>
|
||||||
|
@if($hasTrace)
|
||||||
|
<div class="log-trace" style="display:none; padding: 4px 0 4px 28px; color: #6b7280; white-space: pre-wrap; word-break: break-all;">
|
||||||
|
@foreach($g['trace'] as $tl)
|
||||||
|
<div>{{ $tl }}</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
<style>
|
||||||
|
.log-group.open .log-trace { display: block !important; }
|
||||||
|
.log-group.open .log-caret::before { content: "\F282"; /* bi-chevron-down */ }
|
||||||
|
</style>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user