feat: add tiered storage guard (Green/Yellow/Red) and dashboard queue monitor

Storage Tier System:
- Add StorageTier enum (Green >30% / Yellow 10-30% / Red <10%)
- Extend StorageGuardResult with Tier, CanStartNewRecording, ShouldPauseActive, UsagePercent
- Yellow tier: deny new recordings but allow existing to finish and upload
- Red tier: deny new recordings and pause active sessions
- Auto-recovery: when disk frees up, polling automatically resumes new recordings
- Update LiveRoomPollingBackgroundService to use tier-based checks
- Expose tier + usage percent in Recovery API

Dashboard Queue Monitor:
- Add pending transcode count, pending upload count, queued data volume to dashboard
- Add storage tier badge (Green/Yellow/Red) with usage percentage
- Add queue monitoring card to dashboard view

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
This commit is contained in:
2026-06-04 23:13:26 +08:00
co-authored by Claude Opus 4.8 noreply@anthropic.com
parent cbee29bef9
commit 24e5cf2a06
12 changed files with 203 additions and 22 deletions
@@ -90,8 +90,13 @@ public sealed class DashboardService
{
HasEnoughSpace = storageCheck.HasEnoughSpace,
Message = storageCheck.Message ?? string.Empty,
AvailableBytes = storageCheck.AvailableBytes
AvailableBytes = storageCheck.AvailableBytes,
Tier = storageCheck.Tier.ToString(),
UsagePercent = storageCheck.UsagePercent
},
PendingTranscodeCount = await _recordTaskRepository.CountByStatusAsync(Domain.Enums.RecordTaskStatus.Processing, cancellationToken),
PendingUploadCount = await _recordResultRepository.CountPendingUploadAsync(cancellationToken),
QueuedDataBytes = await _recordResultRepository.SumPendingUploadBytesAsync(cancellationToken),
RecentSessions = recentSessionsTask.Result
.Select(MapRecentSession)
.ToList(),
@@ -207,8 +207,17 @@ public sealed class RecordService
var settings = await _systemSettingsService.GetAsync(cancellationToken);
var storageCheck = _storageGuardService.CheckCanStartOrResume(settings);
if (!storageCheck.HasEnoughSpace)
if (!storageCheck.CanStartNewRecording)
{
var storageMessage = storageCheck.Tier switch
{
Application.Abstractions.Storage.StorageTier.Yellow =>
"Storage is in warning state. New recordings are paused but existing recordings continue. Transcoding and uploading will free up space.",
Application.Abstractions.Storage.StorageTier.Red =>
"Storage is critically low. All recordings are paused until enough disk space is freed.",
_ => storageCheck.Message
};
await _systemLogService.WriteAsync(
SystemLogLevel.Warning,
"Storage",
@@ -222,12 +231,12 @@ public sealed class RecordService
await UpdateAutoStartDecisionAsync(
liveRoom,
AutoStartDecisionCodes.SkippedStorage,
"Auto-start skipped because storage is below threshold.",
$"Auto-start skipped because storage tier is {storageCheck.Tier}.",
storageCheck.Message,
cancellationToken);
}
throw new InvalidOperationException(storageCheck.Message);
throw new InvalidOperationException(storageMessage);
}
var effectiveSettings = _liveRoomRecordingSettingsResolver.Resolve(liveRoom, settings);
@@ -661,14 +670,14 @@ public sealed class RecordService
}
var storageCheck = _storageGuardService.CheckCanStartOrResume(settings);
if (!storageCheck.HasEnoughSpace)
if (!storageCheck.CanStartNewRecording)
{
foreach (var liveRoomId in liveRoomIds)
{
await TryUpdateAutoStartDecisionAsync(
liveRoomId,
AutoStartDecisionCodes.SkippedStorage,
"Auto-start skipped because storage is below threshold.",
$"Auto-start skipped because storage tier is {storageCheck.Tier}.",
storageCheck.Message,
cancellationToken);
await _systemLogService.WriteAsync(