Skip to content

Commit bbf4ce2

Browse files
💾 Feat(Workflow): 添加实时活动日志,仅在调试版本中可见,记录工作流事件
1 parent 11e7d6e commit bbf4ce2

3 files changed

Lines changed: 116 additions & 1 deletion

File tree

KitX Dashboard/ViewModels/Pages/WorkflowPageViewModel.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ internal class WorkflowPageViewModel : ViewModelBase
2323
private readonly IWorkflowManagementService _workflowService;
2424
private readonly IEventService _eventService;
2525

26+
/// <summary>
27+
/// Real-time activity log shown at the bottom of the Workflow page. Only populated
28+
/// in Debug builds so Release users get a clean UI. Each entry is a timestamped line.
29+
/// Capped at 500 entries (older trimmed) to bound memory.
30+
/// </summary>
31+
#if DEBUG
32+
public ObservableCollection<string> ExecutionLog { get; } = new();
33+
public bool IsDebugLogVisible => true;
34+
#else
35+
public ObservableCollection<string> ExecutionLog { get; } = new();
36+
public bool IsDebugLogVisible => false;
37+
#endif
38+
39+
private const int MaxLogEntries = 500;
40+
41+
/// <summary>
42+
/// Appends a timestamped line to <see cref="ExecutionLog"/>. Safe to call from any
43+
/// thread; marshals onto the UI thread. No-op unless DEBUG.
44+
/// </summary>
45+
private void AppendLog(string message)
46+
{
47+
#if DEBUG
48+
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
49+
{
50+
var line = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
51+
ExecutionLog.Add(line);
52+
while (ExecutionLog.Count > MaxLogEntries)
53+
ExecutionLog.RemoveAt(0);
54+
});
55+
#endif
56+
}
57+
2658
public WorkflowPageViewModel()
2759
{
2860
_storageService = App.GetService<IWorkflowStorageService>();
@@ -70,6 +102,11 @@ public sealed override void InitCommands()
70102
{
71103
await LoadWorkflowsAsync();
72104
});
105+
106+
ClearLogCommand = ReactiveCommand.Create(() =>
107+
{
108+
ExecutionLog.Clear();
109+
});
73110
}
74111

75112
public sealed override void InitEvents()
@@ -138,7 +175,11 @@ private async Task LoadWorkflowsAsync()
138175
}
139176

140177
WorkflowCases.Add(w);
178+
AppendLog($"Mounted workflow '{w.Name}' (id={w.Id}, trigger={w.TriggerType}" +
179+
(w.TriggerConfig?.PluginName is { } pn && !string.IsNullOrEmpty(pn)
180+
? $":{pn}/{w.TriggerConfig.TriggerName}" : "") + ")");
141181
}
182+
AppendLog($"Loaded {workflows.Count} workflow(s)");
142183
}
143184

144185
/// <summary>
@@ -236,6 +277,7 @@ private async void DeleteWorkflowAsync(IWorkflowCase workflow)
236277
await _storageService.DeleteWorkflowAsync(workflow.Id);
237278
WorkflowCases.Remove(workflow);
238279
_eventService.Publish(EventNames.WorkflowDeleted, EventArgs.Empty);
280+
AppendLog($"[Delete] Removed workflow '{workflow.Name}' (id={workflow.Id})");
239281
}
240282
}
241283
catch (Exception ex)
@@ -267,6 +309,8 @@ private async void RunWorkflowAsync(IWorkflowCase workflow)
267309
workflow.IsError = true;
268310
workflow.ErrorMessage = $"Plugin '{workflow.TriggerConfig.PluginName}' is not connected";
269311
RefreshWorkflowInList(workflow);
312+
AppendLog($"[Trigger] '{workflow.Name}' requires plugin " +
313+
$"'{workflow.TriggerConfig.PluginName}' which is NOT connected — trigger not armed");
270314
return;
271315
}
272316

@@ -278,8 +322,14 @@ private async void RunWorkflowAsync(IWorkflowCase workflow)
278322
{
279323
var triggerManager = KitX.Core.DI.ServiceHost.GetRequiredService<KitX.Core.Contract.Workflow.ITriggerManager>();
280324
triggerManager?.RegisterWorkflowTrigger(workflow.Id, workflow.TriggerConfig);
325+
AppendLog($"[Trigger] '{workflow.Name}' armed — fires on " +
326+
$"'{workflow.TriggerConfig.PluginName}/{workflow.TriggerConfig.TriggerName}'");
327+
}
328+
catch (Exception trigEx)
329+
{
330+
AppendLog($"[Trigger] '{workflow.Name}' failed to register trigger: {trigEx.Message}");
331+
/* non-critical */
281332
}
282-
catch { /* non-critical */ }
283333

284334
RefreshWorkflowInList(workflow);
285335
}
@@ -289,6 +339,7 @@ private async void RunWorkflowAsync(IWorkflowCase workflow)
289339
workflow.IsError = false;
290340
workflow.ErrorMessage = null;
291341
RefreshWorkflowInList(workflow);
342+
AppendLog($"[Run] Manual run of '{workflow.Name}' (id={workflow.Id}) started");
292343

293344
// Offload to thread pool to prevent UI deadlock — script execution
294345
// may synchronously wait for plugin responses (PluginCall uses
@@ -335,6 +386,8 @@ private async void StopWorkflowAsync(IWorkflowCase workflow)
335386
workflow.ErrorMessage = null;
336387
workflow.IsRunning = false;
337388
RefreshWorkflowInList(workflow);
389+
AppendLog($"[Stop] '{workflow.Name}' stopped" +
390+
(workflow.TriggerConfig?.TriggerType == "PluginEvent" ? " (trigger disarmed)" : ""));
338391
}
339392
catch (Exception ex)
340393
{
@@ -365,6 +418,7 @@ private void OnWorkflowExecutionResult(object? sender, EventArgs e)
365418
if (workflow.TriggerConfig?.TriggerType != "PluginEvent")
366419
workflow.IsRunning = false;
367420
RefreshWorkflowInList(workflow);
421+
AppendLog($"[Done] '{workflow.Name}' completed successfully");
368422
}
369423
else
370424
{
@@ -375,6 +429,7 @@ private void OnWorkflowExecutionResult(object? sender, EventArgs e)
375429
if (workflow.TriggerConfig?.TriggerType != "PluginEvent")
376430
workflow.IsRunning = false;
377431
RefreshWorkflowInList(workflow);
432+
AppendLog($"[Error] '{workflow.Name}' failed: {args.ErrorMessage}");
378433
}
379434
});
380435
}
@@ -481,4 +536,7 @@ internal double NoWorkflow_TipHeight
481536
internal ReactiveCommand<IWorkflowCase, Unit>? StopWorkflowCommand { get; set; }
482537

483538
internal ReactiveCommand<Unit, Unit>? RefreshWorkflowsCommand { get; set; }
539+
540+
/// <summary>Clears the Debug activity log. Bound from the broom button in the log panel.</summary>
541+
internal ReactiveCommand<Unit, Unit>? ClearLogCommand { get; set; }
484542
}

KitX Dashboard/Views/Pages/WorkflowPage.axaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,46 @@
5656
</StackPanel>
5757
</Grid>
5858

59+
<!-- Real-time activity log (Debug builds only).
60+
Shows mounted/triggered/run/complete/error events so users can see which
61+
workflow fired, when, and why it failed — without digging through Log/. -->
62+
<Border DockPanel.Dock="Bottom"
63+
Height="160"
64+
IsVisible="{Binding IsDebugLogVisible}"
65+
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
66+
BorderThickness="1,1,0,0"
67+
Background="{DynamicResource LayerFillColorDefaultBrush}">
68+
<Grid>
69+
<Grid.RowDefinitions>
70+
<RowDefinition Height="Auto" />
71+
<RowDefinition Height="*" />
72+
</Grid.RowDefinitions>
73+
<DockPanel Grid.Row="0" Margin="10,4">
74+
<TextBlock DockPanel.Dock="Left"
75+
FontWeight="SemiBold"
76+
Text="Activity Log (Debug)" />
77+
<Button DockPanel.Dock="Right"
78+
HorizontalAlignment="Right"
79+
Padding="6,2"
80+
Command="{Binding ClearLogCommand}"
81+
ToolTip.Tip="Clear log">
82+
<icon:MaterialIcon Width="14" Height="14" Kind="Broom" />
83+
</Button>
84+
</DockPanel>
85+
<ListBox Grid.Row="1"
86+
x:Name="ActivityLogList"
87+
Margin="0"
88+
Padding="4,0"
89+
Background="Transparent"
90+
BorderThickness="0"
91+
FontFamily="Cascadia Mono,Consolas,Courier New"
92+
FontSize="11"
93+
ItemsSource="{Binding ExecutionLog}"
94+
ScrollViewer.HorizontalScrollBarVisibility="Auto"
95+
ScrollViewer.VerticalScrollBarVisibility="Auto" />
96+
</Grid>
97+
</Border>
98+
5999
<DockPanel Margin="10">
60100
<!-- Empty State -->
61101
<Border Height="{Binding NoWorkflow_TipHeight}"

KitX Dashboard/Views/Pages/WorkflowPage.axaml.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Specialized;
12
using Avalonia.Controls;
23
using KitX.Dashboard.ViewModels.Pages;
34

@@ -12,5 +13,21 @@ public WorkflowPage()
1213
InitializeComponent();
1314

1415
DataContext = workflowViewModel;
16+
17+
// Auto-scroll the activity log to the newest line as items arrive.
18+
// Hooking here (rather than in the VM) keeps UI concerns out of the VM and lets
19+
// us reach the ListBox's internal ScrollViewer cleanly.
20+
if (workflowViewModel.ExecutionLog is INotifyCollectionChanged ncc)
21+
{
22+
ncc.CollectionChanged += (_, args) =>
23+
{
24+
if (args.Action == NotifyCollectionChangedAction.Reset) return;
25+
if (ActivityLogList?.ItemCount is > 0)
26+
{
27+
// Scroll to the last item; Avalonia's ListBox.ScrollIntoView is 0-based.
28+
ActivityLogList.ScrollIntoView(ActivityLogList.ItemCount - 1);
29+
}
30+
};
31+
}
1532
}
1633
}

0 commit comments

Comments
 (0)