Files
ClaudeDo/docs/superpowers/specs/2026-06-23-worker-log-footer-overlay-design.md

4.4 KiB

Worker log → footer auto-route + Log Visualizer overlay

Date: 2026-06-23 Status: approved (design forks resolved with user)

Goal

  1. Auto-route all Worker WARN/ERROR Serilog events to the footer status strip (today only ~10 hand-curated business events reach it).
  2. Make the footer log line clickable → opens a Log Visualizer overlay showing the last 30 min of logs at all levels, color-coded.
  3. Dedupe/rate-limit the footer so repeating warnings (e.g. the current 60s OIDC-discovery failure) don't strobe.

Decisions (locked)

  • Overlay source: Worker-side in-memory ring buffer (30-min window, all levels), fetched via a hub call. No log-file parsing.
  • Levels: overlay shows INF/WRN/ERR; footer flashes WARN/ERROR only.
  • Footer noise: per-message dedupe within a rate-limit window (suppress the footer broadcast for an identical message seen recently; the event is still buffered for the overlay).

Architecture

Worker

  • LogRingBuffer (singleton, Logging/): thread-safe, time-bounded (TimeSpan window, default 30 min) + hard cap (e.g. 5000) ring of WorkerLogRecord(Message, Level, TimestampUtc). Evicts on append by age + cap. Snapshot() returns newest-last.
  • BroadcastLogSink : Serilog.Core.ILogEventSink (Logging/): for every LogEvent
    • map level: Verbose/Debug/Information→Info, Warning→Warn, Error/Fatal→Error;
    • render msg = evt.RenderMessage() (+ ": {ex.GetType().Name}: {ex.Message}" first-line if evt.Exception != null);
    • append to LogRingBuffer (all levels);
    • if Warn|Error and not rate-limited: fire-and-forget HubBroadcaster.WorkerLog(msg, level, evt.Timestamp.UtcDateTime).
    • Loop guard: wrap the broadcast in try/catch and swallow; skip broadcasting events whose SourceContext is SignalR/connections plumbing (still buffered). Broadcasting must never itself log.
    • Dedupe/rate-limit: dict message → lastBroadcastUtc; suppress footer broadcast if now - last < RateLimitWindow (const, 120 s). Periodic prune of the dict.
  • DI wiring (chicken-egg): LogRingBuffer + BroadcastLogSink are created as locals in Program.cs before builder.Build(), captured into UseSerilog(... .WriteTo.Sink(broadcastSink)), and registered as singletons. HubBroadcaster doesn't exist until post-build, so the sink starts detached; after builder.Build() we call broadcastSink.Attach(app.Services.GetRequiredService<HubBroadcaster>()). Buffering works from process start; broadcasting begins once attached.
  • Hub: WorkerHub.GetRecentLogs() -> IReadOnlyList<WorkerLogRecordDto> reads LogRingBuffer.Snapshot(). (Read-only, no auth beyond existing hub.)

UI

  • IWorkerClient / WorkerClient: add Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync(CancellationToken ct = default). ⚠ Update hand-rolled fakes in both test projects (StubWorkerClient + Worker.Tests UiVm fake).
  • Footer: wrap the worker-log TextBlock so it's clickable (Button, transparent) → IslandsShellViewModel.OpenLogVisualizerCommand. Existing OnWorkerLogReceived already routes the (now more numerous) WorkerLog broadcasts to the strip — no change needed for footer routing itself.
  • LogVisualizerViewModel (Modals/): on open, GetRecentLogsAsync()ObservableCollection<LogLineViewModel> (msg, level→brush, HH:mm:ss). A level filter (All / Warn+Err) and a Refresh command. MVP = snapshot on open + Refresh; live-tail is a later nicety.
  • LogVisualizerView (Modals/): ModalShell-based dialog (consistent with other modals), shown via IDialogService.ShowLogVisualizerAsync(vm). Small, scrollable, monospaced, color-coded lines.
  • Localization: new vm.logVisualizer (+ any view keys) in en.json + de.json (parity test enforces).

Out of scope / follow-ups

  • Live-tail while the overlay is open (snapshot + Refresh for MVP).
  • The OIDC-discovery-every-60s failure is a separate bug (Online Inbox enabled, auth.kuns.dev SSL fails). Dedupe tames the footer symptom; the root cause is tracked separately.

Tests

  • Worker: LogRingBufferTests (age + cap eviction, snapshot order), BroadcastLogSinkTests (level mapping; all levels buffered; only Warn/Err broadcast; dedupe suppresses repeat broadcast within window but still buffers; exception rendering; loop-guard source filter).
  • UI: LogVisualizerViewModelTests (loads from worker, populates, filter). Footer-click wiring smoke.