Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/01-foundation/01-02-PLAN.md
Dev 655bb79a99
All checks were successful
Release zip package / release (push) Successful in 10s
chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:15:14 +02:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation 02 execute 2
01-01
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
true
FOUND-05
FOUND-06
FOUND-07
FOUND-08
truths artifacts key_links
OperationProgress record is usable by all feature services for IProgress<T> 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
path provides contains
SharepointToolbox/Core/Models/OperationProgress.cs Shared progress record used by all feature services record OperationProgress
path provides contains
SharepointToolbox/Core/Models/TenantProfile.cs Profile model matching JSON schema TenantUrl
path provides contains
SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs CSOM list pagination wrapping CamlQuery + ListItemCollectionPosition ListItemCollectionPosition
path provides contains
SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs Retry wrapper for CSOM calls with throttle detection ExecuteQueryRetryAsync
path provides contains
SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs Custom Serilog sink that writes to UI log panel ILogEventSink
from to via pattern
SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs Microsoft.SharePoint.Client.ListItemCollectionPosition PnP.Framework CSOM ListItemCollectionPosition
from to via pattern
SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs Application.Current.Dispatcher InvokeAsync for thread safety 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.

<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>

@.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<TenantProfile>
{
    public TenantSwitchedMessage(TenantProfile profile) : base(profile) { }
}
```

**LanguageChangedMessage.cs**
```csharp
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace SharepointToolbox.Core.Messages;

public sealed class LanguageChangedMessage : ValueChangedMessage<string>
{
    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
{
    /// <summary>
    /// 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.
    /// </summary>
    public static async IAsyncEnumerable<ListItem> 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 $"<View><RowLimit>{rowLimit}</RowLimit></View>";

        // Simple replacement approach — adequate for Phase 1
        if (existingXml.Contains("<RowLimit>", StringComparison.OrdinalIgnoreCase))
        {
            return System.Text.RegularExpressions.Regex.Replace(
                existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
                $"<RowLimit>{rowLimit}</RowLimit>",
                System.Text.RegularExpressions.RegexOptions.IgnoreCase);
        }
        return existingXml.Replace("</View>", $"<RowLimit>{rowLimit}</RowLimit></View>",
            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;

    /// <summary>
    /// Executes a SharePoint query with automatic retry on throttle (429/503).
    /// Surfaces retry events via progress for user visibility ("Throttled — retrying in 30s…").
    /// </summary>
    public static async Task ExecuteQueryRetryAsync(
        ClientContext ctx,
        IProgress<OperationProgress>? 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;

/// <summary>
/// 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.
/// </summary>
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`

<success_criteria> All 7 Core/Infrastructure files created and compiling. Models match JSON schema field names. Pagination helper correctly loops until ListItemCollectionPosition is null. </success_criteria>

After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`