# Worker Log Footer Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Surface important Worker lifecycle events in the UI footer as a single rotating, color-coded line that auto-hides after 30s of silence. **Architecture:** Add `WorkerLogLevel` enum in shared `ClaudeDo.Data` project. `HubBroadcaster` gets a `WorkerLog(message, level, timestampUtc)` SignalR event. Seven emit sites in `TaskRunner`, `TaskMergeService`, `TaskResetService` (callers of `WorktreeManager`, not WorktreeManager itself — they have the task title in scope). UI side: `WorkerClient` surfaces a `WorkerLogReceived` event; footer state lives on `IslandsShellViewModel` (existing root VM for `MainWindow`, also owns connection state); `System.Timers.Timer` clears the line after 30s; a `WorkerLogLevelToBrushConverter` maps level → brush in XAML. **Tech Stack:** .NET 8, ASP.NET Core SignalR, Avalonia 12, CommunityToolkit.Mvvm, xUnit. **Spec:** `docs/superpowers/specs/2026-04-23-worker-log-footer-design.md` **Deviation from spec:** Spec names `WorktreeManager.CreateAsync` / `DiscardAsync` as emit sites. In practice, `WorktreeManager` has only the task ID in scope; its callers (`TaskRunner`, `TaskResetService`) have the title. Emitting from callers avoids adding constructor dependencies to `WorktreeManager` and produces identical user-visible behavior. **Build note:** Per project convention, `dotnet build ClaudeDo.slnx` fails on .NET 8 — always build individual csprojs. --- ### Task 1: Add `WorkerLogLevel` enum (shared contract) **Files:** - Create: `src/ClaudeDo.Data/Models/WorkerLogLevel.cs` - [ ] **Step 1: Write the enum** ```csharp namespace ClaudeDo.Data.Models; public enum WorkerLogLevel { Info, Success, Warn, Error, } ``` - [ ] **Step 2: Build Data project** Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj` Expected: Build succeeded, 0 errors. - [ ] **Step 3: Commit** ```bash git add src/ClaudeDo.Data/Models/WorkerLogLevel.cs git commit -m "feat(data): add WorkerLogLevel enum" ``` --- ### Task 2: Add `WorkerLog` broadcaster method + SignalR JSON enum-as-string **Files:** - Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` (append method) - Modify: `src/ClaudeDo.Worker/Program.cs` (line ~23 — the `AddSignalR()` call) - [ ] **Step 1: Add enum-as-string serialization** Replace: ```csharp builder.Services.AddSignalR(); ``` with: ```csharp builder.Services.AddSignalR().AddJsonProtocol(options => { options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); }); ``` - [ ] **Step 2: Add `WorkerLog` method to `HubBroadcaster`** Add the following method inside `HubBroadcaster` class (after the existing `RunCreated` method): ```csharp public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) => _hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc); ``` Add to the using block at top of file (if not already present): ```csharp using ClaudeDo.Data.Models; ``` - [ ] **Step 3: Build** Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` Expected: Build succeeded, 0 errors. - [ ] **Step 4: Commit** ```bash git add src/ClaudeDo.Worker/Hub/HubBroadcaster.cs src/ClaudeDo.Worker/Program.cs git commit -m "feat(worker): add WorkerLog SignalR event" ``` --- ### Task 3: Emit `WorkerLog` from `TaskRunner` **Files:** - Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` Four emit sites in this file: 1. **Created worktree** — right after `_wtManager.CreateAsync` succeeds (around line 69). 2. **Started Claude** — just before invoking Claude process. 3. **Committed changes** — after auto-commit (before the `WorktreeUpdated` broadcast around line 318). 4. **Finished** — at both success (line 330) and failure paths, mirroring the existing `TaskFinished` call. - [ ] **Step 1: Add using for `WorkerLogLevel`** Ensure `TaskRunner.cs` has at the top: ```csharp using ClaudeDo.Data.Models; ``` - [ ] **Step 2: Emit "Created worktree"** After the line `wtCtx = await _wtManager.CreateAsync(task, list, ct);` (around line 69), add: ```csharp await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); ``` (Place inside the same `if` branch that called `CreateAsync`, after the assignment.) - [ ] **Step 3: Emit "Started Claude"** Locate the point just before `ClaudeProcess` is invoked (search for where `ClaudeProcess` or `RunProcessAsync` is called). Just before the invocation, add: ```csharp await _broadcaster.WorkerLog($"Started Claude for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); ``` - [ ] **Step 4: Emit "Committed changes"** Locate the auto-commit code path (around line 318, just before `await _broadcaster.WorktreeUpdated(task.Id);`). Add immediately before that call: ```csharp await _broadcaster.WorkerLog($"Committed changes in \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); ``` - [ ] **Step 5: Emit "Finished (done)"** Find the success finish path (around line 330, where `TaskFinished` is broadcast with status `"done"`). Add immediately before that call: ```csharp await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow); ``` - [ ] **Step 6: Emit "Finished (failed)"** Find the failure path (search for `TaskFinished` with status `"failed"`). Add immediately before: ```csharp await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (failed)", WorkerLogLevel.Error, DateTime.UtcNow); ``` - [ ] **Step 7: Build** Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` Expected: Build succeeded, 0 errors. - [ ] **Step 8: Run existing Worker tests (no regressions)** Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` Expected: All tests pass. - [ ] **Step 9: Commit** ```bash git add src/ClaudeDo.Worker/Runner/TaskRunner.cs git commit -m "feat(worker): emit WorkerLog events from TaskRunner" ``` --- ### Task 4: Emit `WorkerLog` from `TaskMergeService` and `TaskResetService` **Files:** - Modify: `src/ClaudeDo.Worker/Services/TaskMergeService.cs` - Modify: `src/ClaudeDo.Worker/Services/TaskResetService.cs` Both services already have `HubBroadcaster` injected (`_broadcaster`). Both already load the task entity (needed for title). - [ ] **Step 1: Add using in both files** Add to the top of each file (if not already present): ```csharp using ClaudeDo.Data.Models; ``` - [ ] **Step 2: Emit "Merged" in `TaskMergeService.MergeAsync`** Locate the existing log line around line 137: ```csharp _logger.LogInformation( "Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})", ...); ``` Immediately after it (before `return new MergeResult(...)` on line 140), add: ```csharp await _broadcaster.WorkerLog($"Merged \"{task.Title}\" into {targetBranch}", WorkerLogLevel.Success, DateTime.UtcNow); ``` Use whatever variable names `task` and `targetBranch` are in scope — adjust to match the actual local names at that site. - [ ] **Step 3: Emit "Discarded" in `TaskResetService.ResetAsync`** Locate the call `await _wtManager.DiscardAsync(wt, list.WorkingDir, ct);` (line 53). Immediately after it, add: ```csharp await _broadcaster.WorkerLog($"Discarded worktree for \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow); ``` - [ ] **Step 4: Emit "Reset" in `TaskResetService.ResetAsync`** Locate the existing line `_logger.LogInformation("Reset task {TaskId} to Manual ...` (line 66). Immediately after it, add: ```csharp await _broadcaster.WorkerLog($"Reset \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow); ``` - [ ] **Step 5: Build** Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` Expected: Build succeeded, 0 errors. - [ ] **Step 6: Run existing Worker tests** Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` Expected: All tests pass. - [ ] **Step 7: Commit** ```bash git add src/ClaudeDo.Worker/Services/TaskMergeService.cs src/ClaudeDo.Worker/Services/TaskResetService.cs git commit -m "feat(worker): emit WorkerLog for merge, discard, reset" ``` --- ### Task 5: Add `WorkerLogEntry` record + `WorkerLogReceived` event on `WorkerClient` **Files:** - Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` - [ ] **Step 1: Add using for `WorkerLogLevel`** Add at the top of `WorkerClient.cs`: ```csharp using ClaudeDo.Data.Models; ``` - [ ] **Step 2: Declare the `WorkerLogEntry` record** Add at the top of the file (above or below the `WorkerClient` class, same namespace): ```csharp public sealed record WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc); ``` - [ ] **Step 3: Add the event field** Alongside the other `public event Action<...>?` declarations (around lines 42-48), add: ```csharp public event Action? WorkerLogReceivedEvent; ``` - [ ] **Step 4: Register the SignalR handler** Alongside the other `_hub.On<...>` registrations (around lines 80-117), add: ```csharp _hub.On("WorkerLog", (message, level, timestampUtc) => { WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc)); }); ``` - [ ] **Step 5: Build** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: Build succeeded, 0 errors. - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Ui/Services/WorkerClient.cs git commit -m "feat(ui): subscribe to WorkerLog SignalR event" ``` --- ### Task 6: Create `ClaudeDo.Ui.Tests` project and add `WorkerLogLevelToBrushConverter` **Files:** - Create: `src/ClaudeDo.Ui/Converters/WorkerLogLevelToBrushConverter.cs` - Modify: `src/ClaudeDo.Ui/App.axaml` (register converter as resource) - Create: `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` - Create: `tests/ClaudeDo.Ui.Tests/WorkerLogLevelToBrushConverterTests.cs` - [ ] **Step 1: Write the converter** Create `src/ClaudeDo.Ui/Converters/WorkerLogLevelToBrushConverter.cs`: ```csharp using System; using System.Globalization; using Avalonia; using Avalonia.Data.Converters; using Avalonia.Media; using ClaudeDo.Data.Models; namespace ClaudeDo.Ui.Converters; public sealed class WorkerLogLevelToBrushConverter : IValueConverter { private static readonly IBrush SuccessBrush = new SolidColorBrush(Color.Parse("#4CAF50")); private static readonly IBrush WarnBrush = new SolidColorBrush(Color.Parse("#FFA726")); private static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#EF5350")); private static readonly IBrush InfoFallback = new SolidColorBrush(Color.Parse("#888888")); public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is not WorkerLogLevel level) return AvaloniaProperty.UnsetValue; return level switch { WorkerLogLevel.Success => SuccessBrush, WorkerLogLevel.Warn => WarnBrush, WorkerLogLevel.Error => ErrorBrush, WorkerLogLevel.Info => ResolveInfoBrush(), _ => AvaloniaProperty.UnsetValue, }; } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); private static IBrush ResolveInfoBrush() { if (Application.Current is { } app && app.Resources.TryGetResource("TextDimBrush", app.ActualThemeVariant, out var res) && res is IBrush brush) { return brush; } return InfoFallback; } } ``` - [ ] **Step 2: Register converter in `App.axaml`** Open `src/ClaudeDo.Ui/App.axaml`. Inside the `` section (add one if missing), add alongside any existing converter entries: ```xml ``` Ensure the `xmlns:converters="using:ClaudeDo.Ui.Converters"` namespace is declared at the root `` element. If other converters (e.g. `StatusColorConverter`) are already resources in `App.axaml` follow the same pattern; if they're declared per-view, declare this converter at the top of `MainWindow.axaml` in Task 8 instead. - [ ] **Step 3: Create UI test project** Create `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj`: ```xml net8.0 enable false ``` If the existing `tests/ClaudeDo.Worker.Tests/*.csproj` uses different `Microsoft.NET.Test.Sdk` / xUnit versions, match those versions exactly to avoid analyzer mismatches. - [ ] **Step 4: Write the failing test** Create `tests/ClaudeDo.Ui.Tests/WorkerLogLevelToBrushConverterTests.cs`: ```csharp using System.Globalization; using Avalonia; using Avalonia.Media; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Converters; using Xunit; namespace ClaudeDo.Ui.Tests; public class WorkerLogLevelToBrushConverterTests { [Theory] [InlineData(WorkerLogLevel.Success, "#FF4CAF50")] [InlineData(WorkerLogLevel.Warn, "#FFFFA726")] [InlineData(WorkerLogLevel.Error, "#FFEF5350")] public void Convert_maps_level_to_expected_brush_color(WorkerLogLevel level, string expectedArgb) { var converter = new WorkerLogLevelToBrushConverter(); var result = converter.Convert(level, typeof(IBrush), null, CultureInfo.InvariantCulture); var solid = Assert.IsType(result); Assert.Equal(expectedArgb.ToLowerInvariant(), $"#{solid.Color.ToUInt32():X8}".ToLowerInvariant()); } [Fact] public void Convert_info_returns_a_brush_fallback_when_no_app() { var converter = new WorkerLogLevelToBrushConverter(); var result = converter.Convert(WorkerLogLevel.Info, typeof(IBrush), null, CultureInfo.InvariantCulture); Assert.IsAssignableFrom(result); } [Fact] public void Convert_unknown_value_returns_unset() { var converter = new WorkerLogLevelToBrushConverter(); var result = converter.Convert("not a level", typeof(IBrush), null, CultureInfo.InvariantCulture); Assert.Equal(AvaloniaProperty.UnsetValue, result); } } ``` - [ ] **Step 5: Run the tests** Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` Expected: All 5 tests pass. - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Ui/Converters/WorkerLogLevelToBrushConverter.cs src/ClaudeDo.Ui/App.axaml tests/ClaudeDo.Ui.Tests/ git commit -m "feat(ui): add WorkerLogLevelToBrushConverter with tests" ``` --- ### Task 7: Add footer state + 30s auto-clear timer to `IslandsShellViewModel` **Files:** - Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs` - Create: `tests/ClaudeDo.Ui.Tests/IslandsShellViewModelWorkerLogTests.cs` Timer uses `System.Timers.Timer` (not `DispatcherTimer`) so unit tests don't need an Avalonia dispatcher. The elapsed callback marshals to the UI thread via `Dispatcher.UIThread.Post` when the dispatcher is available; in tests the VM logic under test sets properties directly so no marshalling is needed. - [ ] **Step 1: Write the failing tests** Create `tests/ClaudeDo.Ui.Tests/IslandsShellViewModelWorkerLogTests.cs`: ```csharp using System; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels; using Xunit; namespace ClaudeDo.Ui.Tests; public class IslandsShellViewModelWorkerLogTests { private static IslandsShellViewModel NewVm() => // The real constructor requires island VMs + WorkerClient. These tests // only exercise the WorkerLog handling, so we use a test-only constructor // that bypasses the sub-VMs. Add `internal IslandsShellViewModel()` for tests. IslandsShellViewModel.CreateForTests(); [Fact] public void Receiving_event_sets_text_level_and_visible() { var vm = NewVm(); var at = new DateTime(2026, 4, 23, 14, 32, 0, DateTimeKind.Utc); vm.OnWorkerLogReceived(new WorkerLogEntry("Created worktree for \"X\"", WorkerLogLevel.Info, at)); Assert.True(vm.IsWorkerLogVisible); Assert.Equal(WorkerLogLevel.Info, vm.WorkerLogLevel); Assert.Contains("Created worktree for \"X\"", vm.WorkerLogText); } [Fact] public void Second_event_replaces_first() { var vm = NewVm(); vm.OnWorkerLogReceived(new WorkerLogEntry("first", WorkerLogLevel.Info, DateTime.UtcNow)); vm.OnWorkerLogReceived(new WorkerLogEntry("second", WorkerLogLevel.Success, DateTime.UtcNow)); Assert.Contains("second", vm.WorkerLogText); Assert.Equal(WorkerLogLevel.Success, vm.WorkerLogLevel); } [Fact] public void ClearWorkerLog_hides_line() { var vm = NewVm(); vm.OnWorkerLogReceived(new WorkerLogEntry("msg", WorkerLogLevel.Info, DateTime.UtcNow)); vm.ClearWorkerLog(); Assert.False(vm.IsWorkerLogVisible); Assert.Null(vm.WorkerLogText); } [Fact] public void Text_is_formatted_as_HHmm_dot_message_local_time() { var vm = NewVm(); var utc = new DateTime(2026, 4, 23, 12, 0, 0, DateTimeKind.Utc); var expectedLocalHhmm = utc.ToLocalTime().ToString("HH:mm"); vm.OnWorkerLogReceived(new WorkerLogEntry("hello", WorkerLogLevel.Info, utc)); Assert.StartsWith(expectedLocalHhmm + " · ", vm.WorkerLogText); } } ``` - [ ] **Step 2: Run tests to confirm they fail to compile** Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` Expected: Build errors — `CreateForTests`, `OnWorkerLogReceived`, `ClearWorkerLog`, `IsWorkerLogVisible`, `WorkerLogText`, `WorkerLogLevel` do not yet exist. - [ ] **Step 3: Implement on `IslandsShellViewModel`** Open `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`. Add `using`s if missing: ```csharp using System.Timers; using Avalonia.Threading; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; ``` Inside the class, add: ```csharp [ObservableProperty] private string? workerLogText; [ObservableProperty] private WorkerLogLevel workerLogLevel; [ObservableProperty] private bool isWorkerLogVisible; private readonly Timer _workerLogTimer = new(TimeSpan.FromSeconds(30).TotalMilliseconds) { AutoReset = false, }; internal static IslandsShellViewModel CreateForTests() => (IslandsShellViewModel)System.Runtime.Serialization.FormatterServices .GetUninitializedObject(typeof(IslandsShellViewModel)); ``` (If `FormatterServices` is unavailable under `net8.0`, instead add a parameterless `internal IslandsShellViewModel() {}` constructor guarded for tests only.) In the existing real constructor, wire up subscription (after the line `Worker.PropertyChanged += ...` block, around line 63-70): ```csharp Worker.WorkerLogReceivedEvent += OnWorkerLogReceived; _workerLogTimer.Elapsed += (_, _) => { if (Dispatcher.UIThread.CheckAccess()) ClearWorkerLog(); else Dispatcher.UIThread.Post(ClearWorkerLog); }; ``` Add the methods the tests call: ```csharp public void OnWorkerLogReceived(WorkerLogEntry entry) { var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm"); WorkerLogText = $"{hhmm} · {entry.Message}"; WorkerLogLevel = entry.Level; IsWorkerLogVisible = true; _workerLogTimer.Stop(); _workerLogTimer.Start(); } public void ClearWorkerLog() { IsWorkerLogVisible = false; WorkerLogText = null; } ``` - [ ] **Step 4: Run tests** Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` Expected: All tests pass (5 converter + 4 VM = 9 tests). - [ ] **Step 5: Build the UI project as a sanity check** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: Build succeeded. - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs tests/ClaudeDo.Ui.Tests/IslandsShellViewModelWorkerLogTests.cs git commit -m "feat(ui): add worker log state and 30s timer to shell VM" ``` --- ### Task 8: Update `MainWindow.axaml` footer — dock log line right **Files:** - Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml` (lines 104-135 — the footer `Border`) - [ ] **Step 1: Add the converter resource to the window (if not already in App.axaml)** If Task 6 declared the converter in `App.axaml`, skip this step. Otherwise, add a `` block near the top of `MainWindow.axaml`: ```xml ``` Ensure `xmlns:converters="using:ClaudeDo.Ui.Converters"` is declared on the root ``. - [ ] **Step 2: Replace the footer body** Replace the existing footer `` inner contents (the `` at lines 109-134) with: ```xml ``` - [ ] **Step 3: Build the UI** Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` Expected: Build succeeded, 0 errors. No XAML compilation errors. - [ ] **Step 4: Build the full app (entry point)** Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` Expected: Build succeeded. - [ ] **Step 5: Manual smoke test** Start the Worker and the App (two separate processes per CLAUDE.md). Exercise each event and confirm the footer line appears with the expected color and copy: 1. Start a task → expect `HH:MM · Created worktree for ""` (dim/info). 2. Observe while Claude runs → expect `HH:MM · Started Claude for "<title>"` (dim/info). 3. Task commits → expect `HH:MM · Committed changes in "<title>"` (dim/info). 4. Task finishes successfully → expect `HH:MM · Finished "<title>" (done)` (green). 5. Trigger a failing task → expect `HH:MM · Finished "<title>" (failed)` (red). 6. Reset a failed task → expect `HH:MM · Discarded worktree for "<title>"` (amber) followed by `HH:MM · Reset "<title>"` (amber). 7. Merge a completed task → expect `HH:MM · Merged "<title>" into <branch>` (green). 8. Wait 30s with no new events → footer log line disappears (connection pill remains). 9. Trigger a burst of 3 events in quick succession → only the most recent is shown; timer resets on each. 10. Long task title (≥60 chars) → line is ellipsized, connection pill on the left remains fully visible. - [ ] **Step 6: Commit** ```bash git add src/ClaudeDo.Ui/Views/MainWindow.axaml git commit -m "feat(ui): show worker log line in footer" ``` --- ## Self-Review - **Spec coverage:** - Enum `WorkerLogLevel` in `ClaudeDo.Data` — Task 1 ✓ - SignalR enum-as-string — Task 2 ✓ - `HubBroadcaster.WorkerLog` — Task 2 ✓ - 7 emit sites with correct level mapping — Tasks 3, 4 ✓ - `WorkerClient.WorkerLogReceived` event + `WorkerLogEntry` record — Task 5 ✓ - `WorkerLogLevelToBrushConverter` with unit tests — Task 6 ✓ - Footer VM state + 30s timer + tests — Task 7 ✓ - Footer XAML (DockPanel, connection left, log right, level-based color, ellipsis) — Task 8 ✓ - Out-of-scope items (history drawer, filtering, persistence) — correctly omitted ✓ - **Placeholder scan:** No "TBD" / "handle edge cases" / "similar to Task N". All code is inline. - **Type consistency:** `WorkerLogEntry(Message, Level, TimestampUtc)` — same signature used in Task 5 (declaration), Task 7 (consumer tests + VM). `WorkerLog(message, level, timestampUtc)` — same signature in Task 2 (broadcaster) and Tasks 3-4 (callers). `OnWorkerLogReceived` / `ClearWorkerLog` / `IsWorkerLogVisible` / `WorkerLogText` / `WorkerLogLevel` — consistent between Task 7 test and Task 7 implementation.