feat(ui): color and acknowledge pinned log errors

Add independent AppLog message types and explicit Class 1/2 severity. Wrap and color log rows, pin Class 1 above Class 2, and let acknowledgement return entries to the current-launch timeline without changing file format or rotation.

Ref: https://logtape.org/manual/levels
This commit is contained in:
2026-11-02 18:17:18 -08:00
parent 01a3fcaaf7
commit a6aea5bf26
23 changed files with 365 additions and 100 deletions
@@ -159,7 +159,8 @@ public partial class MainViewModel : ViewModelBase
var stream = await _youtubeStream.GetOrCreateReusableStreamAsync();
if (stream == null)
{
AppLog.Write("Reusable stream unavailable; check the OAuth session");
AppLog.Write("Reusable stream unavailable; check the OAuth session",
AppLogMessageType.Error, AppLogErrorClass.Class2);
_notifications.Error("Couldn't go live",
"The reusable stream is unavailable — check the YouTube sign-in and try again.");
StopStream(); // full rollback — audio loop + recording + pills, no Error-status zombie
@@ -174,12 +175,13 @@ public partial class MainViewModel : ViewModelBase
_broadcastSeenLive = false;
if (_currentBroadcastId == null)
{
AppLog.Write("Broadcast creation failed; check the OAuth session");
AppLog.Write("Broadcast creation failed; check the OAuth session",
AppLogMessageType.Error, AppLogErrorClass.Class2);
_notifications.Error("Couldn't go live", "YouTube rejected the broadcast — check the sign-in and try again.");
StopStream(); // full rollback
return;
}
AppLog.Write($"Broadcast created: {_currentBroadcastId}");
AppLog.WriteSuccess($"Broadcast created: {_currentBroadcastId}");
OnPropertyChanged(nameof(CurrentBroadcastId));
// ⛔ THE BINDING — the call whose absence is why nothing ever went live
@@ -198,7 +200,8 @@ public partial class MainViewModel : ViewModelBase
var bindError = await _youtubeStream.BindBroadcastAsync(_currentBroadcastId, stream.Id);
if (bindError != null)
{
AppLog.Write($"Stream binding failed: {bindError}");
AppLog.Write($"Stream binding failed: {bindError}",
AppLogMessageType.Error, AppLogErrorClass.Class2);
// Show the real reason. "Check the sign-in" would be wrong for five of the
// six documented bind failures, and would hide our own ordering bug behind
// a plausible excuse — see BindFailureMessage.
@@ -216,7 +219,8 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception ex)
{
AppLog.Write($"Go-live preparation failed: {ex.Message}");
AppLog.Write($"Go-live preparation failed: {ex.Message}",
AppLogMessageType.Error, AppLogErrorClass.Class2);
_notifications.Error("Couldn't go live", ex.Message);
StopStream(); // full rollback — a created broadcast (if any) gets the close-out
return;
@@ -242,12 +246,14 @@ public partial class MainViewModel : ViewModelBase
}
else
{
AppLog.Write("Could not retrieve liveChatId; chat polling disabled");
AppLog.Write("Could not retrieve liveChatId; chat polling disabled",
AppLogMessageType.Error, AppLogErrorClass.Class2);
}
}
catch (Exception chatEx)
{
AppLog.Write($"Chat start failed (non-fatal): {chatEx.Message}");
AppLog.Write($"Chat start failed (non-fatal): {chatEx.Message}",
AppLogMessageType.Error, AppLogErrorClass.Class2);
}
// TASK 41: a test broadcast hands its session to the Test drawer once
@@ -285,7 +291,8 @@ public partial class MainViewModel : ViewModelBase
var health = await _youtubeStream.GetStreamHealthAsync(_reusableStream.Id);
if (health == null)
{
AppLog.Write("Stream health poll failed; check the OAuth session");
AppLog.Write("Stream health poll failed; check the OAuth session",
AppLogMessageType.Error, AppLogErrorClass.Class2);
return;
}
CurrentHealth.HealthStatus = health.HealthStatus;
@@ -303,7 +310,8 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception ex)
{
AppLog.Write($"Stream health poll failed: {ex.Message}");
AppLog.Write($"Stream health poll failed: {ex.Message}",
AppLogMessageType.Error, AppLogErrorClass.Class2);
}
}
@@ -375,7 +383,8 @@ public partial class MainViewModel : ViewModelBase
{
var closeOutError = await _youtubeStream.EndBroadcastAsync(endedBroadcastId);
if (closeOutError != null)
AppLog.Write($"End close-out: {closeOutError} — enableAutoStop will finish the broadcast");
AppLog.Write($"End close-out: {closeOutError} — enableAutoStop will finish the broadcast",
AppLogMessageType.Error, AppLogErrorClass.Class2);
}
_currentBroadcastId = null;
@@ -435,7 +444,7 @@ public partial class MainViewModel : ViewModelBase
// with IsRecording=true and no encoder (first real launch proved it). The
// failure now runs the NORMAL stop path on the UI thread (pills off, audio
// loop closed, recording finalized, close-out if live), then explains itself.
AppLog.Write($"Frame pump failed: {message}");
AppLog.Write($"Frame pump failed: {message}", AppLogMessageType.Error, AppLogErrorClass.Class2);
void Handle()
{
var live = IsLive;
@@ -567,7 +576,8 @@ public partial class MainViewModel : ViewModelBase
_notifications.Info("Broadcast deleted", "The broadcast was removed from YouTube.");
break;
case PrivateBroadcastOutcome.DeleteFailed:
AppLog.Write($"Private broadcast delete failed: {broadcastId}: {error}");
AppLog.Write($"Private broadcast delete failed: {broadcastId}: {error}",
AppLogMessageType.Error, AppLogErrorClass.Class2);
_notifications.Error("Couldn't delete the broadcast",
$"{error}. It's still on YouTube as a private video — you can delete it in YouTube Studio.");
break;