--- phase: 01-foundation plan: 02 type: execute wave: 2 depends_on: - 01-01 files_modified: - SharepointToolbox/Core/Models/TenantProfile.cs - SharepointToolbox/Core/Models/OperationProgress.cs - SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs - SharepointToolbox/Core/Messages/LanguageChangedMessage.cs - SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs - SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs - SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs autonomous: true requirements: - FOUND-05 - FOUND-06 - FOUND-07 - FOUND-08 must_haves: truths: - "OperationProgress record is usable by all feature services for IProgress reporting" - "TenantSwitchedMessage and LanguageChangedMessage are broadcast-ready via WeakReferenceMessenger" - "SharePointPaginationHelper iterates past 5,000 items using ListItemCollectionPosition" - "ExecuteQueryRetryHelper surfaces retry events as IProgress messages" - "LogPanelSink writes to a RichTextBox-targeted dispatcher-safe callback" artifacts: - path: "SharepointToolbox/Core/Models/OperationProgress.cs" provides: "Shared progress record used by all feature services" contains: "record OperationProgress" - path: "SharepointToolbox/Core/Models/TenantProfile.cs" provides: "Profile model matching JSON schema" contains: "TenantUrl" - path: "SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs" provides: "CSOM list pagination wrapping CamlQuery + ListItemCollectionPosition" contains: "ListItemCollectionPosition" - path: "SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs" provides: "Retry wrapper for CSOM calls with throttle detection" contains: "ExecuteQueryRetryAsync" - path: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs" provides: "Custom Serilog sink that writes to UI log panel" contains: "ILogEventSink" key_links: - from: "SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs" to: "Microsoft.SharePoint.Client.ListItemCollectionPosition" via: "PnP.Framework CSOM" pattern: "ListItemCollectionPosition" - from: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs" to: "Application.Current.Dispatcher" via: "InvokeAsync for thread safety" pattern: "Dispatcher.InvokeAsync" --- Build the Core layer — models, messages, and infrastructure helpers — that every subsequent plan depends on. These are the contracts: no business logic, just types and patterns. Purpose: All feature phases import OperationProgress, TenantProfile, the pagination helper, and the retry helper. Getting these right here means no rework in Phases 2-4. Output: Core/Models, Core/Messages, Core/Helpers, Infrastructure/Logging directories with 7 files. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md @.planning/phases/01-foundation/01-01-SUMMARY.md Task 1: Core models and WeakReferenceMessenger messages SharepointToolbox/Core/Models/TenantProfile.cs, SharepointToolbox/Core/Models/OperationProgress.cs, SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs, SharepointToolbox/Core/Messages/LanguageChangedMessage.cs Create directories: `Core/Models/`, `Core/Messages/` **TenantProfile.cs** ```csharp namespace SharepointToolbox.Core.Models; public class TenantProfile { public string Name { get; set; } = string.Empty; public string TenantUrl { get; set; } = string.Empty; public string ClientId { get; set; } = string.Empty; } ``` Note: Plain class (not record) — mutable for JSON deserialization with System.Text.Json. Field names `Name`, `TenantUrl`, `ClientId` must match existing JSON schema exactly (case-insensitive by default in STJ but preserve casing for compatibility). **OperationProgress.cs** ```csharp namespace SharepointToolbox.Core.Models; public record OperationProgress(int Current, int Total, string Message) { public static OperationProgress Indeterminate(string message) => new(0, 0, message); } ``` **TenantSwitchedMessage.cs** ```csharp using CommunityToolkit.Mvvm.Messaging.Messages; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Core.Messages; public sealed class TenantSwitchedMessage : ValueChangedMessage { public TenantSwitchedMessage(TenantProfile profile) : base(profile) { } } ``` **LanguageChangedMessage.cs** ```csharp using CommunityToolkit.Mvvm.Messaging.Messages; namespace SharepointToolbox.Core.Messages; public sealed class LanguageChangedMessage : ValueChangedMessage { public LanguageChangedMessage(string cultureCode) : base(cultureCode) { } } ``` Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` to verify no errors. cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5 Build succeeds. Four files created. TenantProfile fields match JSON schema. OperationProgress is a record with Indeterminate factory. Task 2: SharePointPaginationHelper, ExecuteQueryRetryHelper, LogPanelSink SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs, SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs, SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs Create directories: `Core/Helpers/`, `Infrastructure/Logging/` **SharePointPaginationHelper.cs** ```csharp using Microsoft.SharePoint.Client; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Core.Helpers; public static class SharePointPaginationHelper { /// /// Enumerates all items in a SharePoint list, bypassing the 5,000-item threshold. /// Uses CamlQuery with RowLimit=2000 and ListItemCollectionPosition for pagination. /// Never call ExecuteQuery directly on a list — always use this helper. /// public static async IAsyncEnumerable GetAllItemsAsync( ClientContext ctx, List list, CamlQuery? baseQuery = null, CancellationToken ct = default) { var query = baseQuery ?? CamlQuery.CreateAllItemsQuery(); query.ViewXml = BuildPagedViewXml(query.ViewXml, rowLimit: 2000); query.ListItemCollectionPosition = null; do { ct.ThrowIfCancellationRequested(); var items = list.GetItems(query); ctx.Load(items); await ctx.ExecuteQueryAsync(); foreach (var item in items) yield return item; query.ListItemCollectionPosition = items.ListItemCollectionPosition; } while (query.ListItemCollectionPosition != null); } private static string BuildPagedViewXml(string? existingXml, int rowLimit) { // Inject or replace RowLimit in existing CAML, or create minimal view if (string.IsNullOrWhiteSpace(existingXml)) return $"{rowLimit}"; // Simple replacement approach — adequate for Phase 1 if (existingXml.Contains("", StringComparison.OrdinalIgnoreCase)) { return System.Text.RegularExpressions.Regex.Replace( existingXml, @"]*>\d+", $"{rowLimit}", System.Text.RegularExpressions.RegexOptions.IgnoreCase); } return existingXml.Replace("", $"{rowLimit}", StringComparison.OrdinalIgnoreCase); } } ``` **ExecuteQueryRetryHelper.cs** ```csharp using Microsoft.SharePoint.Client; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Core.Helpers; public static class ExecuteQueryRetryHelper { private const int MaxRetries = 5; /// /// Executes a SharePoint query with automatic retry on throttle (429/503). /// Surfaces retry events via progress for user visibility ("Throttled — retrying in 30s…"). /// public static async Task ExecuteQueryRetryAsync( ClientContext ctx, IProgress? progress = null, CancellationToken ct = default) { int attempt = 0; while (true) { ct.ThrowIfCancellationRequested(); try { await ctx.ExecuteQueryAsync(); return; } catch (Exception ex) when (IsThrottleException(ex) && attempt < MaxRetries) { attempt++; int delaySeconds = (int)Math.Pow(2, attempt) * 5; // 10, 20, 40, 80, 160s progress?.Report(OperationProgress.Indeterminate( $"Throttled by SharePoint — retrying in {delaySeconds}s (attempt {attempt}/{MaxRetries})…")); await Task.Delay(TimeSpan.FromSeconds(delaySeconds), ct); } } } private static bool IsThrottleException(Exception ex) { var msg = ex.Message; return msg.Contains("429") || msg.Contains("503") || msg.Contains("throttl", StringComparison.OrdinalIgnoreCase); } } ``` **LogPanelSink.cs** ```csharp using Serilog.Core; using Serilog.Events; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Controls; namespace SharepointToolbox.Infrastructure.Logging; /// /// Custom Serilog sink that writes timestamped, color-coded entries to a WPF RichTextBox. /// Format: HH:mm:ss [LEVEL] Message — green=info/success, orange=warning, red=error. /// All writes dispatch to the UI thread via Application.Current.Dispatcher. /// public class LogPanelSink : ILogEventSink { private readonly RichTextBox _richTextBox; public LogPanelSink(RichTextBox richTextBox) { _richTextBox = richTextBox; } public void Emit(LogEvent logEvent) { var message = logEvent.RenderMessage(); var timestamp = logEvent.Timestamp.ToString("HH:mm:ss"); var level = logEvent.Level.ToString().ToUpperInvariant()[..4]; // INFO, WARN, ERRO, FATL var text = $"{timestamp} [{level}] {message}"; var color = GetColor(logEvent.Level); Application.Current?.Dispatcher.InvokeAsync(() => { var para = new Paragraph(new Run(text) { Foreground = new SolidColorBrush(color) }) { Margin = new Thickness(0) }; _richTextBox.Document.Blocks.Add(para); _richTextBox.ScrollToEnd(); }); } private static Color GetColor(LogEventLevel level) => level switch { LogEventLevel.Warning => Colors.Orange, LogEventLevel.Error or LogEventLevel.Fatal => Colors.Red, _ => Colors.LimeGreen }; } ``` Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` to verify no errors. cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5 Build succeeds. SharePointPaginationHelper uses ListItemCollectionPosition. ExecuteQueryRetryHelper detects throttle exceptions. LogPanelSink dispatches to UI thread via Dispatcher.InvokeAsync. - `dotnet build SharepointToolbox.sln` passes with 0 errors - `SharepointToolbox/Core/Models/TenantProfile.cs` contains `TenantUrl` (not `TenantURL` or `Url`) to match JSON schema - `SharePointPaginationHelper.cs` contains `ListItemCollectionPosition` and loop condition checking for null - `ExecuteQueryRetryHelper.cs` contains exponential backoff and progress reporting - `LogPanelSink.cs` contains `Dispatcher.InvokeAsync` All 7 Core/Infrastructure files created and compiling. Models match JSON schema field names. Pagination helper correctly loops until ListItemCollectionPosition is null. After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`