@@ -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}
0 commit comments