diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 56cb28f..fe46fea 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -38,7 +38,17 @@ Decimal phases appear between their surrounding integers in numeric order.
3. User can see real-time progress on any long-running operation and cancel it mid-execution with a button — the operation stops cleanly with no silent continuation
4. When any operation fails, the user sees an actionable error message in the UI — no operation fails silently or swallows an exception
5. UI language switches between English and French dynamically without restarting the application
-**Plans**: TBD
+**Plans**: 8 plans
+
+Plans:
+- [ ] 01-01-PLAN.md — Solution scaffold: WPF project + xUnit test project with Generic Host entry point
+- [ ] 01-02-PLAN.md — Core layer: models, messages, pagination helper, retry helper, LogPanelSink
+- [ ] 01-03-PLAN.md — Persistence layer: ProfileRepository + SettingsRepository + services + unit tests
+- [ ] 01-04-PLAN.md — Auth layer: MsalClientFactory + SessionManager + unit tests
+- [ ] 01-05-PLAN.md — Localization + Serilog: TranslationSource, EN/FR resx, integration tests
+- [ ] 01-06-PLAN.md — ViewModels + WPF shell: FeatureViewModelBase, MainWindow XAML, global exception handlers
+- [ ] 01-07-PLAN.md — UI dialogs: ProfileManagementDialog + SettingsView wired into shell
+- [ ] 01-08-PLAN.md — Checkpoint: full test suite + visual verification of running application
### Phase 2: Permissions
**Goal**: Users can scan SharePoint permissions on one or many sites and export the results as both a raw CSV and a sortable, filterable HTML report — with no silent failures on large libraries and full control over scan scope.
@@ -94,7 +104,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. Foundation | 0/? | Not started | - |
+| 1. Foundation | 0/8 | Planning done | - |
| 2. Permissions | 0/? | Not started | - |
| 3. Storage and File Operations | 0/? | Not started | - |
| 4. Bulk Operations and Provisioning | 0/? | Not started | - |
diff --git a/.planning/phases/01-foundation/01-01-PLAN.md b/.planning/phases/01-foundation/01-01-PLAN.md
new file mode 100644
index 0000000..3a97e4b
--- /dev/null
+++ b/.planning/phases/01-foundation/01-01-PLAN.md
@@ -0,0 +1,226 @@
+---
+phase: 01-foundation
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - SharepointToolbox/SharepointToolbox.csproj
+ - SharepointToolbox/App.xaml
+ - SharepointToolbox/App.xaml.cs
+ - SharepointToolbox.Tests/SharepointToolbox.Tests.csproj
+ - SharepointToolbox.Tests/Services/ProfileServiceTests.cs
+ - SharepointToolbox.Tests/Services/SettingsServiceTests.cs
+ - SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs
+ - SharepointToolbox.Tests/Auth/SessionManagerTests.cs
+ - SharepointToolbox.Tests/ViewModels/FeatureViewModelBaseTests.cs
+ - SharepointToolbox.Tests/Localization/TranslationSourceTests.cs
+ - SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs
+ - SharepointToolbox.sln
+autonomous: true
+requirements:
+ - FOUND-01
+must_haves:
+ truths:
+ - "dotnet build produces zero errors"
+ - "dotnet test produces zero test failures (all tests pending/skipped)"
+ - "Solution contains two projects: SharepointToolbox (WPF) and SharepointToolbox.Tests (xUnit)"
+ - "App.xaml has no StartupUri — Generic Host entry point is wired"
+ artifacts:
+ - path: "SharepointToolbox/SharepointToolbox.csproj"
+ provides: "WPF .NET 10 project with all NuGet packages"
+ contains: "PublishTrimmed>false"
+ - path: "SharepointToolbox/App.xaml.cs"
+ provides: "Generic Host entry point with [STAThread]"
+ contains: "Host.CreateDefaultBuilder"
+ - path: "SharepointToolbox.Tests/SharepointToolbox.Tests.csproj"
+ provides: "xUnit test project"
+ contains: "xunit"
+ - path: "SharepointToolbox.sln"
+ provides: "Solution file with both projects"
+ key_links:
+ - from: "SharepointToolbox/App.xaml.cs"
+ to: "SharepointToolbox/App.xaml"
+ via: "x:Class reference + StartupUri removed"
+ pattern: "StartupUri"
+ - from: "SharepointToolbox/SharepointToolbox.csproj"
+ to: "App.xaml"
+ via: "Page include replacing ApplicationDefinition"
+ pattern: "ApplicationDefinition"
+---
+
+
+Create the solution scaffold: WPF .NET 10 project with all NuGet packages wired, Generic Host entry point, and xUnit test project with stub test files that compile but have no passing tests yet.
+
+Purpose: Every subsequent plan builds on a compiling, test-wired foundation. Getting the Generic Host + WPF STA threading right here prevents the most common startup crash.
+Output: SharepointToolbox.sln with two projects, zero build errors, zero test failures on first run.
+
+
+
+@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/STATE.md
+@.planning/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-RESEARCH.md
+
+
+
+
+
+ Task 1: Create solution and WPF project with all NuGet packages
+
+ SharepointToolbox.sln,
+ SharepointToolbox/SharepointToolbox.csproj,
+ SharepointToolbox/App.xaml,
+ SharepointToolbox/App.xaml.cs,
+ SharepointToolbox/MainWindow.xaml,
+ SharepointToolbox/MainWindow.xaml.cs
+
+
+ Run from the repo root:
+
+ ```
+ dotnet new sln -n SharepointToolbox
+ dotnet new wpf -n SharepointToolbox -f net10.0-windows
+ dotnet sln add SharepointToolbox/SharepointToolbox.csproj
+ ```
+
+ Edit SharepointToolbox/SharepointToolbox.csproj:
+ - Set `net10.0-windows`
+ - Add `enable`, `enable`
+ - Add `false` (critical — PnP.Framework + MSAL use reflection)
+ - Add `SharepointToolbox.App`
+ - Add NuGet packages:
+ - `CommunityToolkit.Mvvm` version 8.4.2
+ - `Microsoft.Extensions.Hosting` version 10.x (latest 10.x)
+ - `Microsoft.Identity.Client` version 4.83.1
+ - `Microsoft.Identity.Client.Extensions.Msal` version 4.83.3
+ - `Microsoft.Identity.Client.Broker` version 4.82.1
+ - `PnP.Framework` version 1.18.0
+ - `Serilog` version 4.3.1
+ - `Serilog.Sinks.File` (latest)
+ - `Serilog.Extensions.Hosting` (latest)
+ - Change `` and `` to demote App.xaml from ApplicationDefinition
+
+ Edit App.xaml: Remove `StartupUri="MainWindow.xaml"`. Keep `x:Class="SharepointToolbox.App"`.
+
+ Edit App.xaml.cs: Replace default App class with Generic Host entry point pattern:
+ ```csharp
+ public partial class App : Application
+ {
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ using IHost host = Host.CreateDefaultBuilder(args)
+ .UseSerilog((ctx, cfg) => cfg
+ .WriteTo.File(
+ Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "SharepointToolbox", "logs", "app-.log"),
+ rollingInterval: RollingInterval.Day,
+ retainedFileCountLimit: 30))
+ .ConfigureServices(RegisterServices)
+ .Build();
+
+ host.Start();
+ App app = new();
+ app.InitializeComponent();
+ app.MainWindow = host.Services.GetRequiredService();
+ app.MainWindow.Visibility = Visibility.Visible;
+ app.Run();
+ }
+
+ private static void RegisterServices(HostBuilderContext ctx, IServiceCollection services)
+ {
+ // Placeholder — services registered in subsequent plans
+ services.AddSingleton();
+ }
+ }
+ ```
+
+ Leave MainWindow.xaml and MainWindow.xaml.cs as the default WPF template output — they will be replaced in plan 01-06.
+
+ Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` and fix any errors before moving to Task 2.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5
+
+ Build output shows "Build succeeded" with 0 errors. App.xaml has no StartupUri. csproj contains PublishTrimmed=false and StartupObject.
+
+
+
+ Task 2: Create xUnit test project with stub test files
+
+ SharepointToolbox.Tests/SharepointToolbox.Tests.csproj,
+ SharepointToolbox.Tests/Services/ProfileServiceTests.cs,
+ SharepointToolbox.Tests/Services/SettingsServiceTests.cs,
+ SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs,
+ SharepointToolbox.Tests/Auth/SessionManagerTests.cs,
+ SharepointToolbox.Tests/ViewModels/FeatureViewModelBaseTests.cs,
+ SharepointToolbox.Tests/Localization/TranslationSourceTests.cs,
+ SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs
+
+
+ Run from the repo root:
+ ```
+ dotnet new xunit -n SharepointToolbox.Tests -f net10.0
+ dotnet sln add SharepointToolbox.Tests/SharepointToolbox.Tests.csproj
+ dotnet add SharepointToolbox.Tests/SharepointToolbox.Tests.csproj reference SharepointToolbox/SharepointToolbox.csproj
+ ```
+
+ Edit SharepointToolbox.Tests/SharepointToolbox.Tests.csproj:
+ - Add `Moq` (latest) NuGet package
+ - Add `Microsoft.NET.Test.Sdk` (already included in xunit template)
+ - Add `enable`, `enable`
+
+ Create stub test files — each file compiles but has a single `[Fact(Skip = "Not implemented yet")]` test so the suite passes (no failures, just skips):
+
+ **SharepointToolbox.Tests/Services/ProfileServiceTests.cs**
+ ```csharp
+ namespace SharepointToolbox.Tests.Services;
+ public class ProfileServiceTests
+ {
+ [Fact(Skip = "Wave 0 stub — implemented in plan 01-03")]
+ public void SaveAndLoad_RoundTrips_Profiles() { }
+ }
+ ```
+
+ Create identical stub pattern for:
+ - `SettingsServiceTests.cs` — class `SettingsServiceTests`, skip reason "plan 01-03"
+ - `MsalClientFactoryTests.cs` — class `MsalClientFactoryTests`, skip reason "plan 01-04"
+ - `SessionManagerTests.cs` — class `SessionManagerTests`, skip reason "plan 01-04"
+ - `FeatureViewModelBaseTests.cs` — class `FeatureViewModelBaseTests`, skip reason "plan 01-06"
+ - `TranslationSourceTests.cs` — class `TranslationSourceTests`, skip reason "plan 01-05"
+ - `LoggingIntegrationTests.cs` — class `LoggingIntegrationTests`, skip reason "plan 01-05"
+
+ Run `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --no-build` after building to confirm all tests are skipped (0 failed).
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj 2>&1 | tail -10
+
+ dotnet test shows 0 failed, 7 skipped (or similar). All stub test files exist in correct subdirectories.
+
+
+
+
+
+- `dotnet build SharepointToolbox.sln` succeeds with 0 errors
+- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` shows 0 failures
+- App.xaml contains no StartupUri attribute
+- SharepointToolbox.csproj contains `false`
+- SharepointToolbox.csproj contains `SharepointToolbox.App`
+- App.xaml.cs Main method is decorated with `[STAThread]`
+
+
+
+Solution compiles cleanly. Both projects in the solution. Test runner executes without failures. Generic Host wiring is correct (most critical risk for this plan — wrong STA threading causes runtime crash).
+
+
+
diff --git a/.planning/phases/01-foundation/01-02-PLAN.md b/.planning/phases/01-foundation/01-02-PLAN.md
new file mode 100644
index 0000000..9250850
--- /dev/null
+++ b/.planning/phases/01-foundation/01-02-PLAN.md
@@ -0,0 +1,341 @@
+---
+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.
+
+
+
diff --git a/.planning/phases/01-foundation/01-03-PLAN.md b/.planning/phases/01-foundation/01-03-PLAN.md
new file mode 100644
index 0000000..551f645
--- /dev/null
+++ b/.planning/phases/01-foundation/01-03-PLAN.md
@@ -0,0 +1,254 @@
+---
+phase: 01-foundation
+plan: 03
+type: execute
+wave: 2
+depends_on:
+ - 01-01
+ - 01-02
+files_modified:
+ - SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs
+ - SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs
+ - SharepointToolbox/Services/ProfileService.cs
+ - SharepointToolbox/Services/SettingsService.cs
+ - SharepointToolbox.Tests/Services/ProfileServiceTests.cs
+ - SharepointToolbox.Tests/Services/SettingsServiceTests.cs
+autonomous: true
+requirements:
+ - FOUND-02
+ - FOUND-10
+ - FOUND-12
+must_haves:
+ truths:
+ - "ProfileService reads Sharepoint_Export_profiles.json without migration — field names are the contract"
+ - "SettingsService reads Sharepoint_Settings.json preserving dataFolder and lang fields"
+ - "Write operations use write-then-replace (file.tmp → validate → File.Move) with SemaphoreSlim(1)"
+ - "ProfileService unit tests: SaveAndLoad round-trips, corrupt file recovery, concurrent write safety"
+ - "SettingsService unit tests: SaveAndLoad round-trips, default settings when file missing"
+ artifacts:
+ - path: "SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs"
+ provides: "File I/O for profiles JSON with write-then-replace"
+ contains: "SemaphoreSlim"
+ - path: "SharepointToolbox/Services/ProfileService.cs"
+ provides: "CRUD operations on TenantProfile collection"
+ exports: ["AddProfile", "RenameProfile", "DeleteProfile", "GetProfiles"]
+ - path: "SharepointToolbox/Services/SettingsService.cs"
+ provides: "Read/write for app settings including data folder and language"
+ exports: ["GetSettings", "SaveSettings"]
+ - path: "SharepointToolbox.Tests/Services/ProfileServiceTests.cs"
+ provides: "Unit tests covering FOUND-02 and FOUND-10"
+ key_links:
+ - from: "SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs"
+ to: "Sharepoint_Export_profiles.json"
+ via: "System.Text.Json deserialization of { profiles: [...] } wrapper"
+ pattern: "profiles"
+ - from: "SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs"
+ to: "Sharepoint_Settings.json"
+ via: "System.Text.Json deserialization of { dataFolder, lang }"
+ pattern: "dataFolder"
+---
+
+
+Build the persistence layer: ProfileRepository and SettingsRepository (Infrastructure) plus ProfileService and SettingsService (Services layer). Implement write-then-replace safety. Write unit tests that validate the round-trip and edge cases.
+
+Purpose: Profiles and settings are the first user-visible data. Corrupt files or wrong field names would break existing users' data on migration. Unit tests lock in the JSON schema contract.
+Output: 4 production files + 2 test files with passing unit tests.
+
+
+
+@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/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-RESEARCH.md
+@.planning/phases/01-foundation/01-02-SUMMARY.md
+
+
+
+```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;
+}
+```
+
+
+// Sharepoint_Export_profiles.json
+{ "profiles": [{ "name": "...", "tenantUrl": "...", "clientId": "..." }] }
+
+// Sharepoint_Settings.json
+{ "dataFolder": "...", "lang": "en" }
+
+
+
+
+
+
+ Task 1: ProfileRepository and ProfileService with write-then-replace
+
+ SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs,
+ SharepointToolbox/Services/ProfileService.cs,
+ SharepointToolbox.Tests/Services/ProfileServiceTests.cs
+
+
+ - Test: SaveAsync then LoadAsync round-trips a list of TenantProfiles with correct field values
+ - Test: LoadAsync on missing file returns empty list (no exception)
+ - Test: LoadAsync on corrupt JSON throws InvalidDataException (not silently returns empty)
+ - Test: Concurrent SaveAsync calls don't corrupt the file (SemaphoreSlim ensures ordering)
+ - Test: ProfileService.AddProfile assigns the new profile and persists immediately
+ - Test: ProfileService.RenameProfile changes Name, persists, throws if profile not found
+ - Test: ProfileService.DeleteProfile removes by Name, throws if not found
+ - Test: Saved JSON wraps profiles in { "profiles": [...] } root object (schema compatibility)
+
+
+ Create `Infrastructure/Persistence/` and `Services/` directories.
+
+ **ProfileRepository.cs** — handles raw file I/O:
+ ```csharp
+ namespace SharepointToolbox.Infrastructure.Persistence;
+
+ public class ProfileRepository
+ {
+ private readonly string _filePath;
+ private readonly SemaphoreSlim _writeLock = new(1, 1);
+
+ public ProfileRepository(string filePath)
+ {
+ _filePath = filePath;
+ }
+
+ public async Task> LoadAsync()
+ {
+ if (!File.Exists(_filePath))
+ return Array.Empty();
+
+ var json = await File.ReadAllTextAsync(_filePath, Encoding.UTF8);
+ var root = JsonSerializer.Deserialize(json,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+ return root?.Profiles ?? Array.Empty();
+ }
+
+ public async Task SaveAsync(IReadOnlyList profiles)
+ {
+ await _writeLock.WaitAsync();
+ try
+ {
+ var root = new ProfilesRoot { Profiles = profiles.ToList() };
+ var json = JsonSerializer.Serialize(root,
+ new JsonSerializerOptions { WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+ var tmpPath = _filePath + ".tmp";
+ Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
+ await File.WriteAllTextAsync(tmpPath, json, Encoding.UTF8);
+ // Validate round-trip before replacing
+ JsonDocument.Parse(await File.ReadAllTextAsync(tmpPath)).Dispose();
+ File.Move(tmpPath, _filePath, overwrite: true);
+ }
+ finally { _writeLock.Release(); }
+ }
+
+ private sealed class ProfilesRoot
+ {
+ public List Profiles { get; set; } = new();
+ }
+ }
+ ```
+ Note: Use `PropertyNamingPolicy = JsonNamingPolicy.CamelCase` to serialize `Name`→`name`, `TenantUrl`→`tenantUrl`, `ClientId`→`clientId` matching the existing JSON schema.
+
+ **ProfileService.cs** — CRUD on top of repository:
+ - Constructor takes `ProfileRepository` (inject via DI later; for now accept in constructor)
+ - `Task> GetProfilesAsync()`
+ - `Task AddProfileAsync(TenantProfile profile)` — validates Name not empty, TenantUrl valid URL, ClientId not empty; throws `ArgumentException` for invalid inputs
+ - `Task RenameProfileAsync(string existingName, string newName)` — throws `KeyNotFoundException` if not found
+ - `Task DeleteProfileAsync(string name)` — throws `KeyNotFoundException` if not found
+ - All mutations load → modify in-memory list → save (single-load-modify-save to preserve order)
+
+ **ProfileServiceTests.cs** — Replace the stub with real tests using temp file paths:
+ ```csharp
+ public class ProfileServiceTests : IDisposable
+ {
+ private readonly string _tempFile = Path.GetTempFileName();
+ // Dispose deletes temp file
+
+ [Fact]
+ public async Task SaveAndLoad_RoundTrips_Profiles() { ... }
+ // etc.
+ }
+ ```
+ Tests must use a temp file, not the real user data file. All tests in `[Trait("Category", "Unit")]`.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~ProfileServiceTests" 2>&1 | tail -10
+
+ All ProfileServiceTests pass (no skips). JSON output uses camelCase field names matching existing schema. Write-then-replace with SemaphoreSlim implemented.
+
+
+
+ Task 2: SettingsRepository and SettingsService
+
+ SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs,
+ SharepointToolbox/Services/SettingsService.cs,
+ SharepointToolbox.Tests/Services/SettingsServiceTests.cs
+
+
+ - Test: LoadAsync returns default settings (dataFolder = empty string, lang = "en") when file missing
+ - Test: SaveAsync then LoadAsync round-trips dataFolder and lang values exactly
+ - Test: Serialized JSON contains "dataFolder" and "lang" keys (not DataFolder/Lang — schema compatibility)
+ - Test: SaveAsync uses write-then-replace (tmp file created, then moved)
+ - Test: SettingsService.SetLanguageAsync("fr") persists lang="fr"
+ - Test: SettingsService.SetDataFolderAsync("C:\\Exports") persists dataFolder path
+
+
+ **AppSettings model** (add to `Core/Models/AppSettings.cs`):
+ ```csharp
+ namespace SharepointToolbox.Core.Models;
+ public class AppSettings
+ {
+ public string DataFolder { get; set; } = string.Empty;
+ public string Lang { get; set; } = "en";
+ }
+ ```
+ Note: STJ with `PropertyNamingPolicy.CamelCase` will serialize `DataFolder`→`dataFolder`, `Lang`→`lang`.
+
+ **SettingsRepository.cs** — same write-then-replace pattern as ProfileRepository:
+ - `Task LoadAsync()` — returns `new AppSettings()` if file missing; throws `InvalidDataException` on corrupt JSON
+ - `Task SaveAsync(AppSettings settings)` — write-then-replace with `SemaphoreSlim(1)` and camelCase serialization
+
+ **SettingsService.cs**:
+ - Constructor takes `SettingsRepository`
+ - `Task GetSettingsAsync()`
+ - `Task SetLanguageAsync(string cultureCode)` — validates "en" or "fr"; throws `ArgumentException` otherwise
+ - `Task SetDataFolderAsync(string path)` — saves path (empty string allowed — means default)
+
+ **SettingsServiceTests.cs** — Replace stub with real tests using temp file.
+ All tests in `[Trait("Category", "Unit")]`.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SettingsServiceTests" 2>&1 | tail -10
+
+ All SettingsServiceTests pass. AppSettings serializes to dataFolder/lang (camelCase). Default settings returned when file is absent.
+
+
+
+
+
+- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "Category=Unit"` — all pass
+- JSON output from ProfileRepository contains `"profiles"` root key with `"name"`, `"tenantUrl"`, `"clientId"` field names
+- JSON output from SettingsRepository contains `"dataFolder"` and `"lang"` field names
+- Both repositories use `SemaphoreSlim(1)` write lock
+- Both repositories use write-then-replace (`.tmp` file then `File.Move`)
+
+
+
+Unit tests green for ProfileService and SettingsService. JSON schema compatibility verified by test assertions on serialized output. Write-then-replace pattern protects against crash-corruption.
+
+
+
diff --git a/.planning/phases/01-foundation/01-04-PLAN.md b/.planning/phases/01-foundation/01-04-PLAN.md
new file mode 100644
index 0000000..d6e6cb1
--- /dev/null
+++ b/.planning/phases/01-foundation/01-04-PLAN.md
@@ -0,0 +1,266 @@
+---
+phase: 01-foundation
+plan: 04
+type: execute
+wave: 3
+depends_on:
+ - 01-02
+ - 01-03
+files_modified:
+ - SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs
+ - SharepointToolbox/Services/SessionManager.cs
+ - SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs
+ - SharepointToolbox.Tests/Auth/SessionManagerTests.cs
+autonomous: true
+requirements:
+ - FOUND-03
+ - FOUND-04
+must_haves:
+ truths:
+ - "MsalClientFactory creates one IPublicClientApplication per ClientId — never shares instances across tenants"
+ - "MsalCacheHelper persists token cache to %AppData%\\SharepointToolbox\\auth\\msal_{clientId}.cache"
+ - "SessionManager.GetOrCreateContextAsync returns a cached ClientContext on second call without interactive login"
+ - "SessionManager.ClearSessionAsync removes MSAL accounts and disposes ClientContext for the specified tenant"
+ - "SessionManager is the only class in the codebase holding ClientContext instances"
+ artifacts:
+ - path: "SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs"
+ provides: "Per-ClientId IPublicClientApplication with MsalCacheHelper"
+ contains: "MsalCacheHelper"
+ - path: "SharepointToolbox/Services/SessionManager.cs"
+ provides: "Singleton holding all ClientContext instances and auth state"
+ exports: ["GetOrCreateContextAsync", "ClearSessionAsync", "IsAuthenticated"]
+ key_links:
+ - from: "SharepointToolbox/Services/SessionManager.cs"
+ to: "SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs"
+ via: "Injected dependency — SessionManager calls MsalClientFactory.GetOrCreateAsync(clientId)"
+ pattern: "GetOrCreateAsync"
+ - from: "SharepointToolbox/Services/SessionManager.cs"
+ to: "PnP.Framework AuthenticationManager"
+ via: "CreateWithInteractiveLogin using MSAL PCA"
+ pattern: "AuthenticationManager"
+---
+
+
+Build the authentication layer: MsalClientFactory (per-tenant MSAL client with persistent cache) and SessionManager (singleton holding all live ClientContext instances). This is the security-critical component — one IPublicClientApplication per ClientId, never shared.
+
+Purpose: Every SharePoint operation in Phases 2-4 goes through SessionManager. Getting the per-tenant isolation and token cache correct now prevents auth token bleed between client tenants — a critical security property for MSP use.
+Output: MsalClientFactory + SessionManager + unit tests validating per-tenant isolation.
+
+
+
+@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/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-RESEARCH.md
+@.planning/phases/01-foundation/01-02-SUMMARY.md
+@.planning/phases/01-foundation/01-03-SUMMARY.md
+
+
+
+```csharp
+public class TenantProfile
+{
+ public string Name { get; set; }
+ public string TenantUrl { get; set; }
+ public string ClientId { get; set; }
+}
+```
+
+
+
+
+
+
+ Task 1: MsalClientFactory — per-ClientId PCA with MsalCacheHelper
+
+ SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs,
+ SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs
+
+
+ - Test: GetOrCreateAsync("clientA") and GetOrCreateAsync("clientA") return the same instance (no duplicate creation)
+ - Test: GetOrCreateAsync("clientA") and GetOrCreateAsync("clientB") return different instances (per-tenant isolation)
+ - Test: Concurrent calls to GetOrCreateAsync with same clientId do not create duplicate instances (SemaphoreSlim)
+ - Test: Cache directory path resolves to %AppData%\SharepointToolbox\auth\ (not a hardcoded path)
+
+
+ Create `Infrastructure/Auth/` directory.
+
+ **MsalClientFactory.cs** — implement exactly as per research Pattern 3:
+ ```csharp
+ namespace SharepointToolbox.Infrastructure.Auth;
+
+ public class MsalClientFactory
+ {
+ private readonly Dictionary _clients = new();
+ private readonly SemaphoreSlim _lock = new(1, 1);
+ private readonly string _cacheDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "SharepointToolbox", "auth");
+
+ public async Task GetOrCreateAsync(string clientId)
+ {
+ await _lock.WaitAsync();
+ try
+ {
+ if (_clients.TryGetValue(clientId, out var existing))
+ return existing;
+
+ var storageProps = new StorageCreationPropertiesBuilder(
+ $"msal_{clientId}.cache", _cacheDir)
+ .Build();
+
+ var pca = PublicClientApplicationBuilder
+ .Create(clientId)
+ .WithDefaultRedirectUri()
+ .WithLegacyCacheCompatibility(false)
+ .Build();
+
+ var helper = await MsalCacheHelper.CreateAsync(storageProps);
+ helper.RegisterCache(pca.UserTokenCache);
+
+ _clients[clientId] = pca;
+ return pca;
+ }
+ finally { _lock.Release(); }
+ }
+ }
+ ```
+
+ **MsalClientFactoryTests.cs** — Replace stub. Tests for per-ClientId isolation and idempotency.
+ Since MsalCacheHelper creates real files, tests must use a temp directory and clean up.
+ Use `[Trait("Category", "Unit")]` on all tests.
+ Mock or subclass `MsalClientFactory` for the concurrent test to avoid real MSAL overhead.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~MsalClientFactoryTests" 2>&1 | tail -10
+
+ MsalClientFactoryTests pass. Different clientIds produce different instances. Same clientId produces same instance on second call.
+
+
+
+ Task 2: SessionManager — singleton ClientContext holder
+
+ SharepointToolbox/Services/SessionManager.cs,
+ SharepointToolbox.Tests/Auth/SessionManagerTests.cs
+
+
+ - Test: IsAuthenticated(tenantUrl) returns false before any authentication
+ - Test: After GetOrCreateContextAsync succeeds, IsAuthenticated(tenantUrl) returns true
+ - Test: ClearSessionAsync removes authentication state for the specified tenant
+ - Test: ClearSessionAsync on unknown tenantUrl does not throw (idempotent)
+ - Test: ClientContext is disposed on ClearSessionAsync (verify via mock/wrapper)
+ - Test: GetOrCreateContextAsync throws ArgumentException for null/empty tenantUrl or clientId
+
+
+ **SessionManager.cs** — singleton, owns all ClientContext instances:
+ ```csharp
+ namespace SharepointToolbox.Services;
+
+ public class SessionManager
+ {
+ private readonly MsalClientFactory _msalFactory;
+ private readonly Dictionary _contexts = new();
+ private readonly SemaphoreSlim _lock = new(1, 1);
+
+ public SessionManager(MsalClientFactory msalFactory)
+ {
+ _msalFactory = msalFactory;
+ }
+
+ public bool IsAuthenticated(string tenantUrl) =>
+ _contexts.ContainsKey(NormalizeUrl(tenantUrl));
+
+ ///
+ /// Returns existing ClientContext or creates a new one via interactive MSAL login.
+ /// Only SessionManager holds ClientContext instances — never return to callers for storage.
+ ///
+ public async Task GetOrCreateContextAsync(
+ TenantProfile profile,
+ CancellationToken ct = default)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(profile.TenantUrl);
+ ArgumentException.ThrowIfNullOrEmpty(profile.ClientId);
+
+ var key = NormalizeUrl(profile.TenantUrl);
+
+ await _lock.WaitAsync(ct);
+ try
+ {
+ if (_contexts.TryGetValue(key, out var existing))
+ return existing;
+
+ var pca = await _msalFactory.GetOrCreateAsync(profile.ClientId);
+ var authManager = AuthenticationManager.CreateWithInteractiveLogin(
+ profile.ClientId,
+ (url, port) =>
+ {
+ // WAM/browser-based interactive login
+ return pca.AcquireTokenInteractive(
+ new[] { "https://graph.microsoft.com/.default" })
+ .ExecuteAsync(ct);
+ });
+
+ var ctx = await authManager.GetContextAsync(profile.TenantUrl);
+ _contexts[key] = ctx;
+ return ctx;
+ }
+ finally { _lock.Release(); }
+ }
+
+ ///
+ /// Clears MSAL accounts and disposes the ClientContext for the given tenant.
+ /// Called by "Clear Session" button and on tenant profile deletion.
+ ///
+ public async Task ClearSessionAsync(string tenantUrl)
+ {
+ var key = NormalizeUrl(tenantUrl);
+ await _lock.WaitAsync();
+ try
+ {
+ if (_contexts.TryGetValue(key, out var ctx))
+ {
+ ctx.Dispose();
+ _contexts.Remove(key);
+ }
+ }
+ finally { _lock.Release(); }
+ }
+
+ private static string NormalizeUrl(string url) =>
+ url.TrimEnd('/').ToLowerInvariant();
+ }
+ ```
+
+ Note on PnP AuthenticationManager: The exact API for `CreateWithInteractiveLogin` with MSAL PCA may vary in PnP.Framework 1.18.0. The implementation above is a skeleton — executor should verify the PnP API surface and adjust accordingly. The key invariant is: `MsalClientFactory.GetOrCreateAsync` is called first, then PnP creates the context using the returned PCA. Do NOT call `PublicClientApplicationBuilder.Create` directly in SessionManager.
+
+ **SessionManagerTests.cs** — Replace stub. Use Moq to mock `MsalClientFactory`.
+ Test `IsAuthenticated`, `ClearSessionAsync` idempotency, and argument validation.
+ Interactive login cannot be tested in unit tests — mark `GetOrCreateContextAsync_CreatesContext` as `[Fact(Skip = "Requires interactive MSAL — integration test only")]`.
+ All other tests in `[Trait("Category", "Unit")]`.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SessionManagerTests" 2>&1 | tail -10
+
+ SessionManagerTests pass (interactive login test skipped). IsAuthenticated, ClearSessionAsync, and argument validation tests are green. SessionManager is the only holder of ClientContext.
+
+
+
+
+
+- `dotnet test --filter "Category=Unit"` passes
+- MsalClientFactory._clients dictionary holds one entry per unique clientId
+- SessionManager.ClearSessionAsync calls ctx.Dispose() (verified via test)
+- No class outside SessionManager stores a ClientContext reference
+
+
+
+Auth layer unit tests green. Per-tenant isolation (one PCA per ClientId, one context per tenantUrl) confirmed by tests. SessionManager is the single source of truth for authenticated connections.
+
+
+
diff --git a/.planning/phases/01-foundation/01-05-PLAN.md b/.planning/phases/01-foundation/01-05-PLAN.md
new file mode 100644
index 0000000..f53c59e
--- /dev/null
+++ b/.planning/phases/01-foundation/01-05-PLAN.md
@@ -0,0 +1,253 @@
+---
+phase: 01-foundation
+plan: 05
+type: execute
+wave: 3
+depends_on:
+ - 01-02
+files_modified:
+ - SharepointToolbox/Localization/TranslationSource.cs
+ - SharepointToolbox/Localization/Strings.resx
+ - SharepointToolbox/Localization/Strings.fr.resx
+ - SharepointToolbox.Tests/Localization/TranslationSourceTests.cs
+ - SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs
+autonomous: true
+requirements:
+ - FOUND-08
+ - FOUND-09
+must_haves:
+ truths:
+ - "TranslationSource.Instance[key] returns the EN string for English culture"
+ - "Setting TranslationSource.Instance.CurrentCulture to 'fr' changes string lookup without app restart"
+ - "PropertyChanged fires with empty string key (signals all properties changed) on culture switch"
+ - "Serilog writes to rolling daily log file at %AppData%\\SharepointToolbox\\logs\\app-{date}.log"
+ - "Serilog ILogger is injectable via DI — does not use static Log.Logger directly in services"
+ - "LoggingIntegrationTests verify a log file is created and contains the written message"
+ artifacts:
+ - path: "SharepointToolbox/Localization/TranslationSource.cs"
+ provides: "Singleton INotifyPropertyChanged string lookup for runtime culture switching"
+ contains: "PropertyChangedEventArgs(string.Empty)"
+ - path: "SharepointToolbox/Localization/Strings.resx"
+ provides: "EN default resource file with all Phase 1 UI strings"
+ - path: "SharepointToolbox/Localization/Strings.fr.resx"
+ provides: "FR overlay — all keys present, values stubbed with EN text"
+ key_links:
+ - from: "SharepointToolbox/Localization/TranslationSource.cs"
+ to: "SharepointToolbox/Localization/Strings.resx"
+ via: "ResourceManager from Strings class"
+ pattern: "Strings.ResourceManager"
+ - from: "MainWindow.xaml (plan 01-06)"
+ to: "SharepointToolbox/Localization/TranslationSource.cs"
+ via: "XAML binding: Source={x:Static loc:TranslationSource.Instance}, Path=[key]"
+ pattern: "TranslationSource.Instance"
+---
+
+
+Build the logging infrastructure and dynamic localization system. Serilog wired into Generic Host. TranslationSource singleton enabling runtime culture switching without restart.
+
+Purpose: Every feature phase needs ILogger injection and localizable strings. The TranslationSource pattern (INotifyPropertyChanged indexer binding) is the only approach that refreshes WPF bindings at runtime — standard x:Static resx bindings are evaluated once at startup.
+Output: TranslationSource + EN/FR resx files + Serilog integration + unit/integration tests.
+
+
+
+@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/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-RESEARCH.md
+@.planning/phases/01-foundation/01-02-SUMMARY.md
+
+
+
+```csharp
+public sealed class LanguageChangedMessage : ValueChangedMessage
+{
+ public LanguageChangedMessage(string cultureCode) : base(cultureCode) { }
+}
+```
+
+
+
+
+
+
+ Task 1: TranslationSource singleton + EN/FR resx files
+
+ SharepointToolbox/Localization/TranslationSource.cs,
+ SharepointToolbox/Localization/Strings.resx,
+ SharepointToolbox/Localization/Strings.fr.resx,
+ SharepointToolbox.Tests/Localization/TranslationSourceTests.cs
+
+
+ - Test: TranslationSource.Instance["app.title"] returns "SharePoint Toolbox" (EN default)
+ - Test: After setting CurrentCulture to fr-FR, TranslationSource.Instance["app.title"] returns FR value (or EN fallback if FR not defined)
+ - Test: Changing CurrentCulture fires PropertyChanged with EventArgs having empty string PropertyName
+ - Test: Setting same culture twice does NOT fire PropertyChanged (equality check)
+ - Test: Missing key returns "[key]" not null (prevents NullReferenceException in bindings)
+ - Test: TranslationSource.Instance is same instance on multiple accesses (singleton)
+
+
+ Create `Localization/` directory.
+
+ **TranslationSource.cs** — implement exactly as per research Pattern 4:
+ ```csharp
+ namespace SharepointToolbox.Localization;
+
+ public class TranslationSource : INotifyPropertyChanged
+ {
+ public static readonly TranslationSource Instance = new();
+ private ResourceManager _resourceManager = Strings.ResourceManager;
+ private CultureInfo _currentCulture = CultureInfo.CurrentUICulture;
+
+ public string this[string key] =>
+ _resourceManager.GetString(key, _currentCulture) ?? $"[{key}]";
+
+ public CultureInfo CurrentCulture
+ {
+ get => _currentCulture;
+ set
+ {
+ if (Equals(_currentCulture, value)) return;
+ _currentCulture = value;
+ Thread.CurrentThread.CurrentUICulture = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ }
+ ```
+
+ **Strings.resx** — Create with ResXResourceWriter or manually as XML. Include ALL Phase 1 UI strings. Key naming mirrors existing PowerShell convention (see CONTEXT.md).
+
+ Required keys (minimum set for Phase 1 — add more as needed during shell implementation):
+ ```
+ app.title = SharePoint Toolbox
+ toolbar.connect = Connect
+ toolbar.manage = Manage Profiles...
+ toolbar.clear = Clear Session
+ tab.permissions = Permissions
+ tab.storage = Storage
+ tab.search = File Search
+ tab.duplicates = Duplicates
+ tab.templates = Templates
+ tab.bulk = Bulk Operations
+ tab.structure = Folder Structure
+ tab.settings = Settings
+ tab.comingsoon = Coming soon
+ btn.cancel = Cancel
+ settings.language = Language
+ settings.lang.en = English
+ settings.lang.fr = French
+ settings.folder = Data output folder
+ settings.browse = Browse...
+ profile.name = Profile name
+ profile.url = Tenant URL
+ profile.clientid = Client ID
+ profile.add = Add
+ profile.rename = Rename
+ profile.delete = Delete
+ status.ready = Ready
+ status.cancelled = Operation cancelled
+ err.auth.failed = Authentication failed. Check tenant URL and Client ID.
+ err.generic = An error occurred. See log for details.
+ ```
+
+ **Strings.fr.resx** — All same keys, values stubbed with EN text. A comment `` on each value is acceptable. FR completeness is Phase 5.
+
+ **TranslationSourceTests.cs** — Replace stub with real tests.
+ All tests in `[Trait("Category", "Unit")]`.
+ TranslationSource.Instance is a static singleton — reset culture to EN in test teardown to avoid test pollution.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~TranslationSourceTests" 2>&1 | tail -10
+
+ TranslationSourceTests pass. Missing key returns "[key]". Culture switch fires PropertyChanged with empty property name. Strings.resx contains all required keys.
+
+
+
+ Task 2: Serilog integration tests and App.xaml.cs logging wiring verification
+
+ SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs
+
+
+ The Serilog file sink is already wired in App.xaml.cs (plan 01-01). This task writes an integration test to verify the wiring produces an actual log file and that the LogPanelSink (from plan 01-02) can be instantiated without errors.
+
+ **LoggingIntegrationTests.cs** — Replace stub:
+ ```csharp
+ [Trait("Category", "Integration")]
+ public class LoggingIntegrationTests : IDisposable
+ {
+ private readonly string _tempLogDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+
+ [Fact]
+ public async Task Serilog_WritesLogFile_WhenMessageLogged()
+ {
+ Directory.CreateDirectory(_tempLogDir);
+ var logFile = Path.Combine(_tempLogDir, "test-.log");
+
+ var logger = new LoggerConfiguration()
+ .WriteTo.File(logFile, rollingInterval: RollingInterval.Day)
+ .CreateLogger();
+
+ logger.Information("Test log message {Value}", 42);
+ await logger.DisposeAsync();
+
+ var files = Directory.GetFiles(_tempLogDir, "*.log");
+ Assert.Single(files);
+ var content = await File.ReadAllTextAsync(files[0]);
+ Assert.Contains("Test log message 42", content);
+ }
+
+ [Fact]
+ public void LogPanelSink_CanBeInstantiated_WithRichTextBox()
+ {
+ // Verify the sink type instantiates without throwing
+ // Cannot test actual UI writes without STA thread — this is structural smoke only
+ var sinkType = typeof(LogPanelSink);
+ Assert.NotNull(sinkType);
+ Assert.True(typeof(ILogEventSink).IsAssignableFrom(sinkType));
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_tempLogDir))
+ Directory.Delete(_tempLogDir, recursive: true);
+ }
+ }
+ ```
+
+ Note: `LogPanelSink` instantiation test avoids creating a real `RichTextBox` (requires STA thread). It only verifies the type implements `ILogEventSink`. Full UI-thread integration is verified in the manual checkpoint (plan 01-08).
+
+ Also update `App.xaml.cs` RegisterServices to add `LogPanelSink` registration comment for plan 01-06:
+ ```csharp
+ // LogPanelSink registered in plan 01-06 after MainWindow is created
+ // (requires RichTextBox reference from MainWindow)
+ ```
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~LoggingIntegrationTests" 2>&1 | tail -10
+
+ LoggingIntegrationTests pass. Log file created in temp directory with expected content. LogPanelSink type check passes.
+
+
+
+
+
+- `dotnet test --filter "Category=Unit"` and `--filter "Category=Integration"` both pass
+- Strings.resx contains all keys listed in the action section
+- Strings.fr.resx contains same key set (verified by comparing key counts)
+- TranslationSource.Instance is not null
+- PropertyChanged fires with `string.Empty` PropertyName on culture change
+
+
+
+Localization system supports runtime culture switching confirmed by tests. All Phase 1 UI strings defined in EN resx. FR resx has same key set (stubbed). Serilog integration test verifies log file creation.
+
+
+
diff --git a/.planning/phases/01-foundation/01-06-PLAN.md b/.planning/phases/01-foundation/01-06-PLAN.md
new file mode 100644
index 0000000..0bb47fc
--- /dev/null
+++ b/.planning/phases/01-foundation/01-06-PLAN.md
@@ -0,0 +1,393 @@
+---
+phase: 01-foundation
+plan: 06
+type: execute
+wave: 4
+depends_on:
+ - 01-03
+ - 01-04
+ - 01-05
+files_modified:
+ - SharepointToolbox/ViewModels/FeatureViewModelBase.cs
+ - SharepointToolbox/ViewModels/MainWindowViewModel.cs
+ - SharepointToolbox/ViewModels/ProfileManagementViewModel.cs
+ - SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs
+ - SharepointToolbox/Views/MainWindow.xaml
+ - SharepointToolbox/Views/MainWindow.xaml.cs
+ - SharepointToolbox/App.xaml.cs
+ - SharepointToolbox.Tests/ViewModels/FeatureViewModelBaseTests.cs
+autonomous: true
+requirements:
+ - FOUND-01
+ - FOUND-05
+ - FOUND-06
+ - FOUND-07
+must_haves:
+ truths:
+ - "MainWindow displays: top toolbar, center TabControl with 8 feature tabs, bottom RichTextBox log panel (150px), bottom StatusBar"
+ - "Toolbar ComboBox bound to TenantProfiles ObservableCollection; selecting a different item triggers TenantSwitchedMessage"
+ - "FeatureViewModelBase provides CancellationTokenSource lifecycle, IsRunning, IProgress, OperationCanceledException handling"
+ - "Global exception handlers (DispatcherUnhandledException + TaskScheduler.UnobservedTaskException) funnel to log panel + MessageBox"
+ - "LogPanelSink wired to MainWindow RichTextBox after Generic Host starts"
+ - "FeatureViewModelBaseTests: progress reporting, cancellation, and error handling all green"
+ artifacts:
+ - path: "SharepointToolbox/ViewModels/FeatureViewModelBase.cs"
+ provides: "Base class for all feature ViewModels with canonical async command pattern"
+ contains: "CancellationTokenSource"
+ - path: "SharepointToolbox/ViewModels/MainWindowViewModel.cs"
+ provides: "Shell ViewModel with TenantProfiles and connection state"
+ contains: "ObservableCollection"
+ - path: "SharepointToolbox/Views/MainWindow.xaml"
+ provides: "WPF shell with toolbar, TabControl, log panel, StatusBar"
+ contains: "RichTextBox"
+ key_links:
+ - from: "SharepointToolbox/Views/MainWindow.xaml"
+ to: "SharepointToolbox/ViewModels/MainWindowViewModel.cs"
+ via: "DataContext binding in MainWindow.xaml.cs constructor"
+ pattern: "DataContext"
+ - from: "SharepointToolbox/App.xaml.cs"
+ to: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs"
+ via: "LoggerConfiguration.WriteTo.Sink(new LogPanelSink(mainWindow.LogPanel))"
+ pattern: "LogPanelSink"
+ - from: "SharepointToolbox/ViewModels/MainWindowViewModel.cs"
+ to: "SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs"
+ via: "WeakReferenceMessenger.Default.Send on ComboBox selection change"
+ pattern: "TenantSwitchedMessage"
+---
+
+
+Build the WPF shell — MainWindow XAML + all ViewModels. Wire LogPanelSink to the RichTextBox. Implement FeatureViewModelBase with the canonical async pattern. Register global exception handlers.
+
+Purpose: This is the first time the application visually exists. All subsequent feature plans add TabItems to the already-wired TabControl.
+Output: Runnable WPF application showing the shell with placeholder tabs, log panel, and status bar.
+
+
+
+@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/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-RESEARCH.md
+@.planning/phases/01-foundation/01-03-SUMMARY.md
+@.planning/phases/01-foundation/01-04-SUMMARY.md
+@.planning/phases/01-foundation/01-05-SUMMARY.md
+
+
+
+```csharp
+public class TenantProfile { string Name; string TenantUrl; string ClientId; }
+public record OperationProgress(int Current, int Total, string Message)
+```
+
+
+```csharp
+public sealed class TenantSwitchedMessage : ValueChangedMessage
+public sealed class LanguageChangedMessage : ValueChangedMessage
+```
+
+
+```csharp
+// ProfileService: GetProfilesAsync(), AddProfileAsync(), RenameProfileAsync(), DeleteProfileAsync()
+// SessionManager: IsAuthenticated(url), GetOrCreateContextAsync(profile, ct), ClearSessionAsync(url)
+// SettingsService: GetSettingsAsync(), SetLanguageAsync(code), SetDataFolderAsync(path)
+```
+
+
+```csharp
+// TranslationSource.Instance[key] — binding: Source={x:Static loc:TranslationSource.Instance}, Path=[key]
+```
+
+
+// Toolbar (L→R): ComboBox (220px) → Button "Connect" → Button "Manage Profiles..." → separator → Button "Clear Session"
+// TabControl: 8 tabs (Permissions, Storage, File Search, Duplicates, Templates, Bulk Operations, Folder Structure, Settings)
+// Log panel: RichTextBox, 150px tall, always visible, x:Name="LogPanel"
+// StatusBar: tenant name | operation status | progress %
+
+
+
+
+
+
+ Task 1: FeatureViewModelBase + unit tests
+
+ SharepointToolbox/ViewModels/FeatureViewModelBase.cs,
+ SharepointToolbox.Tests/ViewModels/FeatureViewModelBaseTests.cs
+
+
+ - Test: IsRunning is true while operation executes, false after completion
+ - Test: ProgressValue and StatusMessage update via IProgress on UI thread
+ - Test: Calling CancelCommand during operation causes StatusMessage to show cancellation message
+ - Test: OperationCanceledException is caught gracefully — IsRunning becomes false, no exception propagates
+ - Test: Exception during operation sets StatusMessage to error text — IsRunning becomes false
+ - Test: RunCommand cannot be invoked while IsRunning (CanExecute returns false)
+
+
+ Create `ViewModels/` directory.
+
+ **FeatureViewModelBase.cs** — implement exactly as per research Pattern 2:
+ ```csharp
+ namespace SharepointToolbox.ViewModels;
+
+ public abstract class FeatureViewModelBase : ObservableRecipient
+ {
+ private CancellationTokenSource? _cts;
+ private readonly ILogger _logger;
+
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(CancelCommand))]
+ private bool _isRunning;
+
+ [ObservableProperty]
+ private string _statusMessage = string.Empty;
+
+ [ObservableProperty]
+ private int _progressValue;
+
+ public IAsyncRelayCommand RunCommand { get; }
+ public RelayCommand CancelCommand { get; }
+
+ protected FeatureViewModelBase(ILogger logger)
+ {
+ _logger = logger;
+ RunCommand = new AsyncRelayCommand(ExecuteAsync, () => !IsRunning);
+ CancelCommand = new RelayCommand(() => _cts?.Cancel(), () => IsRunning);
+ IsActive = true; // Activates ObservableRecipient for WeakReferenceMessenger
+ }
+
+ private async Task ExecuteAsync()
+ {
+ _cts = new CancellationTokenSource();
+ IsRunning = true;
+ StatusMessage = string.Empty;
+ ProgressValue = 0;
+ try
+ {
+ var progress = new Progress(p =>
+ {
+ ProgressValue = p.Total > 0 ? (int)(100.0 * p.Current / p.Total) : 0;
+ StatusMessage = p.Message;
+ WeakReferenceMessenger.Default.Send(new ProgressUpdatedMessage(p));
+ });
+ await RunOperationAsync(_cts.Token, progress);
+ }
+ catch (OperationCanceledException)
+ {
+ StatusMessage = TranslationSource.Instance["status.cancelled"];
+ _logger.LogInformation("Operation cancelled by user.");
+ }
+ catch (Exception ex)
+ {
+ StatusMessage = $"{TranslationSource.Instance["err.generic"]} {ex.Message}";
+ _logger.LogError(ex, "Operation failed.");
+ }
+ finally
+ {
+ IsRunning = false;
+ _cts?.Dispose();
+ _cts = null;
+ }
+ }
+
+ protected abstract Task RunOperationAsync(CancellationToken ct, IProgress progress);
+
+ protected override void OnActivated()
+ {
+ Messenger.Register(this, (r, m) => r.OnTenantSwitched(m.Value));
+ }
+
+ protected virtual void OnTenantSwitched(TenantProfile profile)
+ {
+ // Derived classes override to reset their state
+ }
+ }
+ ```
+
+ Also create `Core/Messages/ProgressUpdatedMessage.cs` (needed for StatusBar update):
+ ```csharp
+ public sealed class ProgressUpdatedMessage : ValueChangedMessage
+ {
+ public ProgressUpdatedMessage(OperationProgress progress) : base(progress) { }
+ }
+ ```
+
+ **FeatureViewModelBaseTests.cs** — Replace stub. Use a concrete test subclass:
+ ```csharp
+ private class TestViewModel : FeatureViewModelBase
+ {
+ public TestViewModel(ILogger logger) : base(logger) { }
+ public Func, Task>? OperationFunc { get; set; }
+ protected override Task RunOperationAsync(CancellationToken ct, IProgress p)
+ => OperationFunc?.Invoke(ct, p) ?? Task.CompletedTask;
+ }
+ ```
+ All tests in `[Trait("Category", "Unit")]`.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~FeatureViewModelBaseTests" 2>&1 | tail -10
+
+ All FeatureViewModelBaseTests pass. IsRunning lifecycle correct. Cancellation handled gracefully. Exception caught with error message set.
+
+
+
+ Task 2: MainWindowViewModel, shell ViewModels, and MainWindow XAML
+
+ SharepointToolbox/ViewModels/MainWindowViewModel.cs,
+ SharepointToolbox/ViewModels/ProfileManagementViewModel.cs,
+ SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs,
+ SharepointToolbox/Views/MainWindow.xaml,
+ SharepointToolbox/Views/MainWindow.xaml.cs,
+ SharepointToolbox/App.xaml.cs
+
+
+ Create `ViewModels/Tabs/` and `Views/` directories.
+
+ **MainWindowViewModel.cs**:
+ ```csharp
+ [ObservableProperty] private TenantProfile? _selectedProfile;
+ [ObservableProperty] private string _connectionStatus = "Not connected";
+ public ObservableCollection TenantProfiles { get; } = new();
+
+ // ConnectCommand: calls SessionManager.GetOrCreateContextAsync(SelectedProfile)
+ // ClearSessionCommand: calls SessionManager.ClearSessionAsync(SelectedProfile.TenantUrl)
+ // ManageProfilesCommand: opens ProfileManagementDialog as modal
+ // OnSelectedProfileChanged (partial): sends TenantSwitchedMessage via WeakReferenceMessenger
+ // LoadProfilesAsync: called on startup, loads from ProfileService
+ ```
+
+ **ProfileManagementViewModel.cs**: Wraps ProfileService for dialog binding.
+ - `ObservableCollection Profiles`
+ - `AddCommand`, `RenameCommand`, `DeleteCommand`
+ - Validates inputs (non-empty Name, valid URL format, non-empty ClientId)
+
+ **SettingsViewModel.cs** (inherits FeatureViewModelBase):
+ - `string SelectedLanguage` bound to language ComboBox
+ - `string DataFolder` bound to folder TextBox
+ - `BrowseFolderCommand` opens FolderBrowserDialog
+ - On language change: updates `TranslationSource.Instance.CurrentCulture` + calls `SettingsService.SetLanguageAsync`
+ - `RunOperationAsync`: not applicable — stub throws `NotSupportedException` (Settings tab has no long-running operation)
+
+ **MainWindow.xaml** — Full shell layout as locked in CONTEXT.md:
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ **MainWindow.xaml.cs**: Constructor receives `MainWindowViewModel` via DI constructor injection. Sets `DataContext = viewModel`. Calls `viewModel.LoadProfilesAsync()` in `Loaded` event.
+
+ **App.xaml.cs** — Update RegisterServices:
+ ```csharp
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddSingleton();
+ ```
+
+ Wire LogPanelSink AFTER MainWindow is resolved (it needs the RichTextBox reference):
+ ```csharp
+ host.Start();
+ App app = new();
+ app.InitializeComponent();
+ var mainWindow = host.Services.GetRequiredService();
+
+ // Wire LogPanelSink now that we have the RichTextBox
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.File(/* rolling file path */)
+ .WriteTo.Sink(new LogPanelSink(mainWindow.LogPanel))
+ .CreateLogger();
+
+ app.MainWindow = mainWindow;
+ app.MainWindow.Visibility = Visibility.Visible;
+ ```
+
+ **Global exception handlers** in App.xaml.cs (after app created):
+ ```csharp
+ app.DispatcherUnhandledException += (s, e) =>
+ {
+ Log.Fatal(e.Exception, "Unhandled UI exception");
+ MessageBox.Show(
+ $"A fatal error occurred:\n{e.Exception.Message}\n\nCheck log for details.",
+ "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ e.Handled = true;
+ };
+ TaskScheduler.UnobservedTaskException += (s, e) =>
+ {
+ Log.Fatal(e.Exception, "Unobserved task exception");
+ e.SetObserved();
+ };
+ ```
+
+ Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` — fix any XAML or CS compilation errors.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5
+
+ Build succeeds with 0 errors. MainWindow.xaml contains RichTextBox x:Name="LogPanel". All 8 tab headers use TranslationSource bindings. Global exception handlers registered in App.xaml.cs.
+
+
+
+
+
+- `dotnet build SharepointToolbox.sln` passes with 0 errors
+- `dotnet test --filter "Category=Unit"` all pass
+- MainWindow.xaml contains `x:Name="LogPanel"` RichTextBox
+- App.xaml.cs registers `DispatcherUnhandledException` and `TaskScheduler.UnobservedTaskException`
+- FeatureViewModelBase contains no `async void` methods (anti-pattern violation)
+- ObservableCollection is never modified from Task.Run (pattern 7 compliance)
+
+
+
+Application compiles and launches to a visible WPF shell. FeatureViewModelBase tests green. All ViewModels registered in DI. Log panel wired to Serilog.
+
+
+
diff --git a/.planning/phases/01-foundation/01-07-PLAN.md b/.planning/phases/01-foundation/01-07-PLAN.md
new file mode 100644
index 0000000..45a2dbb
--- /dev/null
+++ b/.planning/phases/01-foundation/01-07-PLAN.md
@@ -0,0 +1,271 @@
+---
+phase: 01-foundation
+plan: 07
+type: execute
+wave: 5
+depends_on:
+ - 01-06
+files_modified:
+ - SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml
+ - SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml.cs
+ - SharepointToolbox/Views/Tabs/SettingsView.xaml
+ - SharepointToolbox/Views/Tabs/SettingsView.xaml.cs
+ - SharepointToolbox/Views/MainWindow.xaml
+autonomous: true
+requirements:
+ - FOUND-02
+ - FOUND-09
+ - FOUND-12
+must_haves:
+ truths:
+ - "ProfileManagementDialog opens as a modal window from the Manage Profiles button"
+ - "User can add a new profile (Name + Tenant URL + Client ID fields) and it appears in the toolbar ComboBox"
+ - "User can rename and delete existing profiles in the dialog"
+ - "SettingsView has a language ComboBox (English / French) and a data folder TextBox with Browse button"
+ - "Changing language in SettingsView switches the UI language immediately without restart"
+ - "Data folder setting persists to Sharepoint_Settings.json"
+ artifacts:
+ - path: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
+ provides: "Modal dialog for profile CRUD"
+ contains: "ProfileManagementViewModel"
+ - path: "SharepointToolbox/Views/Tabs/SettingsView.xaml"
+ provides: "Settings tab content with language and folder controls"
+ contains: "TranslationSource"
+ key_links:
+ - from: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
+ to: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
+ via: "DataContext = viewModel (constructor injected)"
+ pattern: "DataContext"
+ - from: "SharepointToolbox/Views/Tabs/SettingsView.xaml"
+ to: "SharepointToolbox/Localization/TranslationSource.cs"
+ via: "Language ComboBox selection sets TranslationSource.Instance.CurrentCulture"
+ pattern: "TranslationSource"
+ - from: "SharepointToolbox/Views/MainWindow.xaml"
+ to: "SharepointToolbox/Views/Tabs/SettingsView.xaml"
+ via: "Settings TabItem ContentTemplate or direct UserControl reference"
+ pattern: "SettingsView"
+---
+
+
+Build the two user-facing views completing Phase 1 UX: ProfileManagementDialog (profile CRUD modal) and SettingsView (language + data folder). Wire SettingsView into the MainWindow Settings tab.
+
+Purpose: These are the last two user-visible pieces before the visual checkpoint. After this plan the application is functional enough for a human to create a tenant profile, connect, and switch language.
+Output: ProfileManagementDialog + SettingsView wired into the shell.
+
+
+
+@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/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-06-SUMMARY.md
+
+
+
+```csharp
+public class ProfileManagementViewModel : ObservableObject
+{
+ public ObservableCollection Profiles { get; }
+ public TenantProfile? SelectedProfile { get; set; }
+ public string NewName { get; set; }
+ public string NewTenantUrl { get; set; }
+ public string NewClientId { get; set; }
+ public IAsyncRelayCommand AddCommand { get; }
+ public IAsyncRelayCommand RenameCommand { get; }
+ public IAsyncRelayCommand DeleteCommand { get; }
+}
+```
+
+
+```csharp
+public class SettingsViewModel : FeatureViewModelBase
+{
+ public string SelectedLanguage { get; set; } // "en" or "fr"
+ public string DataFolder { get; set; }
+ public RelayCommand BrowseFolderCommand { get; }
+}
+```
+
+
+// ProfileManagementDialog: modal Window, fields: Name + Tenant URL + Client ID
+// Profile fields: { name, tenantUrl, clientId } — JSON schema
+// SettingsView: language ComboBox (English/French) + DataFolder TextBox + Browse button
+// Language switch: immediate, no restart, via TranslationSource.Instance.CurrentCulture
+
+
+
+
+
+
+ Task 1: ProfileManagementDialog XAML and code-behind
+
+ SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml,
+ SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml.cs
+
+
+ Create `Views/Dialogs/` directory.
+
+ **ProfileManagementDialog.xaml** — modal Window (not UserControl):
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ **ProfileManagementDialog.xaml.cs**:
+ - Constructor receives `ProfileManagementViewModel` via DI (register as `Transient` in App.xaml.cs — already done in plan 01-06)
+ - Sets `DataContext = viewModel`
+ - `CloseButton_Click`: calls `this.Close()`
+ - `Owner` set by caller (`MainWindowViewModel.ManageProfilesCommand` opens as `new ProfileManagementDialog { Owner = Application.Current.MainWindow }.ShowDialog()`)
+
+ After adding: the Add command in `ProfileManagementViewModel` must also trigger `MainWindowViewModel.TenantProfiles` refresh. Implement by having `ProfileManagementViewModel` accept a callback or raise an event. The simplest approach: `MainWindowViewModel.ManageProfilesCommand` reloads profiles after the dialog closes (dialog is modal — `ShowDialog()` blocks until closed).
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5
+
+ Build succeeds. ProfileManagementDialog.xaml contains all three input fields (Name, Tenant URL, Client ID). All labels use TranslationSource bindings.
+
+
+
+ Task 2: SettingsView XAML and MainWindow Settings tab wiring
+
+ SharepointToolbox/Views/Tabs/SettingsView.xaml,
+ SharepointToolbox/Views/Tabs/SettingsView.xaml.cs,
+ SharepointToolbox/Views/MainWindow.xaml
+
+
+ Create `Views/Tabs/` directory.
+
+ **SettingsView.xaml** — UserControl (embedded in TabItem):
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ **SettingsView.xaml.cs**: Constructor receives `SettingsViewModel` via DI. Sets `DataContext = viewModel`. Calls `viewModel.LoadAsync()` in `Loaded` event to populate current settings.
+
+ Add `LoadAsync()` to SettingsViewModel if not present — loads current settings from SettingsService and sets `SelectedLanguage` and `DataFolder` properties.
+
+ **MainWindow.xaml** — Update Settings TabItem to use SettingsView (replace placeholder TextBlock):
+ ```xml
+
+
+
+ ```
+ Add namespace: `xmlns:views="clr-namespace:SharepointToolbox.Views.Tabs"`
+
+ Also register `SettingsView` in DI in App.xaml.cs (if not already):
+ ```csharp
+ services.AddTransient();
+ ```
+ And resolve it in MainWindow constructor to inject into the Settings TabItem Content, OR use a DataTemplate approach. The simpler approach for Phase 1: resolve `SettingsView` from DI in `MainWindow.xaml.cs` constructor and set it as the TabItem Content directly:
+ ```csharp
+ SettingsTabItem.Content = serviceProvider.GetRequiredService();
+ ```
+ Add `x:Name="SettingsTabItem"` to the Settings TabItem in XAML.
+
+ Run `dotnet build` and fix any errors.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5
+
+ Build succeeds. SettingsView.xaml contains language ComboBox with "en"/"fr" options and data folder TextBox with Browse button. MainWindow.xaml Settings tab shows SettingsView (not placeholder TextBlock).
+
+
+
+
+
+- `dotnet build SharepointToolbox.sln` passes with 0 errors
+- `dotnet test --filter "Category=Unit"` still passes (no regressions)
+- ProfileManagementDialog has all three input fields using TranslationSource keys
+- SettingsView language ComboBox has Tag="en" and Tag="fr" items
+- MainWindow Settings TabItem Content is SettingsView (not placeholder)
+
+
+
+All Phase 1 UI is built. Application runs and shows: shell with 8 tabs, log panel, status bar, language switching, profile management dialog, and settings. Ready for the visual checkpoint in plan 01-08.
+
+
+
diff --git a/.planning/phases/01-foundation/01-08-PLAN.md b/.planning/phases/01-foundation/01-08-PLAN.md
new file mode 100644
index 0000000..1b48c26
--- /dev/null
+++ b/.planning/phases/01-foundation/01-08-PLAN.md
@@ -0,0 +1,161 @@
+---
+phase: 01-foundation
+plan: 08
+type: execute
+wave: 6
+depends_on:
+ - 01-07
+files_modified: []
+autonomous: false
+requirements:
+ - FOUND-01
+ - FOUND-02
+ - FOUND-03
+ - FOUND-04
+ - FOUND-05
+ - FOUND-06
+ - FOUND-07
+ - FOUND-08
+ - FOUND-09
+ - FOUND-10
+ - FOUND-12
+must_haves:
+ truths:
+ - "Application launches without crashing from dotnet run"
+ - "All 8 tabs visible with correct localized headers"
+ - "Language switch from Settings tab changes tab headers immediately without restart"
+ - "Profile management dialog opens, allows adding/renaming/deleting profiles"
+ - "Log panel at bottom shows timestamped messages with color coding"
+ - "Status bar shows tenant name and connection status"
+ - "All unit tests pass (zero failures)"
+ artifacts:
+ - path: "SharepointToolbox/App.xaml.cs"
+ provides: "Running application entry point"
+ - path: "SharepointToolbox/Views/MainWindow.xaml"
+ provides: "Visible shell with all required regions"
+ key_links:
+ - from: "Visual inspection"
+ to: "Phase 1 success criteria (ROADMAP.md)"
+ via: "Manual verification checklist"
+ pattern: "checkpoint"
+---
+
+
+Run the full test suite, launch the application, and perform visual/functional verification of all Phase 1 success criteria before marking the phase complete.
+
+Purpose: Automated tests validate logic, but WPF UI can fail visually in ways tests cannot catch (layout wrong, bindings silently failing, log panel invisible, crash on startup).
+Output: Confirmed working foundation. Green light for Phase 2.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/01-foundation/01-CONTEXT.md
+@.planning/phases/01-foundation/01-07-SUMMARY.md
+
+
+
+
+
+ Task 1: Run full test suite and verify zero failures
+
+
+ Run the complete test suite:
+ ```
+ dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --no-build -v normal
+ ```
+
+ Expected result: All Unit and Integration tests pass. The following tests may remain as `Skip`:
+ - `SessionManagerTests.GetOrCreateContextAsync_CreatesContext` (requires interactive MSAL)
+
+ If any tests fail:
+ 1. Read the failure message carefully
+ 2. Fix the underlying code (do NOT delete or skip a failing test)
+ 3. Re-run until all non-interactive tests pass
+
+ Also run a build to confirm zero warnings (treat warnings as potential future failures):
+ ```
+ dotnet build SharepointToolbox.sln -warnaserror
+ ```
+ If warnings-as-errors produces failures from NuGet or generated code, switch back to `dotnet build SharepointToolbox.sln` and list remaining warnings in the SUMMARY.
+
+
+ cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj 2>&1 | tail -15
+
+ Test output shows 0 failed. All non-interactive tests pass. Build produces 0 errors.
+
+
+
+
+ Complete Phase 1 Foundation:
+ - WPF shell with 8-tab layout, log panel (150px, black background, green text), StatusBar
+ - Toolbar: tenant ComboBox (220px), Connect, Manage Profiles, separator, Clear Session
+ - Profile management dialog (modal) — add, rename, delete tenant profiles
+ - Settings tab: language switcher (EN/FR) + data folder picker
+ - Dynamic language switching — changes tab headers without restart
+ - Serilog rolling file log + LogPanelSink writing to in-app RichTextBox
+ - Global exception handlers wired
+ - All infrastructure patterns in place (pagination helper, retry helper, FeatureViewModelBase)
+
+
+ Run the application: `dotnet run --project SharepointToolbox/SharepointToolbox.csproj`
+
+ Check each item:
+
+ 1. **Shell layout**: Window shows toolbar at top, TabControl in center with 8 tabs (Permissions, Storage, File Search, Duplicates, Templates, Bulk Operations, Folder Structure, Settings), log panel at bottom (black, 150px), status bar below log.
+
+ 2. **Tab headers**: All 8 tabs have their English localized text (not "[tab.xxx]" — which would mean missing resx key).
+
+ 3. **Language switch**:
+ - Open Settings tab
+ - Change language to French
+ - Verify tab headers change immediately (no restart)
+ - Change back to English to reset
+
+ 4. **Profile management**:
+ - Click "Manage Profiles..."
+ - Modal dialog appears
+ - Add a test profile: Name="Test", URL="https://test.sharepoint.com", ClientId="test-id"
+ - Profile appears in the toolbar ComboBox after dialog closes
+ - Rename the profile in the dialog — new name shows in ComboBox
+ - Delete the profile — removed from ComboBox
+
+ 5. **Log panel**:
+ - Verify log entries appear (at least startup messages) in `HH:mm:ss [XXXX] message` format
+ - Verify green color for info entries
+
+ 6. **Data folder**:
+ - Open Settings tab
+ - Click Browse, select a folder
+ - Verify folder path appears in the TextBox
+
+ 7. **Error handler** (optional — skip if risky):
+ - Confirm `%AppData%\SharepointToolbox\logs\` directory exists and contains today's log file
+
+ Report any visual issues, missing strings, or crashes.
+
+ Type "approved" if all checks pass, or describe specific issues found
+
+
+
+
+
+All Phase 1 ROADMAP success criteria met:
+1. User can create, rename, delete, and switch between tenant profiles via the UI
+2. MSAL token cache infrastructure ready (interactive login requires a real Azure AD tenant — not testable in this checkpoint)
+3. Per-tab progress bar + cancel button infrastructure built (no long-running ops in Phase 1 to demo, but FeatureViewModelBase tests prove the pattern)
+4. Log panel surfaces errors in red; global exception handlers registered
+5. Language switches between EN and FR dynamically without restart
+
+
+
+Human approves visual checkpoint. All unit tests green. Phase 1 complete — ready to begin Phase 2 (Permissions).
+
+
+