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
+69 -2
View File
@@ -1,8 +1,10 @@
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Helpers;
@@ -38,6 +40,8 @@ public sealed class LogsPanelTests
window.Show();
window.UpdateLayout();
var vm = (MainViewModel)window.DataContext;
vm.EditingActive = false;
window.UpdateLayout();
var panel = (UserControl)window.FindName("LeftPanel")!;
var chatTab = (ToggleButton)panel.FindName("LiveChatTabButton")!;
var logsTab = (ToggleButton)panel.FindName("LogsTabButton")!;
@@ -46,16 +50,22 @@ public sealed class LogsPanelTests
Assert.True(vm.IsLiveChatTabSelected);
Assert.True(chatTab.IsChecked);
Assert.False(logsTab.IsChecked);
Assert.Equal(System.Windows.Visibility.Collapsed, logList.Visibility);
Assert.False(logList.IsVisible);
vm.IsLogsTabSelected = true;
window.UpdateLayout();
Assert.True(logsTab.IsChecked);
Assert.False(chatTab.IsChecked);
Assert.Equal(System.Windows.Visibility.Visible, logList.Visibility);
Assert.True(logList.IsVisible);
AppLog.Write("LogsPanel older marker");
AppLog.Write("LogsPanel newer marker");
AppLog.WriteWarning("LogsPanel warning marker");
AppLog.WriteSuccess("LogsPanel success marker");
AppLog.Write("LogsPanel class 2 marker " + new string('2', 240),
AppLogMessageType.Error, AppLogErrorClass.Class2);
AppLog.Write("LogsPanel class 1 marker " + new string('1', 240),
AppLogMessageType.Error, AppLogErrorClass.Class1);
vm.RefreshCurrentLaunchLogs();
window.UpdateLayout();
@@ -66,6 +76,47 @@ public sealed class LogsPanelTests
Assert.True(newerIndex < olderIndex, "the Logs tab must put the newest entry first");
Assert.DoesNotContain(entries, text => text.Contains("previous-launch-sentinel", StringComparison.Ordinal));
Assert.Same(vm.CurrentLaunchLogEntries, logList.ItemsSource);
var class1 = vm.PinnedLogEntries.Single(entry => entry.Text.Contains("LogsPanel class 1 marker", StringComparison.Ordinal));
var class2 = vm.PinnedLogEntries.Single(entry => entry.Text.Contains("LogsPanel class 2 marker", StringComparison.Ordinal));
Assert.True(Array.IndexOf(vm.PinnedLogEntries.ToArray(), class1)
< Array.IndexOf(vm.PinnedLogEntries.ToArray(), class2));
Assert.Equal("CLASS 1", class1.ErrorClassLabel);
Assert.Equal("CLASS 2", class2.ErrorClassLabel);
Assert.Equal("CLASS 1", class1.TypeLabel);
var pinnedPanel = (ItemsControl)panel.FindName("PinnedLogPanel")!;
Assert.Equal(System.Windows.Visibility.Visible, pinnedPanel.Visibility);
var class1Text = Find(pinnedPanel, element => element is TextBlock text
&& text.Text.Contains("LogsPanel class 1 marker", StringComparison.Ordinal)) as TextBlock;
var class2Text = Find(pinnedPanel, element => element is TextBlock text
&& text.Text.Contains("LogsPanel class 2 marker", StringComparison.Ordinal)) as TextBlock;
Assert.NotNull(class1Text);
Assert.NotNull(class2Text);
Assert.Equal(TextWrapping.Wrap, class1Text!.TextWrapping);
Assert.Equal(BrushColor("Brush.Error"), Assert.IsType<SolidColorBrush>(class1Text.Foreground).Color);
Assert.Equal(BrushColor("Brush.Attention"), Assert.IsType<SolidColorBrush>(class2Text!.Foreground).Color);
var warningText = Find(logList, element => element is TextBlock text
&& text.Text.Contains("LogsPanel warning marker", StringComparison.Ordinal)) as TextBlock;
var successText = Find(logList, element => element is TextBlock text
&& text.Text.Contains("LogsPanel success marker", StringComparison.Ordinal)) as TextBlock;
var infoText = Find(logList, element => element is TextBlock text
&& text.Text.Contains("LogsPanel newer marker", StringComparison.Ordinal)) as TextBlock;
Assert.NotNull(warningText);
Assert.NotNull(successText);
Assert.NotNull(infoText);
Assert.Equal(BrushColor("Brush.Secondary"), Assert.IsType<SolidColorBrush>(warningText!.Foreground).Color);
Assert.Equal(BrushColor("Brush.Positive"), Assert.IsType<SolidColorBrush>(successText!.Foreground).Color);
Assert.Equal(BrushColor("Brush.OnSurfaceVariant"), Assert.IsType<SolidColorBrush>(infoText!.Foreground).Color);
var class1Ack = Find(pinnedPanel, element => element is Button button
&& button.DataContext is AppLogEntry entry && entry.Sequence == class1.Sequence) as Button;
Assert.NotNull(class1Ack?.Command);
Assert.Same(class1, class1Ack!.CommandParameter);
class1Ack!.Command!.Execute(class1Ack.CommandParameter);
Assert.DoesNotContain(class1, vm.PinnedLogEntries);
Assert.Contains(class1, vm.CurrentLaunchLogEntries);
}
finally
{
@@ -81,4 +132,20 @@ public sealed class LogsPanelTests
try { Directory.Delete(directory, recursive: true); } catch { }
}
}
private static System.Windows.Media.Color BrushColor(string key)
=> Assert.IsType<SolidColorBrush>(Application.Current!.FindResource(key)).Color;
private static DependencyObject? Find(DependencyObject parent, Func<DependencyObject, bool> predicate)
{
if (predicate(parent)) return parent;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (predicate(child)) return child;
var found = Find(child, predicate);
if (found != null) return found;
}
return null;
}
}