feat: add dashboard, file preview metadata, and bandwidth statistics

Dashboard:
- Add DashboardDto, DashboardService with SQL-level aggregate queries
- Add GET /api/dashboard endpoint with real-time system status
- Add repository aggregate methods (CountByAvailability, SumDuration, etc.)
- Add DashboardView.vue as new landing page with KPI cards, storage status, recent sessions, top rooms
- Update router to make dashboard the new / route, add nav item in sidebar

File Preview:
- Add IVideoMetadataService + FfmpegVideoMetadataService for video metadata extraction
- Extend MediaBrowserItemDto with Metadata and ThumbnailUrl fields
- Add includeMetadata param to media browser API, add thumbnail endpoint
- Add SessionPlaylistDto and GET /api/record-sessions/{id}/playlist for continuous playback

Bandwidth Statistics:
- Add -progress pipe:1 to live recording ffmpeg args for bitrate output
- Parse total_size/bitrate/speed from ffmpeg progress lines during recording
- Write bandwidth samples as SystemLogEntry (Category=Bandwidth) every 30s
- Add BandwidthStatisticsService, BandwidthController with session timeline + daily summary
- Add bandwidth TypeScript types

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
This commit is contained in:
2026-06-04 00:30:02 +08:00
co-authored by Claude Opus 4.8 noreply@anthropic.com
parent a5c2cc3202
commit cbee29bef9
22 changed files with 1392 additions and 27 deletions
@@ -8,6 +8,7 @@ import { useViewport } from "@/composables/useViewport";
import {
ArrowDown,
Bell,
DataAnalysis,
Document,
Fold,
House,
@@ -36,6 +37,7 @@ const navigationGroups = [
key: "monitor",
title: "直播监控",
items: [
{ index: "/", label: "仪表盘", icon: DataAnalysis },
{ index: "/live-rooms", label: "直播间", icon: House },
{ index: "/record-tasks", label: "录制任务", icon: VideoCamera },
{ index: "/recovery", label: "恢复中心", icon: RefreshRight }
@@ -72,6 +74,8 @@ const backendStatusDescription = computed(() =>
const backendStatusTone = computed(() => (backendUnavailable.value ? "is-danger" : "is-healthy"));
const pageEyebrow = computed(() => {
switch (route.name) {
case "dashboard":
return "系统概览";
case "live-rooms":
return "直播间监控";
case "record-tasks":