diff --git a/.planning/1-CONTEXT.md b/.planning/1-CONTEXT.md deleted file mode 100644 index 36fd572..0000000 --- a/.planning/1-CONTEXT.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -phase: 1 -title: Foundation -status: ready-for-planning -created: 2026-04-02 ---- - -# Phase 1 Context: Foundation - -## Decided Areas (from prior research + STATE.md) - -These are locked — do not re-litigate during planning or execution. - -| Decision | Value | -|---|---| -| Runtime | .NET 10 LTS + WPF | -| MVVM framework | CommunityToolkit.Mvvm 8.4.2 | -| SharePoint library | PnP.Framework 1.18.0 | -| Auth | MSAL.NET 4.83.1 + Extensions.Msal 4.83.3 + Desktop 4.82.1 | -| Token cache | MsalCacheHelper — one `IPublicClientApplication` per ClientId | -| DI host | Microsoft.Extensions.Hosting 10.x | -| Logging | Serilog 4.3.1 + rolling file sink → `%AppData%\SharepointToolbox\logs\` | -| JSON | System.Text.Json (built-in) | -| JSON persistence | Write-then-replace (`file.tmp` → validate → `File.Move`) + `SemaphoreSlim(1)` per file | -| Async pattern | `AsyncRelayCommand` everywhere — zero `async void` handlers | -| Trimming | `PublishTrimmed=false` — accept ~150–200 MB EXE | -| Architecture | 4-layer MVVM: View → ViewModel → Service → Infrastructure | -| Cross-VM messaging | `WeakReferenceMessenger` for tenant-switched events | -| Session holder | Singleton `SessionManager` — only class that holds `ClientContext` objects | -| Localization | .resx resource files (EN default, FR overlay) | - -## Gray Areas — Defaults Applied (user skipped discussion) - -### 1. Shell Layout - -**Default:** Mirror the existing tool's spatial contract — users are already trained on it. - -- **Window structure:** `MainWindow` with a top `ToolBar`, a center `TabControl` (feature tabs), and a bottom docked log panel. -- **Log panel:** Always visible, 150 px tall, not collapsible in Phase 1 (collapsibility is cosmetic — defer to a later phase). Uses a `RichTextBox`-equivalent (`RichTextBox` XAML control) with color-coded entries. -- **Tab strip:** `TabControl` with one `TabItem` per feature area. Phase 1 delivers a shell with placeholder tabs for all features so navigation is wired from day one. -- **Tabs to stub out:** Permissions, Storage, File Search, Duplicates, Templates, Bulk Operations, Folder Structure, Settings — all stubbed with a `"Coming soon"` placeholder `TextBlock` except Settings (partially functional in Phase 1 for profile management and language switching). -- **Status bar:** `StatusBar` at the very bottom (below the log panel) showing: current tenant display name | operation status text | progress percentage. - -### 2. Tenant Selector Placement - -**Default:** Prominent top-toolbar presence — tenant context is the most critical runtime state. - -- **Toolbar layout (left to right):** `ComboBox` (tenant display name list, ~220 px wide) → `Button "Connect"` → `Button "Manage Profiles..."` → separator → `Button "Clear Session"`. -- **ComboBox:** Bound to `MainWindowViewModel.TenantProfiles` ObservableCollection. Selecting a different item triggers a tenant-switch command (WeakReferenceMessenger broadcast to reset all feature VMs). -- **"Manage Profiles..." button:** Opens a modal `ProfileManagementDialog` (separate Window) for CRUD — create, rename, delete profiles. Inline editing in the toolbar would be too cramped. -- **"Clear Session" button:** Clears the MSAL token cache for the currently selected tenant and resets connection state. Lives in the toolbar (not buried in settings) because MSP users need quick access when switching client accounts mid-session. -- **Profile fields:** Name (display label), Tenant URL, Client ID — matches existing `{ name, tenantUrl, clientId }` JSON schema exactly. - -### 3. Progress + Cancel UX - -**Default:** Per-tab pattern — each feature tab owns its progress state. No global progress bar. - -- **Per-tab layout (bottom of each tab's content area):** `ProgressBar` (indeterminate or 0–100) + `TextBlock` (operation description, e.g. "Scanning site 3 of 12…") + `Button "Cancel"` — shown only when an operation is running (`Visibility` bound to `IsRunning`). -- **`CancellationTokenSource`:** Owned by each ViewModel, recreated per operation. Cancel button calls `_cts.Cancel()`. -- **`IProgress`:** `OperationProgress` is a shared record `{ int Current, int Total, string Message }` — defined in the `Core/` layer and used by all feature services. Concrete implementation uses `Progress` which marshals to the UI thread automatically. -- **Log panel as secondary channel:** Every progress step that produces a meaningful event also writes a timestamped line to the log panel. The per-tab progress bar is the live indicator; the log is the audit trail. -- **Status bar:** `StatusBar` at the bottom updates its operation text from the active tab's progress events via WeakReferenceMessenger — so the user sees progress even if they switch away from the running tab. - -### 4. Error Surface UX - -**Default:** Log panel as primary surface; modal dialog only for blocking errors. - -- **Non-fatal errors** (an operation failed, a SharePoint call returned an error): Written to log panel in red. The per-tab status area shows a brief summary (e.g. "Completed with 2 errors — see log"). No modal. -- **Fatal/blocking errors** (auth failure, unhandled exception): `MessageBox.Show` modal with the error message and a "Copy to Clipboard" button for diagnostics. Keep it simple — no custom dialog in Phase 1. -- **No toasts in Phase 1:** Toast/notification infrastructure is a cosmetic feature — defer. The log panel is always visible and sufficient. -- **Log entry format:** `HH:mm:ss [LEVEL] Message` — color coded: green = info/success, orange = warning, red = error. `LEVEL` maps to Serilog severity. -- **Global exception handler:** `Application.DispatcherUnhandledException` and `TaskScheduler.UnobservedTaskException` both funnel to the log panel + a fatal modal. Neither swallows the exception. -- **Empty catch block policy:** Any `catch` block must do exactly one of: log-and-recover, log-and-rethrow, or log-and-surface. Empty catch = build defect. Enforce via code review on every PR in Phase 1. - -## JSON Compatibility - -Existing file names and schema must be preserved exactly — users have live data in these files. - -| File | Schema | -|---|---| -| `Sharepoint_Export_profiles.json` | `{ "profiles": [{ "name": "...", "tenantUrl": "...", "clientId": "..." }] }` | -| `Sharepoint_Settings.json` | `{ "dataFolder": "...", "lang": "en" }` | - -The C# `SettingsService` must read these files without migration — the field names are the contract. - -## Localization - -- **EN strings are the default `.resx`** — `Strings.resx` (neutral/EN). FR is `Strings.fr.resx`. -- **Key naming:** Mirror existing PowerShell key convention (`tab.perms`, `btn.run.scan`, `menu.language`, etc.) so the EN default content is easily auditable against the existing app. -- **Dynamic switching:** `CultureInfo.CurrentUICulture` swap + `WeakReferenceMessenger` broadcast triggers all bound `LocalizedString` markup extensions to re-evaluate. No app restart needed. -- **FR completeness:** FR strings will be stubbed with EN fallback in Phase 1 — FR completeness is a Phase 5 concern. - -## Infrastructure Patterns (Phase 1 Deliverables) - -These are shared helpers that all feature phases reuse. They must be built and tested in Phase 1 before any feature work begins. - -1. **`SharePointPaginationHelper`** — static helper that wraps `CamlQuery` with `RowLimit ≤ 2,000` and `ListItemCollectionPosition` looping. All list enumeration in the codebase must call this — never raw `ExecuteQuery` on a list. -2. **`AsyncRelayCommand` pattern** — a thin base or example `FeatureViewModel` that demonstrates the canonical async command pattern: create `CancellationTokenSource`, bind `IsRunning`, bind `IProgress`, handle `OperationCanceledException` gracefully. -3. **`ObservableCollection` threading rule** — results are accumulated in `List` on a background thread, then assigned as `new ObservableCollection(list)` via `Dispatcher.InvokeAsync`. Never modify an `ObservableCollection` from `Task.Run`. -4. **`ExecuteQueryRetryAsync` wrapper** — wraps PnP Framework's retry logic. All CSOM calls use this; surface retry events as log + progress messages ("Throttled — retrying in 30s…"). -5. **`ClientContext` disposal** — always `await using`. Unit tests verify `Dispose()` is called on cancellation. - -## Deferred Ideas (out of scope for Phase 1) - -- Log panel collapsibility (cosmetic, Phase 3+) -- Dark/light theme toggle (cosmetic, post-v1) -- Toast/notification system (Phase 3+) -- FR locale completeness (Phase 5) -- User access export, storage charts, simplified permissions view (v1.x features, Phase 5) - -## code_context - -| Asset | Path | Notes | -|---|---|---| -| Existing profile JSON schema | `Sharepoint_ToolBox.ps1:68–72` | `Save-Profiles` shows exact field names | -| Existing settings JSON schema | `Sharepoint_ToolBox.ps1:147–152` | `Save-Settings` shows `dataFolder` + `lang` | -| Existing localization keys (EN) | `Sharepoint_ToolBox.ps1:2795–2870` (approx) | Full EN key set for `.resx` migration | -| Existing tab names | `Sharepoint_ToolBox.ps1:3824` | 9 tabs: Perms, Storage, Templates, Search, Dupes, Transfer, Bulk, Struct, Versions | -| Log panel pattern | `Sharepoint_ToolBox.ps1:6–17` | Color + timestamp format to mirror | diff --git a/.planning/10-CONTEXT.md b/.planning/10-CONTEXT.md deleted file mode 100644 index 14cdf55..0000000 --- a/.planning/10-CONTEXT.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -phase: 10 -title: Branding Data Foundation -status: ready-for-planning -created: 2026-04-08 ---- - -# Phase 10 Context: Branding Data Foundation - -## Decided Areas (from prior research + STATE.md) - -These are locked — do not re-litigate during planning or execution. - -| Decision | Value | -|---|---| -| Logo storage format | Base64 strings in JSON (not file paths) | -| MSP logo location | `BrandingSettings.cs` model → `branding.json` | -| Client logo location | On `TenantProfile` model (per-tenant) | -| File path after import | Discarded — only base64 persists | -| SVG support | Rejected (XSS risk) — PNG/JPG only | -| User directory service | New `GraphUserDirectoryService`, separate from `GraphUserSearchService` | -| Directory auto-load | No — explicit "Load Directory" button required | -| New NuGet packages | None — existing stack covers everything | -| Export service signature | Optional `ReportBranding? branding = null` parameter on existing export methods | - -## Discussed Areas - -### 1. Logo Metadata Model - -**Decision:** Store base64 + MIME type as separate fields in a shared `LogoData` record. - -- Shared model: `LogoData { string Base64, string MimeType }` — used by both MSP logo (in `BrandingSettings`) and client logo (on `TenantProfile`) -- `MimeType` is `"image/png"` or `"image/jpeg"`, determined at import time from magic bytes -- No other metadata stored — no original filename, dimensions, or import date -- Export services concatenate `data:{MimeType};base64,{Base64}` for HTML `` tags -- WPF preview converts `Base64` bytes to `BitmapImage` directly - -### 2. Logo Validation & Compression - -**Decision:** Validate format via magic bytes, auto-compress oversized files silently. - -- **Format detection:** Read file header magic bytes only — ignore file extension entirely - - PNG signature: `89 50 4E 47` (first 4 bytes) - - JPEG signature: `FF D8 FF` (first 3 bytes) - - Anything else → reject with specific error message (e.g., "File format is BMP, only PNG and JPG are accepted") -- **Size handling:** If file exceeds 512 KB, auto-compress silently (no user notification) - - Strategy: resize dimensions (e.g., max 300px width/height) + reduce quality - - Keep original format — PNG stays PNG, JPEG stays JPEG (no format conversion) - - Compress until under 512 KB -- **Dimension limits:** None — the 512 KB cap and compression handle naturally -- **Validation errors:** Specific messages for format rejection (format-related only, since size is auto-handled) - -### 3. Profile Deletion & Duplication Behavior - -**Decision:** Warn about logo loss on deletion; do NOT copy logo on duplication. - -- **Deletion:** When deleting a tenant profile that has a client logo, the confirmation message explicitly mentions it: "This will also remove its client logo." The logo is embedded in the profile JSON, so deletion is automatic — no orphaned files. -- **Duplication:** Duplicating a tenant profile copies connection fields (Name, TenantUrl, ClientId) but starts with a blank client logo. The user must re-import or auto-pull for the new profile. Rationale: duplicated profiles are typically for different tenants, so the logo shouldn't carry over. - -## Deferred Ideas (out of scope for Phase 10) - -- Logo preview in Settings UI (Phase 12) -- Auto-pull client logo from Entra branding API (Phase 11/12) -- Report header layout with logos side-by-side (Phase 11) -- "Load Directory" button placement decision (Phase 14) -- Session-scoped directory cache (UDIR-F01, deferred) - -## code_context - -| Asset | Path | Reuse | -|---|---|---| -| TenantProfile model | `SharepointToolbox/Core/Models/TenantProfile.cs` | Extend with `LogoData? ClientLogo` property | -| AppSettings model | `SharepointToolbox/Core/Models/AppSettings.cs` | Reference for BrandingSettings pattern | -| SettingsRepository | `SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs` | Clone pattern for BrandingRepository (write-then-replace + SemaphoreSlim) | -| ProfileRepository | `SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs` | Already handles TenantProfile persistence — will serialize new logo field | -| GraphUserSearchService | `SharepointToolbox/Services/GraphUserSearchService.cs` | Reference for Graph SDK usage, auth, and query patterns | -| GraphClientFactory | `SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs` | Provides `GraphServiceClient` for new directory service | -| DI registration | `SharepointToolbox/App.xaml.cs` (lines 73-163) | Register new BrandingRepository, BrandingService, GraphUserDirectoryService | -| Profile deletion UI | `SharepointToolbox/ViewModels/ProfileManagementViewModel.cs` | Update deletion confirmation message to mention logo | diff --git a/.planning/MILESTONE-AUDIT.md b/.planning/MILESTONE-AUDIT.md deleted file mode 100644 index 62ad64c..0000000 --- a/.planning/MILESTONE-AUDIT.md +++ /dev/null @@ -1,155 +0,0 @@ -# Milestone Audit: SharePoint Toolbox v2 — v1 Release - -**Audited:** 2026-04-07 -**Milestone:** v1 (5 phases, 42 requirements) -**Verdict:** PASSED — all requirements satisfied, all phases integrated, build and tests green - ---- - -## Phase Verification Summary - -| Phase | Status | Score | Verification | -|-------|--------|-------|-------------| -| 01 — Foundation | PASSED | 11/11 | 01-VERIFICATION.md | -| 02 — Permissions | HUMAN_NEEDED | 7/7 automated | 02-VERIFICATION.md (2 human items pending) | -| 03 — Storage & File Ops | **MISSING** | No VERIFICATION.md | Summaries exist for all 8 plans; integration checker confirmed all wiring | -| 04 — Bulk Ops & Provisioning | HUMAN_NEEDED | 12/12 automated | 04-VERIFICATION.md (7 human items pending) | -| 05 — Distribution & Hardening | HUMAN_NEEDED | 6/6 automated | 05-VERIFICATION.md (2 human items pending) | - -### Gap: Phase 03 Missing Verification - -Phase 03 has no `03-VERIFICATION.md` file. All 8 plan summaries exist and confirm code was delivered. The integration checker independently verified: -- All 3 Phase 3 service interfaces (IStorageService, ISearchService, IDuplicatesService) registered in DI -- All 5 export services registered and wired to ViewModels -- All 4 Phase 3 tabs (Storage, Search, Duplicates + exports) wired in MainWindow -- 13 Phase 3 requirements (STOR-01–05, SRCH-01–04, DUPL-01–03) covered - -**Recommendation:** Run a retroactive phase verification for Phase 03 or accept integration checker evidence as sufficient. - ---- - -## Requirements Coverage - -All 42 v1 requirements are marked complete in REQUIREMENTS.md with phase traceability: - -| Category | IDs | Count | Status | -|----------|-----|-------|--------| -| Foundation | FOUND-01 to FOUND-12 | 12 | All SATISFIED | -| Permissions | PERM-01 to PERM-07 | 7 | All SATISFIED | -| Storage | STOR-01 to STOR-05 | 5 | All SATISFIED | -| File Search | SRCH-01 to SRCH-04 | 4 | All SATISFIED | -| Duplicates | DUPL-01 to DUPL-03 | 3 | All SATISFIED | -| Templates | TMPL-01 to TMPL-04 | 4 | All SATISFIED | -| Folder Structure | FOLD-01 to FOLD-02 | 2 | All SATISFIED | -| Bulk Operations | BULK-01 to BULK-05 | 5 | All SATISFIED | -| **Total** | | **42** | **42/42 mapped and complete** | - -**Orphaned requirements:** None -**Unmapped requirements:** None - ---- - -## Cross-Phase Integration - -Integration checker ran full verification. Results: - -| Check | Status | -|-------|--------| -| DI wiring (all 5 phases) | PASS — all services registered in App.xaml.cs | -| MainWindow tabs (10 tabs) | PASS — all declared and wired from DI | -| FeatureViewModelBase inheritance (10 VMs) | PASS | -| SessionManager usage (9 ViewModels + SiteListService) | PASS | -| ExecuteQueryRetryHelper (9 CSOM services, 40+ call sites) | PASS | -| SharePointPaginationHelper (2 services using list enumeration) | PASS | -| TranslationSource localization (15 XAML files, 170 bindings) | PASS | -| TenantSwitchedMessage propagation | PASS | -| Export chain completeness (all features) | PASS | -| Build | PASS — 0 warnings, 0 errors | -| Tests | PASS — 134 passed, 22 skipped (live CSOM), 0 failed | -| EN/FR key parity | PASS — 199/199 keys | - -**Orphaned code:** `FeatureTabBase.xaml` — Phase 1 placeholder, now superseded by full tab views. Harmless dead code. - ---- - -## Tech Debt & Deferred Items - -### From Phase Verifications - -| Item | Source | Severity | Description | -|------|--------|----------|-------------| -| Hardcoded export button text | Phase 2 | Info | `PermissionsView.xaml` uses `Content="Export CSV"` / `"Export HTML"` instead of `rad.csv.perms` / `rad.html.perms` localization keys. French users see English button labels. | -| Missing Designer.cs property | Phase 2 | Info | `Strings.Designer.cs` lacks `tab_permissions` typed accessor. Runtime binding via `TranslationSource` works fine. | -| No invalid-row highlighting | Phase 4 | Warning | `BulkMembersView.xaml`, `BulkSitesView.xaml`, `FolderStructureView.xaml` show IsValid as text column but lack `RowStyle` + `DataTrigger` for visual red highlighting on invalid rows. | -| FeatureTabBase dead code | Phase 1→all | Info | `Views/Controls/FeatureTabBase.xaml` is no longer imported by any tab view after all phases replaced stubs. | -| Cancel test locale mismatch | Phase 3 (03-08) | Info | `FeatureViewModelBaseTests.CancelCommand_DuringOperation_SetsStatusMessageToCancelled` asserts `.Contains("cancel")` but app returns French string "Opération annulée". Pre-existing; deferred. | - -### Deferred v2 Requirements - -These are explicitly out of scope for v1 and tracked in REQUIREMENTS.md: -- UACC-01/02: User access audit across sites -- SIMP-01/02/03: Simplified plain-language permission reports -- VIZZ-01/02/03: Storage metrics graphs (pie/bar chart) - ---- - -## Human Verification Backlog - -11 items across 3 phases require human confirmation (runtime UI/locale checks that cannot be automated): - -### Phase 2 (2 items) -1. Full Permissions tab UI visual checkpoint (layout, disabled states, French locale) -2. Export button localization decision (accept hardcoded English or bind to resx keys) - -### Phase 4 (7 items) -1. Application launches with all 10 tabs visible -2. Bulk Members — Load Example populates DataGrid with 7 rows -3. Bulk Sites — semicolon CSV auto-detection works -4. Invalid row display in DataGrid (IsValid=False, Errors column) -5. Confirmation dialog appears before bulk operations -6. Transfer tab — two-step browse flow (SitePickerDialog → FolderBrowserDialog) -7. Templates tab — 5 capture checkboxes visible and checked by default - -### Phase 5 (2 items) -1. Clean-machine EXE launch (no .NET runtime installed) -2. French locale runtime rendering (diacritics display correctly in all tabs) - ---- - -## Build & Test Summary - -| Metric | Value | -|--------|-------| -| Build | 0 errors, 0 warnings | -| Tests passed | 134 | -| Tests skipped | 22 (live CSOM — expected) | -| Tests failed | 0 | -| EN locale keys | 199 | -| FR locale keys | 199 | -| Published EXE | 200.9 MB self-contained | -| Phases complete | 5/5 | -| Requirements satisfied | 42/42 | - ---- - -## Verdict - -**PASSED** — The milestone has achieved its definition of done: - -1. All 42 v1 requirements are implemented with real code and verified by phase-level checks -2. All cross-phase integration points are wired (DI, messaging, shared infrastructure) -3. Build compiles cleanly with zero warnings -4. 134 automated tests pass with zero failures -5. Self-contained 200.9 MB EXE produced successfully -6. Full EN/FR locale parity (199 keys each) - -**Remaining actions before shipping:** -- [ ] Complete 11 human verification items (UI visual checks, clean-machine launch) -- [ ] Decide on Phase 03 retroactive verification (or accept integration check as sufficient) -- [ ] Address 3 Warning-level tech debt items (invalid-row highlighting in bulk DataGrids) -- [ ] Optionally clean up FeatureTabBase dead code and fix cancel test locale mismatch - ---- - -*Audited: 2026-04-07* -*Auditor: Claude (milestone audit)* diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md deleted file mode 100644 index e039d0e..0000000 --- a/.planning/MILESTONES.md +++ /dev/null @@ -1,23 +0,0 @@ -# Milestones - -## v1.0 MVP (Shipped: 2026-04-07) - -**Phases completed:** 5 phases, 36 plans | 164 commits | 10,071 LOC (C# + XAML) -**Timeline:** 28 days (2026-03-10 → 2026-04-07) -**Tests:** 134 pass, 22 skip (live CSOM), 0 fail - -**Key accomplishments:** -- Full C#/WPF rewrite of 6,400-line PowerShell tool into 10,071-line MVVM application -- Multi-tenant MSAL authentication with per-tenant token caching and instant switching -- SharePoint permissions scanner with multi-site support, CSV/HTML export, 5,000-item pagination -- Storage metrics, file search, and duplicate detection with configurable depth and exports -- Bulk operations (members, sites, file transfer) with per-item error reporting, retry, and cancellation -- Self-contained 200 MB EXE with full EN/FR localization (199 keys each) - -**Archives:** -- [v1.0-ROADMAP.md](milestones/v1.0-ROADMAP.md) -- [v1.0-REQUIREMENTS.md](milestones/v1.0-REQUIREMENTS.md) -- [v1.0-MILESTONE-AUDIT.md](milestones/v1.0-MILESTONE-AUDIT.md) - ---- - diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md deleted file mode 100644 index 36033c8..0000000 --- a/.planning/PROJECT.md +++ /dev/null @@ -1,123 +0,0 @@ -# SharePoint Toolbox v2 - -## What This Is - -A C#/WPF desktop application for IT administrators and MSPs to audit and manage SharePoint Online permissions, storage, files, and sites across multiple client tenants. Replaces a 6,400-line monolithic PowerShell script with a structured 10,071-line MVVM application shipping as a single self-contained EXE. - -## Core Value - -Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. - -## Current State - -**Shipped:** v2.2 Report Branding & User Directory (2026-04-09) -**Status:** Active — v2.3 Tenant Management & Report Enhancements - -## Current Milestone: v2.3 Tenant Management & Report Enhancements - -**Goal:** Streamline tenant onboarding with automated app registration, add self-healing ownership for access-denied sites, and enhance report output with group expansion and entry consolidation. - -**Target features:** -- App registration on target tenant (auto via Graph API + guided fallback) during profile create/edit -- App removal from target tenant -- Auto-take ownership of SharePoint sites on access denied (global toggle) -- Expand groups in HTML reports (clickable to show members) -- Report consolidation toggle (merge duplicate user entries across locations) - -
-v2.2 shipped features - -- HTML report branding with MSP logo (global) and client logo (per tenant) -- Auto-pull client logo from Entra branding API -- Logo validation (PNG/JPG, 512 KB limit) with auto-compression -- User directory browse mode in user access audit tab with paginated load -- Member/guest filter and department/job title columns -- Directory user selection triggers existing audit pipeline -
- -
-v1.1 shipped features - -- Global multi-site selection in toolbar (pick sites once, all tabs use them) -- User access audit tab with Graph API people-picker, direct/group/inherited access distinction -- Simplified permissions with plain-language labels, color-coded risk levels, detail-level toggle -- Storage visualization with LiveCharts2 pie/donut and bar charts by file type -
- -Tech stack: C# / WPF / .NET 10 / PnP Framework / Microsoft Graph SDK / MSAL / Serilog / CommunityToolkit.Mvvm / LiveCharts2 -Tests: 285 automated (xUnit), 26 skipped (require live SharePoint tenant) -Distribution: 200 MB self-contained EXE (win-x64) -LOC: ~16,900 C# - -## Requirements - -### Validated - -- Full C#/WPF rewrite of all existing PowerShell features — v1.0 -- Multi-tenant authentication with cached sessions — v1.0 -- Thorough error handling (per-item reporting, no silent failures) — v1.0 -- Modular architecture (separate files per feature area, DI, MVVM) — v1.0 -- Self-contained single EXE distribution — v1.0 - -### Shipped in v1.1 - -- [x] Global multi-site selection in toolbar (SITE-01/02) — v1.1 -- [x] Export all SharePoint/Teams accesses a specific user has across selected sites (UACC-01/02) — v1.1 -- [x] Simplified permissions reports (plain language, summary views) (SIMP-01/02/03) — v1.1 -- [x] Storage metrics graph by file type (pie/donut and bar chart, toggleable) (VIZZ-01/02/03) — v1.1 - -### Shipped in v2.2 - -- [x] HTML report branding with MSP and client logos (BRAND-01/02/03/04/05/06) — v2.2 -- [x] User directory browse mode in user access audit tab (UDIR-01/02/03/04/05) — v2.2 - -### Active in v2.3 - -- [ ] Automated app registration on target tenant with guided fallback -- [ ] App removal from target tenant -- [ ] Auto-take ownership of sites on access denied (global toggle) -- [ ] Expand groups in HTML reports -- [ ] Report consolidation toggle (merge duplicate entries) - -### Out of Scope - -- Cross-platform support (Mac/Linux) — WPF is Windows-only; not justified for current user base -- SQLite or database storage — JSON sufficient for config, profiles, and templates -- Web-based UI — must remain a local desktop application -- Cloud/SaaS deployment — local tool by design -- Mobile support — desktop admin tool -- Real-time monitoring / alerts — requires background service, beyond scope -- Automated remediation (auto-revoke) — liability risk -- Content migration between tenants — separate product category - -## Context - -- **v1.0 shipped** with full feature parity: permissions, storage, search, duplicates, bulk operations, templates, folder provisioning -- **v1.1 shipped** with enhanced reports: user access audit, simplified permissions, storage charts, global site selection -- **v2.2 shipped** with report branding (logos in HTML exports) and user directory browse mode -- **Localization:** 230+ EN/FR keys, full parity verified -- **Architecture:** 140+ C# files + 17 XAML files across Core/Infrastructure/Services/ViewModels/Views layers - -## Constraints - -- **Platform:** Windows desktop only — WPF requires Windows -- **Distribution:** Self-contained EXE (~200 MB) — no .NET runtime dependency -- **Auth method:** Interactive browser-based Azure AD login (no client secrets stored) -- **Data storage:** JSON files for profiles, settings, templates -- **SharePoint API:** PnP Framework / Microsoft Graph SDK -- **Local only:** No telemetry, no cloud services, no external dependencies at runtime - -## Key Decisions - -| Decision | Rationale | Outcome | -|----------|-----------|---------| -| Rewrite to C#/WPF instead of improving PowerShell | Better async/await, proper OOP, richer UI, better tooling | ✓ Good — 10k LOC structured app vs 6.4k monolithic script | -| WPF over WinForms | Modern data binding, MVVM pattern, richer styling | ✓ Good — clean separation of concerns | -| Self-contained EXE | Users shouldn't need to install .NET runtime | ✓ Good — 200 MB single file, zero dependencies | -| Keep JSON storage | Simple, human-readable, sufficient for config/profiles | ✓ Good — atomic write-then-replace pattern works well | -| Multi-tenant session caching | MSP workflow requires fast switching between tenants | ✓ Good — per-clientId MSAL PCA with MsalCacheHelper | -| BulkOperationRunner pattern | Continue-on-error with per-item results for all bulk ops | ✓ Good — consistent error handling across 4 bulk features | -| Wave 0 scaffold pattern | Models + interfaces + test stubs before implementation | ✓ Good — all phases had test targets from day 1 | - ---- -*Last updated: 2026-04-09 after v2.3 milestone started* diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md deleted file mode 100644 index f5ca69f..0000000 --- a/.planning/REQUIREMENTS.md +++ /dev/null @@ -1,71 +0,0 @@ -# Requirements: SharePoint Toolbox v2.3 - -**Defined:** 2026-04-09 -**Core Value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. - -## v2.3 Requirements - -Requirements for v2.3 Tenant Management & Report Enhancements. Each maps to roadmap phases. - -### App Registration - -- [x] **APPREG-01**: User can register the app on a target tenant from the profile create/edit dialog -- [x] **APPREG-02**: App auto-detects if user has Global Admin permissions before attempting registration -- [x] **APPREG-03**: App creates Azure AD application + service principal + grants required permissions atomically (with rollback on failure) -- [x] **APPREG-04**: User sees guided fallback instructions when auto-registration is not possible (insufficient permissions) -- [x] **APPREG-05**: User can remove the app registration from a target tenant -- [x] **APPREG-06**: App clears cached tokens and sessions when app registration is removed - -### Site Ownership - -- [x] **OWN-01**: User can enable/disable auto-take-ownership in application settings (global toggle, OFF by default) -- [x] **OWN-02**: App automatically takes site collection admin ownership when encountering access denied during scans (when toggle is ON) - -### Report Enhancements - -- [x] **RPT-01**: User can expand SharePoint groups in HTML reports to see group members -- [x] **RPT-02**: Group member resolution uses transitive membership to include nested group members -- [x] **RPT-03**: User can enable/disable entry consolidation per export (toggle in export settings) -- [x] **RPT-04**: Consolidated reports merge rows for the same user with identical access levels across multiple locations into a single row - -## Future Requirements - -### Site Ownership (deferred) - -- **OWN-03**: Persistent cleanup-pending list tracking sites where ownership was elevated -- **OWN-04**: Startup warning when stale ownership entries exist from previous sessions - -## Out of Scope - -| Feature | Reason | -|---------|--------| -| Auto-revoke permissions | Liability risk — read-only auditing tool, not remediation | -| Real-time ownership monitoring | Requires background service, beyond scope of desktop tool | -| Group expansion in CSV reports | CSV format doesn't support expandable sections; consolidation covers the dedup need | -| Custom permission scope selection for app registration | Fixed scope set covers all Toolbox features; custom scopes add complexity without value | - -## Traceability - -| Requirement | Phase | Status | -|-------------|-------|--------| -| APPREG-01 | Phase 19 | Complete | -| APPREG-02 | Phase 19 | Complete | -| APPREG-03 | Phase 19 | Complete | -| APPREG-04 | Phase 19 | Complete | -| APPREG-05 | Phase 19 | Complete | -| APPREG-06 | Phase 19 | Complete | -| OWN-01 | Phase 18 | Complete | -| OWN-02 | Phase 18 | Complete | -| RPT-01 | Phase 17 | Complete | -| RPT-02 | Phase 17 | Complete | -| RPT-03 | Phase 16 | Complete | -| RPT-04 | Phase 15 | Complete | - -**Coverage:** -- v2.3 requirements: 12 total -- Mapped to phases: 12 -- Unmapped: 0 - ---- -*Requirements defined: 2026-04-09* -*Last updated: 2026-04-09 after roadmap created* diff --git a/.planning/RETROSPECTIVE.md b/.planning/RETROSPECTIVE.md deleted file mode 100644 index 3ab71f5..0000000 --- a/.planning/RETROSPECTIVE.md +++ /dev/null @@ -1,53 +0,0 @@ -# Retrospective - -## Milestone: v1.0 — MVP - -**Shipped:** 2026-04-07 -**Phases:** 5 | **Plans:** 36 | **Commits:** 164 | **LOC:** 10,071 - -### What Was Built -- Complete C#/WPF rewrite replacing 6,400-line PowerShell monolith -- 10 feature tabs: Permissions, Storage, Search, Duplicates, Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates, Settings -- Multi-tenant MSAL authentication with per-tenant token caching -- CSV and interactive HTML export across all scan features -- Bulk operations with continue-on-error, per-item reporting, retry, and cancellation -- Self-contained 200 MB EXE with full EN/FR localization (199 keys) - -### What Worked -- **Wave 0 scaffold pattern**: Creating models, interfaces, and test stubs before implementation gave every phase testable targets from day 1 -- **FeatureViewModelBase contract**: Establishing the async/cancel/progress pattern in Phase 1 meant Phases 2-4 had zero friction adding new features -- **BulkOperationRunner abstraction**: One shared helper gave consistent error semantics across 4 different bulk operations -- **Phase dependency ordering**: Foundation → Permissions → Storage → Bulk → Hardening prevented rework -- **Atomic commits per task**: Each plan produced clear, reviewable commit history - -### What Was Inefficient -- **Phase 03 missing verification**: The only phase without a VERIFICATION.md — caught during milestone audit, but should have been produced during execution -- **Stale audit notes**: Phase 2 verification reported export buttons as hardcoded English, but they were actually already localized by the time of the audit — suggests verification reads code at a point-in-time snapshot that may not reflect later fixes -- **Cancel test locale mismatch**: The French locale test failure was flagged in Phase 3 plan 08 summary but deferred until post-milestone cleanup — should have been fixed inline - -### Patterns Established -- Write-then-replace JSON persistence with SemaphoreSlim for thread safety -- TranslationSource singleton with PropertyChanged(string.Empty) for runtime culture switching -- ExecuteQueryRetryHelper for throttle-aware CSOM calls (429/503 detection) -- SharePointPaginationHelper with ListItemCollectionPosition for 5,000+ item lists -- CsvValidationService with auto-delimiter detection (comma/semicolon) and BOM handling -- DataGrid RowStyle DataTrigger for invalid-row visual highlighting - -### Key Lessons -- Establish shared infrastructure patterns (auth, retry, pagination, progress) in Phase 1 — every subsequent phase benefits -- Test scaffolds (Wave 0) eliminate the "no tests until the end" anti-pattern -- Phase verifications should be mandatory during execution, not optional — catching Phase 03's gap at audit time is late -- Localization tests (key parity + diacritic spot-checks) are cheap and catch real bugs - ---- - -## Cross-Milestone Trends - -| Metric | v1.0 | -|--------|------| -| Phases | 5 | -| Plans | 36 | -| LOC | 10,071 | -| Tests | 134 pass / 22 skip | -| Timeline | 28 days | -| Commits | 164 | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md deleted file mode 100644 index 91c889e..0000000 --- a/.planning/ROADMAP.md +++ /dev/null @@ -1,136 +0,0 @@ -# Roadmap: SharePoint Toolbox v2 - -## Milestones - -- ✅ **v1.0 MVP** — Phases 1-5 (shipped 2026-04-07) — [archive](milestones/v1.0-ROADMAP.md) -- ✅ **v1.1 Enhanced Reports** — Phases 6-9 (shipped 2026-04-08) — [archive](milestones/v1.1-ROADMAP.md) -- ✅ **v2.2 Report Branding & User Directory** — Phases 10-14 (shipped 2026-04-09) — [archive](milestones/v2.2-ROADMAP.md) -- 🔄 **v2.3 Tenant Management & Report Enhancements** — Phases 15-19 (in progress) - -## Phases - -
-✅ v1.0 MVP (Phases 1-5) — SHIPPED 2026-04-07 - -- [x] Phase 1: Foundation (8/8 plans) — completed 2026-04-02 -- [x] Phase 2: Permissions (7/7 plans) — completed 2026-04-02 -- [x] Phase 3: Storage and File Operations (8/8 plans) — completed 2026-04-02 -- [x] Phase 4: Bulk Operations and Provisioning (10/10 plans) — completed 2026-04-03 -- [x] Phase 5: Distribution and Hardening (3/3 plans) — completed 2026-04-03 - -
- -
-✅ v1.1 Enhanced Reports (Phases 6-9) — SHIPPED 2026-04-08 - -- [x] Phase 6: Global Site Selection (5/5 plans) — completed 2026-04-07 -- [x] Phase 7: User Access Audit (10/10 plans) — completed 2026-04-07 -- [x] Phase 8: Simplified Permissions (6/6 plans) — completed 2026-04-07 -- [x] Phase 9: Storage Visualization (4/4 plans) — completed 2026-04-07 - -
- -
-✅ v2.2 Report Branding & User Directory (Phases 10-14) — SHIPPED 2026-04-09 - -- [x] Phase 10: Branding Data Foundation (3/3 plans) — completed 2026-04-08 -- [x] Phase 11: HTML Export Branding + ViewModel Integration (4/4 plans) — completed 2026-04-08 -- [x] Phase 12: Branding UI Views (3/3 plans) — completed 2026-04-08 -- [x] Phase 13: User Directory ViewModel (2/2 plans) — completed 2026-04-08 -- [x] Phase 14: User Directory View (2/2 plans) — completed 2026-04-09 - -
- -### v2.3 Tenant Management & Report Enhancements (Phases 15-19) - -- [x] **Phase 15: Consolidation Data Model** (2 plans) — PermissionConsolidator service and merged-row model; zero API calls, pure data shapes (completed 2026-04-09) -- [x] **Phase 16: Report Consolidation Toggle** (2 plans) — Export settings toggle wired to PermissionConsolidator; first user-visible consolidation behavior (completed 2026-04-09) -- [x] **Phase 17: Group Expansion in HTML Reports** (2 plans) — Clickable group expansion in HTML exports with transitive membership resolution (completed 2026-04-09) -- [x] **Phase 18: Auto-Take Ownership** (2 plans) — Global toggle and automatic site collection admin elevation on access denied (completed 2026-04-09) -- [x] **Phase 19: App Registration & Removal** (2 plans) — Automated Entra app registration with guided fallback and clean removal (completed 2026-04-09) - -## Phase Details - -### Phase 15: Consolidation Data Model -**Goal**: The data shape and merge logic for report consolidation exist and are fully testable in isolation before any UI touches them -**Depends on**: Nothing (no API calls, no UI dependencies) -**Requirements**: RPT-04 -**Success Criteria** (what must be TRUE): - 1. A `ConsolidatedPermissionEntry` model exists that represents a single user's merged access across multiple locations with identical access levels - 2. A `PermissionConsolidator` service accepts a flat list of permission rows and returns a consolidated list where duplicate user+level rows are merged - 3. Consolidation logic has unit test coverage — a known 10-row input with 3 duplicate pairs produces the expected 7-row output - 4. Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off) -**Plans:** 2/2 plans complete -Plans: -- [x] 15-01-PLAN.md — Models (LocationInfo, ConsolidatedPermissionEntry) + PermissionConsolidator service -- [x] 15-02-PLAN.md — Unit tests (10 test cases) + full solution build verification - -### Phase 16: Report Consolidation Toggle -**Goal**: Users can choose to merge duplicate permission entries per export through a toggle in the export settings dialog -**Depends on**: Phase 15 -**Requirements**: RPT-03 -**Success Criteria** (what must be TRUE): - 1. A consolidation toggle is visible in the export settings dialog (or export options panel) and defaults to OFF - 2. When the toggle is OFF, the exported HTML report is byte-for-byte identical to the pre-v2.3 output - 3. When the toggle is ON, the exported HTML report merges rows for the same user with identical access levels into a single row showing all affected locations - 4. The toggle state is remembered for the session (does not reset between exports within the same session) -**Plans:** 2/2 plans complete -Plans: -- [ ] 16-01-PLAN.md — ViewModel properties + XAML Export Options GroupBox + localization + CSV consolidation -- [ ] 16-02-PLAN.md — HTML consolidated rendering with expandable location sub-lists + full test verification - -### Phase 17: Group Expansion in HTML Reports -**Goal**: Users can expand SharePoint group entries in HTML reports to see the group's members, including members of nested groups -**Depends on**: Phase 16 -**Requirements**: RPT-01, RPT-02 -**Success Criteria** (what must be TRUE): - 1. SharePoint group rows in the HTML report render as expandable — clicking a group name reveals its member list inline - 2. Member resolution includes transitive membership: nested groups are recursively resolved so every leaf user is shown - 3. Group expansion is triggered at export time via Graph API — the permission scan itself is unchanged - 4. When Graph cannot resolve a group's members (throttled or insufficient scope), the report shows the group row with a "members unavailable" label rather than failing the export -**Plans:** 2/2 plans complete -Plans: -- [ ] 17-01-PLAN.md — ResolvedMember model + ISharePointGroupResolver service (CSOM + Graph transitive resolution) + DI registration -- [ ] 17-02-PLAN.md — HtmlExportService expandable group pills + toggleGroup JS + PermissionsViewModel wiring - -### Phase 18: Auto-Take Ownership -**Goal**: Users can enable automatic site collection admin elevation so that access-denied sites during scans no longer block audit progress -**Depends on**: Phase 15 -**Requirements**: OWN-01, OWN-02 -**Success Criteria** (what must be TRUE): - 1. A global "Auto-take ownership on access denied" toggle exists in application settings and defaults to OFF - 2. When the toggle is OFF, access-denied sites produce the same error behavior as before v2.3 (no regression) - 3. When the toggle is ON and a scan hits access denied on a site, the app automatically calls `Tenant.SetSiteAdmin` to elevate ownership and retries the site without interrupting the scan - 4. The scan result for an auto-elevated site is visually distinguishable from a normally-scanned site (e.g., a flag or icon in the results) -**Plans:** 2/2 plans complete -Plans: -- [ ] 18-01-PLAN.md — Settings toggle + OwnershipElevationService + PermissionEntry.WasAutoElevated flag -- [ ] 18-02-PLAN.md — Scan-loop elevation logic + DataGrid visual differentiation - -### Phase 19: App Registration & Removal -**Goal**: Users can register and remove the Toolbox's Azure AD application on a target tenant directly from the profile dialog, with a guided fallback when permissions are insufficient -**Depends on**: Phase 18 -**Requirements**: APPREG-01, APPREG-02, APPREG-03, APPREG-04, APPREG-05, APPREG-06 -**Success Criteria** (what must be TRUE): - 1. A "Register App" action is available in the profile create/edit dialog and is the recommended path for new tenant onboarding - 2. Before attempting registration, the app checks for Global Admin role and surfaces a clear message if the signed-in user lacks the required permissions, then presents step-by-step manual registration instructions as a fallback - 3. Registration creates the Azure AD application, service principal, and grants all required API permissions in a single atomic operation — if any step fails, all partial changes are rolled back and the user sees a specific error explaining what failed and why - 4. A "Remove App" action in the profile dialog removes the Azure AD application registration from the target tenant - 5. After removal, all cached MSAL tokens and session state for that tenant are cleared, and subsequent operations require re-authentication -**Plans:** 2/2 plans complete -Plans: -- [ ] 19-01-PLAN.md — IAppRegistrationService + AppRegistrationResult model + TenantProfile.AppId + service implementation + unit tests -- [ ] 19-02-PLAN.md — ViewModel RegisterApp/RemoveApp commands + XAML dialog UI + fallback panel + localization + VM tests - -## Progress - -| Phase | Milestone | Plans | Status | Completed | -|-------|-----------|-------|--------|-----------| -| 1-5 | v1.0 | 36/36 | Shipped | 2026-04-07 | -| 6-9 | v1.1 | 25/25 | Shipped | 2026-04-08 | -| 10-14 | v2.2 | 14/14 | Shipped | 2026-04-09 | -| 15. Consolidation Data Model | v2.3 | 2/2 | Complete | 2026-04-09 | -| 16. Report Consolidation Toggle | v2.3 | 2/2 | Complete | 2026-04-09 | -| 17. Group Expansion in HTML Reports | 2/2 | Complete | 2026-04-09 | — | -| 18. Auto-Take Ownership | 2/2 | Complete | 2026-04-09 | — | -| 19. App Registration & Removal | 2/2 | Complete | 2026-04-09 | — | diff --git a/.planning/STATE.md b/.planning/STATE.md deleted file mode 100644 index 66f7036..0000000 --- a/.planning/STATE.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -gsd_state_version: 1.0 -milestone: v2.3 -milestone_name: Tenant Management & Report Enhancements -status: planning -stopped_at: Completed 19-02-PLAN.md -last_updated: "2026-04-09T13:23:47.593Z" -last_activity: 2026-04-09 — Roadmap created for v2.3 (phases 15-19) -progress: - total_phases: 5 - completed_phases: 5 - total_plans: 10 - completed_plans: 10 ---- - -# Project State - -## Project Reference - -See: .planning/PROJECT.md (updated 2026-04-09) - -**Core value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. -**Current focus:** v2.3 Tenant Management & Report Enhancements — Phase 15 next - -## Current Position - -Phase: 15 — Consolidation Data Model (not started) -Plan: — -Status: Roadmap approved — ready to plan Phase 15 -Last activity: 2026-04-09 — Roadmap created for v2.3 (phases 15-19) - -``` -v2.3 Progress: ░░░░░░░░░░ 0% (0/5 phases) -``` - -## Shipped Milestones - -- v1.0 MVP — Phases 1-5 (shipped 2026-04-07) -- v1.1 Enhanced Reports — Phases 6-9 (shipped 2026-04-08) -- v2.2 Report Branding & User Directory — Phases 10-14 (shipped 2026-04-09) - -## v2.3 Phase Map - -| Phase | Name | Requirements | Status | -|-------|------|--------------|--------| -| 15 | Consolidation Data Model | RPT-04 | Not started | -| 16 | Report Consolidation Toggle | RPT-03 | Not started | -| 17 | Group Expansion in HTML Reports | RPT-01, RPT-02 | Not started | -| 18 | Auto-Take Ownership | OWN-01, OWN-02 | Not started | -| 19 | App Registration & Removal | APPREG-01..06 | Not started | - -## Accumulated Context - -### Decisions - -Decisions are logged in PROJECT.md Key Decisions table. - -**v2.3 notable constraints:** -- Phase 19 has the highest blast radius (Entra changes) — must be last -- Phase 15 is zero-API-call foundation; unblocks Phase 16 (consolidation) and Phase 18 (ownership) independently -- Group expansion (Phase 17) calls Graph at export time, not at scan time — scan pipeline unchanged -- Auto-take ownership uses PnP `Tenant.SetSiteAdmin` — requires Tenant Admin scope -- App registration must be atomic with rollback; partial Entra state is worse than no state -- [Phase 15]: MakeKey declared internal for test access via InternalsVisibleTo without exposing as public API -- [Phase 15]: LINQ GroupBy+Select for consolidation merge instead of mutable dictionary — consistent with functional codebase style -- [Phase 15-consolidation-data-model]: RPT-04-g test data uses 11 rows (not 10) to produce 7 consolidated rows — plan description had a counting error; 4 unique rows + 3 merged groups = 7 -- [Phase 16-01]: Consolidated branch uses early-return pattern inside WriteSingleFileAsync to leave existing code path untouched -- [Phase 16-01]: PermissionsViewModel gets MergePermissions as no-op placeholder reserved for future use -- [Phase 16-report-consolidation-toggle]: BuildConsolidatedHtml is a private method via early-return in BuildHtml — existing code path completely untouched -- [Phase 16-report-consolidation-toggle]: Separate locIdx counter for location groups (loc0, loc1...) distinct from grpIdx for user groups (ugrp0...) prevents ID collision -- [Phase 17]: Static helpers IsAadGroup/ExtractAadGroupId/StripClaims declared internal to enable unit testing via InternalsVisibleTo without polluting public API -- [Phase 17]: Graph client created lazily on first AAD group encountered to avoid unnecessary auth overhead for groups with no nested AAD members -- [Phase 17]: groupMembers optional param in HtmlExportService — null produces identical pre-Phase-17 output; ISharePointGroupResolver injected as optional last param in PermissionsViewModel; resolution failure degrades gracefully with LogWarning -- [Phase 18-auto-take-ownership]: OwnershipElevationService uses Tenant.SetSiteAdmin from PnP.Framework -- [Phase 18-auto-take-ownership]: WasAutoElevated last positional param with default=false preserves all existing PermissionEntry callsites -- [Phase 18-auto-take-ownership]: AutoTakeOwnership ViewModel setter uses fire-and-forget pattern matching DataFolder -- [Phase 18-auto-take-ownership]: Toggle read before scan loop (not in exception filter) — await in when clause unsupported; pre-read bool preserves semantics -- [Phase 18-auto-take-ownership]: WasAutoElevated DataTrigger last in RowStyle.Triggers — amber wins over RiskLevel color -- [Phase 19-app-registration-removal]: AppRegistrationService uses AppGraphClientFactory alias to disambiguate from Microsoft.Graph.GraphClientFactory -- [Phase 19-app-registration-removal]: BuildRequiredResourceAccess declared internal to enable direct unit testing without live Graph calls -- [Phase 19-app-registration-removal]: SharePoint AllSites.FullControl GUID marked LOW confidence — must be verified against live tenant -- [Phase 19-app-registration-removal]: ProfileManagementViewModel constructor gains IAppRegistrationService as last param — existing logo tests updated to 5-param -- [Phase 19-app-registration-removal]: TranslationSource.Instance used directly in ViewModel for status strings (consistent with runtime locale switching) -- [Phase 19-app-registration-removal]: BooleanToVisibilityConverter declared in Window.Resources (WPF built-in, no custom converter needed) - -### Pending Todos - -None. - -### Blockers/Concerns - -None. - -## Session Continuity - -Last session: 2026-04-09T13:20:36.865Z -Stopped at: Completed 19-02-PLAN.md -Resume file: None -Next step: `/gsd:plan-phase 15` diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md deleted file mode 100644 index 1c7ffda..0000000 --- a/.planning/codebase/ARCHITECTURE.md +++ /dev/null @@ -1,302 +0,0 @@ -# Architecture - -**Analysis Date:** 2026-04-02 - -## Pattern Overview - -**Overall:** Monolithic PowerShell Application with WinForms UI and Async Runspace Pattern - -**Key Characteristics:** -- Single-file PowerShell script (6408 lines) serving as entry point -- Native WinForms GUI (no external UI framework dependencies) -- Asynchronous operations via dedicated PowerShell runspaces to prevent UI blocking -- Hashtable-based state management for inter-runspace communication -- PnP.PowerShell module for all SharePoint Online interactions -- Profile and template persistence via JSON files -- Region-based code organization for logical grouping - -## Layers - -**Presentation Layer (GUI):** -- Purpose: User interface and interaction handling -- Location: `Sharepoint_ToolBox.ps1` lines 2990-3844 (GUI setup) + event handlers -- Contains: WinForms controls, dialogs, input validation, visual updates -- Depends on: Shared helpers, Settings layer -- Used by: Event handlers, runspace callbacks via synchronized hashtable - -**Application Layer (Business Logic):** -- Purpose: Core operations for each feature (permissions, storage, templates, search, duplicates) -- Location: `Sharepoint_ToolBox.ps1` multiple regions: - - Permissions: lines 1784-2001 - - Storage: lines 2002-2110 - - File Search: lines 2112-2233 - - Duplicates: lines 2235-2408 - - Templates: lines 475-1360 - - Transfer/Bulk: lines 2410-3000 -- Contains: PnP API calls, data aggregation, report generation -- Depends on: PnP.PowerShell module, Presentation feedback -- Used by: Event handlers via runspaces, HTML/CSV export functions - -**Data Access Layer:** -- Purpose: File I/O, persistence, caching -- Location: `Sharepoint_ToolBox.ps1` dedicated regions: - - Profile Management: lines 48-127 - - Settings: lines 129-154 - - Template Management: lines 475-533 -- Contains: JSON serialization/deserialization, profile CRUD, settings management -- Depends on: File system access -- Used by: Application layer, GUI initialization - -**Export & Reporting Layer:** -- Purpose: Transform data to CSV and interactive HTML -- Location: `Sharepoint_ToolBox.ps1`: - - Permissions HTML: lines 1361-1617 - - Storage HTML: lines 1619-1784 - - Search HTML: lines 2112-2233 - - Duplicates HTML: lines 2235-2408 - - Transfer HTML: lines 2412-2547 -- Contains: HTML template generation, JavaScript for interactivity, CSV formatting -- Depends on: Application layer data, System.Drawing for styling -- Used by: Feature implementations for export operations - -**Integration Layer:** -- Purpose: External service communication (SharePoint, PnP.PowerShell) -- Location: `Sharepoint_ToolBox.ps1` PnP function regions -- Contains: Connect-PnPOnline, Get-PnP* cmdlets, authentication handling -- Depends on: PnP.PowerShell module, credentials from user input -- Used by: Application layer operations - -**Utilities & Helpers:** -- Purpose: Cross-cutting formatting, UI helpers, internationalization -- Location: `Sharepoint_ToolBox.ps1`: - - Shared Helpers: lines 4-46 - - Internationalization: lines 2732-2989 - - UI Control Factories: lines 3119-3146 -- Contains: Write-Log, Format-Bytes, EscHtml, T() translator, control builders -- Depends on: System.Windows.Forms, language JSON file -- Used by: All other layers - -## Data Flow - -**Permissions Report Generation:** - -1. User selects site(s) and report options in GUI (Permissions tab) -2. Click "Générer le rapport" triggers event handler at line 4068+ -3. Validation via `Validate-Inputs` (line 30) -4. GUI triggers runspace via `Start-Job` with user parameters -5. Runspace calls `Generate-PnPSitePermissionRpt` (line 1852) -6. `Generate-PnPSitePermissionRpt` connects to SharePoint via `Connect-PnPOnline` (line 1864) -7. Recursive permission scanning: - - `Get-PnPWebPermission` (line 1944) for site/webs - - `Get-PnPListPermission` (line 1912) for lists and libraries - - `Get-PnPFolderPermission` (line 1882) for folders (if enabled) - - `Get-PnPPermissions` (line 1786) extracts individual role assignments -8. Results accumulated in `$script:AllPermissions` array -9. Export based on format choice: - - CSV: `Merge-PermissionRows` (line 1363) then `Export-Csv` - - HTML: `Export-PermissionsToHTML` (line 1389) generates interactive report -10. Output file path returned to UI via synchronized hashtable -11. User can open report via `btnPermOpen` click handler - -**Storage Metrics Scan:** - -1. User selects storage options and sites -2. Click "Générer les métriques" triggers runspace job -3. Job calls `Get-SiteStorageMetrics` (line 2004) -4. Per-site or per-library scanning: - - Connect to web via `Connect-PnPOnline` - - `Get-PnPList` retrieves document libraries (if per-library mode) - - `Get-PnPFolderStorageMetric` for library/root metrics - - `Collect-FolderStorage` (recursive nested function) walks folder tree to configured depth -5. Results accumulate in `$script:storageResults` with hierarchy intact -6. HTML or CSV export writes report file -7. File path communicated back to UI - -**Site Picker (Browse Sites):** - -1. User clicks "Voir les sites" button -2. `Show-SitePicker` dialog opens (line 212) -3. User clicks "Charger les sites" button -4. Dialog initializes `$script:_pkl` state hashtable (line 315) -5. Runspace spawned in `btnLoad.Add_Click` (line 395) -6. Runspace connects to admin site and retrieves all sites via `Get-PnPTenantSite` -7. Results queued back to UI via synchronized `$script:_pkl.Sync` hashtable -8. Timer polls `$script:_pkl.Sync` and updates ListView asynchronously -9. User filters by text, sorts columns, checks/unchecks sites -10. Returns selected site URLs in `$script:SelectedSites` array - -**File Search:** - -1. User enters search criteria (extensions, regex, date ranges, etc.) -2. Click "Lancer la recherche" triggers runspace -3. Runspace uses PnP Search API (KQL) with filters: - - File extension filters via `fileExtension:ext1` OR syntax - - Date range filters via `Created >= date` - - Regex applied client-side after retrieval -4. Results paginated and accumulated -5. Exported to CSV or HTML with interactive filtering/sorting - -**Duplicate Detection:** - -1. User chooses file or folder mode and comparison criteria -2. Click "Lancer le scan" triggers runspace -3. File duplicates: Search API with filename-based grouping -4. Folder duplicates: Enumerate all folders, compare attributes (size, dates, subfolder/file counts) -5. Results grouped by match criteria -6. HTML export shows grouped duplicates with visual indicators (green/orange for matching/differing fields) - -**Template Capture & Apply:** - -1. Capture mode: `Show-TemplateManager` dialog (line 542) - - User selects "Capture from site" - - Runspace scans site structure via `Get-PnPList`, `Get-PnPFolderItem`, `Get-PnPWebPermission` - - Captures libraries, folders, permission groups, site logo, title - - Persisted to `Sharepoint_Templates.json` -2. Apply mode: User selects template and target site - - Runspace creates lists/libraries via `New-PnPList` - - Replicates folder structure via `New-PnPFolder` - - Applies permission groups if selected - - Logs creation results - -**State Management:** - -- `$script:` variables hold state across runspace calls (profiles, sites, results, settings) -- Synchronized hashtables (`$script:_pkl`, `$script:_sync`) enable runspace-to-UI communication -- Timer at line 3850-3870 polls synchronized hashtable and updates GUI with progress/results -- Event handlers trigger jobs but don't block waiting for completion (asynchronous pattern) - -## Key Abstractions - -**Runspace Encapsulation:** - -- Purpose: Execute long-running SharePoint operations without freezing GUI -- Pattern: `$job = Start-Job -ScriptBlock { ... } -RunspacePool $rsPool` -- Example: `Start-NextStorageScan` (line 4536) manages storage scan runspace jobs -- Trade-off: Requires careful state management via shared hashtables; no direct closures - -**Hashtable-Based State:** - -- Purpose: Share mutable state between main runspace and job runspaces -- Pattern: `$sync = @{ Data = @(); Status = "Running" }` passed to job -- Example: `$script:_pkl` (line 315) manages site picker state across checkbox events -- Benefit: Avoids closure complexity; timer can poll changes safely - -**Dialog Modal Isolation:** - -- Purpose: Site picker and template manager as isolated UI contexts -- Pattern: `Show-SitePicker` and `Show-TemplateManager` create self-contained `Form` objects -- State stored in `$script:_pkl` and `$script:_tpl` respectively -- Returns result arrays (selected sites, template data) to main form - -**Language Translation System:** - -- Purpose: Internationalization without external dependencies -- Pattern: `T("key")` function (line 2908) looks up keys in `$script:LangDict` hashtable -- Source: `lang/fr.json` contains French translations; English is hardcoded -- Used throughout: All UI labels, buttons, messages use `T()` for localization - -**HTML Export Templates:** - -- Purpose: Dynamically generate interactive HTML reports with embedded JavaScript -- Pattern: String templates with `@"` heredoc syntax containing HTML/CSS/JS -- Examples: - - `Export-PermissionsToHTML` (line 1389): Responsive table, collapsible groups, copy-to-clipboard - - `Export-StorageToHTML` (line 1621): Tree visualization, sorting, filtering - - `Export-DuplicatesToHTML` (line 2235): Grouped duplicates with visual indicators -- Benefit: No external libraries; reports are self-contained single-file HTML - -## Entry Points - -**Main GUI Form:** -- Location: `Sharepoint_ToolBox.ps1` line 2992 -- Triggers: Script execution via `.ps1` file or PowerShell IDE -- Responsibilities: - - Initialize WinForms components (form, controls, menus) - - Load and populate profiles/settings from JSON - - Register event handlers for all buttons and controls - - Run main event loop `[void]$form.ShowDialog()` - -**Feature Event Handlers:** -- Location: Various in lines 4068+ (Event Handlers region) -- Examples: - - `btnPermRun.Add_Click` → Permissions report generation - - `btnStorRun.Add_Click` → Storage metrics scan - - `btnSearchRun.Add_Click` → File search - - `btnDupRun.Add_Click` → Duplicate detection -- Pattern: Validate inputs, start runspace job, launch progress animation, register cleanup callback - -**Background Runspaces:** -- Entry: `Start-Job -ScriptBlock { Generate-PnPSitePermissionRpt ... }` -- Execution: PnP cmdlets execute within runspace's isolated context -- Completion: Job completion callback writes results to synchronized hashtable; timer detects and updates UI - -**Language Switch:** -- Location: Menu → Language submenu (line 3011+) -- Handler: `Switch-AppLanguage` (line 4167) -- Updates: All UI labels via `Update-UILanguage` (line 2951) - -## Error Handling - -**Strategy:** Try/Catch with graceful degradation; errors logged to UI RichTextBox - -**Patterns:** - -1. **Runspace Error Handling:** - ```powershell - try { $result = Get-PnPList } - catch { Write-Log "Error: $($_.Exception.Message)" "Red" } - ``` - -2. **Connection Validation:** - - `Validate-Inputs` (line 30) checks required fields before operation - - `Connect-PnPOnline` fails if credentials invalid; caught and logged - -3. **File I/O Protection:** - ```powershell - if (Test-Path $path) { - try { $data = Get-Content $path -Raw | ConvertFrom-Json } - catch {} # Silently ignore JSON parse errors - } - ``` - -4. **UI Update Safety:** - - `Write-Log` checks `if ($script:LogBox -and !$script:LogBox.IsDisposed)` before updating - - Prevents access to disposed UI objects after form close - -5. **Missing Configuration Handling:** - - Settings default to English + current directory if file missing - - Profiles default to empty array if file missing - - Templates default to empty if file corrupted - -## Cross-Cutting Concerns - -**Logging:** -- Framework: `Write-Log` function (line 6) -- Pattern: Writes colored messages to RichTextBox + host console -- Usage: All operations log status (connecting, scanning, exporting) -- Timestamps: `Get-Date -Format 'HH:mm:ss'` prefixes each message - -**Validation:** -- Entry point: `Validate-Inputs` (line 30) checks ClientID and Site URL -- Pattern: Early return if missing; user sees MessageBox with missing field hint -- Localization: Error messages use `T()` function for i18n - -**Authentication:** -- Method: Interactive browser login via `Connect-PnPOnline -Interactive` -- Pattern: PnP module opens browser for Azure AD consent; token cached within session -- Credential scope: Per site connection; multiple connections supported (for multi-site operations) -- Token management: Automatic via PnP.PowerShell; no manual handling - -**Asynchronous Progress:** -- Animation: `Start-ProgressAnim` (line 3845) flashes "Running..." in status label -- Polling: Timer at line 3850-3870 checks `$job.State` and synchronized hashtable every 300ms -- Cleanup: `Stop-ProgressAnim` (line 3850) stops animation when job completes - -**UI Responsiveness:** -- Pattern: `[System.Windows.Forms.Application]::DoEvents()` called during long operations -- Benefit: Allows UI events (button clicks, close) to process while waiting -- Cost: Runspace jobs recommended for truly long operations (>5 second operations) - ---- - -*Architecture analysis: 2026-04-02* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md deleted file mode 100644 index e9e3111..0000000 --- a/.planning/codebase/CONCERNS.md +++ /dev/null @@ -1,221 +0,0 @@ -# Codebase Concerns - -**Analysis Date:** 2026-04-02 - -## Tech Debt - -**Silent Error Handling (Widespread):** -- Issue: 38 empty `catch` blocks that suppress errors without logging -- Files: `Sharepoint_ToolBox.ps1` (lines 1018, 1020, 1067, 1068, 1144, 2028, 2030, etc.) -- Impact: Failures go unnoticed, making debugging difficult. Users don't know why operations fail. Error conditions are hidden from logs. -- Fix approach: Add logging to all `catch` blocks. Use `BgLog` for background tasks, `Write-Log` for UI threads. Example: `catch { BgLog "Folder enumeration failed: $_" "DarkGray" }` instead of `catch {}` - -**Resource Cleanup Issues:** -- Issue: Runspace and PowerShell objects created in background jobs may not be properly disposed if exceptions occur -- Files: `Sharepoint_ToolBox.ps1` lines 1040-1052, 4564-4577, 5556-5577 -- Impact: Memory leaks possible if UI interactions are interrupted. Zombie runspaces could accumulate over multiple operations. -- Fix approach: Wrap all runspace/PS object creation in try-finally blocks. Ensure `$rs.Dispose()` and `$ps.Dispose()` are called in finally block, not just in success path - -**Overly Broad Error Suppression:** -- Issue: 27 instances of `-ErrorAction SilentlyContinue` spread throughout code -- Files: `Sharepoint_ToolBox.ps1` (e.g., lines 1142, 1188, 2070, 4436, etc.) -- Impact: Real failures indistinguishable from expected failures (e.g., list doesn't exist vs. connection failed). Masks bugs. -- Fix approach: Use selective error suppression. Only suppress when you've explicitly checked for the condition (e.g., "if list doesn't exist, create it"). Otherwise use `-ErrorAction Stop` with explicit try-catch. - -**Inconsistent JSON Error Handling:** -- Issue: JSON parsing in `Load-Profiles`, `Load-Settings`, `Load-Templates` uses empty catch blocks -- Files: `Sharepoint_ToolBox.ps1` lines 61, 140, 568-570 -- Impact: Corrupted JSON files silently fail and return empty defaults, losing user data silently -- Fix approach: Log actual error message. Implement validation schema. Create backup of corrupted files. - -## Known Bugs - -**Blank Client ID Warning Not Actionable:** -- Symptoms: "WARNING: No Client ID returned" appears but doesn't prevent further operations or clear user input -- Files: `Sharepoint_ToolBox.ps1` line 4237 -- Trigger: Azure AD app registration completes but returns null ClientId (can happen with certain tenant configurations) -- Workaround: Manually register app via Azure Portal and paste Client ID -- Fix approach: Check for null ClientId before continuing, clear the warning state properly - -**Group Member Addition Silent Failures:** -- Symptoms: Members appear not to be added to sites, but no error shown in UI -- Files: `Sharepoint_ToolBox.ps1` lines 1222-1225, 5914, 5922 (try-catch with SilentlyContinue) -- Trigger: User exists but cannot be added to group (permissions, licensing, or source-specific SP group issues) -- Workaround: Manual group membership assignment -- Fix approach: Replace SilentlyContinue with explicit logging of why Add-PnPGroupMember failed - -**Folder Metadata Loss in Template Application:** -- Symptoms: Folder permissions captured correctly but not reapplied when permissions=true but structure already exists -- Files: `Sharepoint_ToolBox.ps1` lines 1229-1292 (folder-level permission application depends on library structure map being built first) -- Trigger: Target library created by Apply-FolderTree but permissions application logic expects library to already exist in template structure -- Workaround: Delete and recreate target library, or manually apply permissions via SharePoint UI -- Fix approach: Build library map before applying folder tree, or add validation that all referenced libraries exist - -**CSV Import for Bulk Operations Not Validated:** -- Symptoms: Invalid CSV format silently fails, users see no clear error, form appears unresponsive -- Files: `Sharepoint_ToolBox.ps1` lines 5765-5800 (CSV parsing with inadequate error context) -- Trigger: CSV with missing headers, wrong delimiter, or invalid format -- Workaround: Edit CSV manually to match expected format, restart tool -- Fix approach: Add CSV schema validation before processing, show specific validation errors - -## Security Considerations - -**Client ID and Tenant URL Hardcoded in Temp Files:** -- Risk: Temp registration script contains unencrypted Client ID and Tenant ID in plaintext -- Files: `Sharepoint_ToolBox.ps1` lines 4210-4245 (temp file creation) -- Current mitigation: Temp file cleanup attempted but not guaranteed if process crashes -- Recommendations: Use SecureString to pass credentials, delete temp file with -Force in finally block, or use named pipes instead of files - -**No Validation of Tenant URL Format:** -- Risk: Arbitrary URLs accepted, could be typos leading to authentication against wrong tenant -- Files: `Sharepoint_ToolBox.ps1` lines 4306-4317, 30-43 (Validate-Inputs) -- Current mitigation: URL used as-is, relies on PnP authentication failure to catch issues -- Recommendations: Add regex validation for SharePoint tenant URLs, warn on suspicious patterns - -**Profile File Contains Credentials in Plaintext:** -- Risk: `Sharepoint_Export_profiles.json` contains Client ID and Tenant URL in plaintext on disk -- Files: `Sharepoint_ToolBox.ps1` lines 50-72 (profile persistence) -- Current mitigation: File located in user home directory (Windows ACL protection), but still plaintext -- Recommendations: Consider encrypting profile file with DPAPI, or move to Windows Credential Manager - -**PnP PowerShell Module Trust Not Validated:** -- Risk: Module imported without version pinning, could load compromised version -- Files: `Sharepoint_ToolBox.ps1` lines 151, 1151, 4218, 4521, 5833 (Import-Module PnP.PowerShell) -- Current mitigation: None -- Recommendations: Pin module version in manifest, use `-MinimumVersion` parameter, check module signature - -## Performance Bottlenecks - -**Synchronous UI Freezes During Large Operations:** -- Problem: File search with 50,000 result limit processes all results at once, building HTML string in memory -- Files: `Sharepoint_ToolBox.ps1` lines 2112-2133 (Export-SearchResultsToHTML builds entire table in string) -- Cause: All results concatenated into single `$rows` string before sending to UI -- Improvement path: Implement pagination in HTML reports, stream results rather than buffering all in memory. For large datasets, chunk exports into multiple files. - -**Folder Storage Recursion Not Depth-Limited by Default:** -- Problem: `Collect-FolderStorage` recurses unlimited depth unless explicitly capped, can take hours on deep folder structures -- Files: `Sharepoint_ToolBox.ps1` lines 2009-2032, 4432-4455 -- Cause: CurrentDepth compared against FolderDepth limit, but FolderDepth defaults to 999 if not set -- Improvement path: Default to depth 3-4, show estimated scan time based on depth, implement cancellation token - -**No Parallel Processing for Multiple Sites:** -- Problem: Sites processed sequentially in Permissions/Storage reports, one site blocks all others -- Files: `Sharepoint_ToolBox.ps1` lines 4379-4401 (foreach loop in permissions scan) -- Cause: Single-threaded approach with `Connect-PnPOnline` context switches -- Improvement path: Queue-based processing for multiple sites (partially done for storage scans), implement async context management - -**HTML Report Generation for Large Duplicates List:** -- Problem: Export-DuplicatesToHTML builds entire HTML in memory, slow for 10,000+ duplicates -- Files: `Sharepoint_ToolBox.ps1` lines 2235-2400 (HTML string concatenation in loop) -- Cause: All groups converted to HTML before writing to file -- Improvement path: Stream HTML generation, write to file incrementally, implement lazy-loading tables in browser - -## Fragile Areas - -**Language System (T() function):** -- Files: `Sharepoint_ToolBox.ps1` (translation lookups throughout, ~15 hardcoded English fallbacks like "Veuillez renseigner") -- Why fragile: Language loading can fail silently, UI control updates hardcoded at multiple locations, no fallback chain for missing translations -- Safe modification: Add validation that all UI strings have corresponding translation keys before form creation. Create helper function that returns English default if key missing. -- Test coverage: No tests for translation system. Gaps: Missing translations for error messages, hardcoded "Veuillez renseigner" strings that bypass T() function - -**Profile Management:** -- Files: `Sharepoint_ToolBox.ps1` lines 50-127 -- Why fragile: Profile list is in-memory array that syncs with JSON file. If Save-Profiles fails, changes are lost. No transaction semantics. -- Safe modification: Implement write-lock pattern. Create backup before write. Validate JSON before replacing file. -- Test coverage: No validation that profile save actually persists. Race condition if opened in multiple instances. - -**Runspace State Machine:** -- Files: `Sharepoint_ToolBox.ps1` lines 1034-1095, 4580-4650 (runspace creation, async timer polling) -- Why fragile: UI state (`btnGenPerms.Enabled = false`) set before runspace begins, but no explicit state reset if runspace crashes or hangs -- Safe modification: Implement state enum (Idle, Running, Done, Error). Always reset state in finally block. Set timeout on runspace execution. -- Test coverage: No timeout tests. Gaps: What happens if runspace hangs indefinitely? Button remains disabled forever. - -**Site Picker List View:** -- Files: `Sharepoint_ToolBox.ps1` lines 157-250 (_Pkl-* functions) -- Why fragile: AllSites list updates while UI may be reading it (SuppressCheck flag used but incomplete synchronization) -- Safe modification: Use proper locking or rebuild entire list atomically. Current approach relies on flag which may miss updates. -- Test coverage: No concurrent access tests. Gaps: What if site is checked while sort is happening? - -## Scaling Limits - -**Permissions Report HTML with Large Item Counts:** -- Current capacity: Tested up to ~5,000 items, performance degrades significantly -- Limit: HTML table becomes unusable in browser above 10,000 rows (sorting, filtering slow) -- Scaling path: Implement client-side virtual scrolling in HTML template, paginate into multiple reports, add server-side filtering before export - -**File Search Result Limit:** -- Current capacity: 50,000 result maximum hardcoded -- Limit: Beyond 50,000 files, results truncated without warning -- Scaling path: Implement pagination in SharePoint Search API, show "more results available" warning, allow user to refine search - -**Runspace Queue Processing:** -- Current capacity: Single queue per scan, sequential processing -- Limit: If background job produces messages faster than timer dequeues, queue could grow unbounded -- Scaling path: Implement back-pressure (slow producer if queue > 1000 items), implement priority queue - -**Profile JSON File Size:** -- Current capacity: Profiles loaded entirely into memory, no limit on file size -- Limit: If user creates 1,000+ profiles, JSON file becomes slow to load/save -- Scaling path: Implement profile paging, index file by profile name, lazy-load profile details - -## Dependencies at Risk - -**PnP.PowerShell Module Version Mismatch:** -- Risk: Module API changes between major versions, cmdlet parameter changes -- Impact: Features relying on specific cmdlet parameters break silently -- Migration plan: Pin to stable version range in script header. Create version compatibility matrix. Test against 2-3 stable versions. - -**System.Windows.Forms Dependency:** -- Risk: WinForms support in PowerShell 7 is deprecated, future versions may not ship it -- Impact: GUI completely broken on future PowerShell versions -- Migration plan: Consider migrating to WPF or cross-platform GUI framework (Avalonia). Current WinForms code is tied to Assembly loading. - -## Missing Critical Features - -**No Operation Cancellation:** -- Problem: Running operations (permissions scan, storage metrics, file search) cannot be stopped mid-execution -- Blocks: User stuck waiting for slow operations to complete, no way to abort except kill process - -**No Audit Log:** -- Problem: No record of who ran what operation, what results were exported, when last backup occurred -- Blocks: Compliance, troubleshooting - -**No Dry-Run for Most Operations:** -- Problem: Only version cleanup has dry-run. Permission changes, site creation applied immediately without preview -- Blocks: Prevents risk assessment before making changes - -## Test Coverage Gaps - -**PnP Connection Failures:** -- What's not tested: Connection timeouts, intermittent network issues, authentication failures mid-operation -- Files: `Sharepoint_ToolBox.ps1` lines 36, 157, 170, 2036, 4458, etc. (Connect-PnPOnline calls) -- Risk: Tool may hang indefinitely if connection drops. No retry logic. -- Priority: High - -**Malformed JSON Resilience:** -- What's not tested: Templates.json, Profiles.json, Settings.json with invalid JSON, missing fields, type mismatches -- Files: `Sharepoint_ToolBox.ps1` lines 61, 140, 568 (ConvertFrom-Json) -- Risk: Tool fails to start or loses user data -- Priority: High - -**Large-Scale Operations:** -- What's not tested: Permissions scan on site with 50,000+ items, storage metrics on 10,000+ folders, file search returning 40,000+ results -- Files: Bulk scanning functions throughout -- Risk: Memory exhaustion, timeout, UI freeze -- Priority: Medium - -**Runspace Cleanup on Error:** -- What's not tested: Runspace exception handling, cleanup if UI window closes during background operation -- Files: `Sharepoint_ToolBox.ps1` lines 1040-1052, 4564-4577, 5556-5577 -- Risk: Zombie processes, resource leaks -- Priority: Medium - -**CSV Format Validation:** -- What's not tested: Invalid column headers, wrong delimiter, missing required columns in bulk operations -- Files: `Sharepoint_ToolBox.ps1` lines 5765-5800 -- Risk: Silent failures, partial data import -- Priority: Medium - ---- - -*Concerns audit: 2026-04-02* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md deleted file mode 100644 index 595955c..0000000 --- a/.planning/codebase/CONVENTIONS.md +++ /dev/null @@ -1,210 +0,0 @@ -# Coding Conventions - -**Analysis Date:** 2026-04-02 - -## Naming Patterns - -**Functions:** -- PascalCase for public functions: `Write-Log`, `Get-ProfilesFilePath`, `Load-Profiles`, `Save-Profiles`, `Show-InputDialog` -- Verb-Noun format using standard PowerShell verbs: Get-, Load-, Save-, Show-, Export-, Apply-, Validate-, Merge-, Refresh-, Switch- -- Private/internal functions prefixed with underscore: `_Pkl-FormatMB`, `_Pkl-Sort`, `_Pkl-Repopulate`, `_Tpl-Repopulate`, `_Tpl-Log` -- Descriptive names reflecting operation scope: `Get-SiteStorageMetrics`, `Collect-FolderStorage`, `Collect-WebStorage`, `Export-PermissionsToHTML` - -**Variables:** -- camelCase for local variables: `$message`, `$color`, `$data`, `$index`, `$siteSrl` -- PascalCase for control variables and form elements: `$form`, `$LogBox`, `$ClientId`, `$SiteURL` -- Prefixed script-scope variables with `$script:` for shared state: `$script:LogBox`, `$script:Profiles`, `$script:SelectedSites`, `$script:_pkl`, `$script:_tpl` -- Abbreviated but meaningful names in tight loops: `$s` (site), `$e` (event), `$i` (index), `$m` (message), `$c` (color) -- Hashtable keys use camelCase: `@{ name = "...", clientId = "...", tenantUrl = "..." }` - -**Parameters:** -- Type hints included in function signatures: `[string]$Message`, `[array]$Data`, `[switch]$IncludeSubsites`, `[int]$CurrentDepth` -- Optional parameters use `= $null` or `= $false` defaults: `[string]$Color = "LightGreen"`, `[System.Windows.Forms.Form]$Owner = $null` -- Single-letter abbreviated parameters in nested functions: `param($s, $e)` for event handlers - -**File/Directory Names:** -- Single main script file: `Sharepoint_ToolBox.ps1` -- Settings/profile files: `Sharepoint_Settings.json`, `Sharepoint_Export_profiles.json`, `Sharepoint_Templates.json` -- Generated report files use pattern: `{ReportType}_{site/date}_{timestamp}.{csv|html}` - -## Code Style - -**Formatting:** -- No explicit formatter configured -- Indentation: 4 spaces (PowerShell default) -- Line length: practical limit around 120 characters (some HTML generation lines exceed this) -- Braces on same line for blocks: `function Name { ... }`, `if ($condition) { ... }` -- Region markers used for file organization: `#region ===== Section Name =====` and `#endregion` - -**Regions Organization (in `Sharepoint_ToolBox.ps1`):** -- Shared Helpers (utility functions) -- Profile Management (profile CRUD, loading/saving) -- Settings (configuration handling) -- Site Picker (dialog and list management) -- Template Management (capture, apply, storage) -- HTML Export: Permissions and Storage (report generation) -- PnP: Permissions and Storage Metrics (SharePoint API operations) -- File Search (advanced file search functionality) -- Transfer (file/folder transfer operations) -- Bulk Site Creation (site creation from templates) -- Internationalization (multi-language support) -- GUI (main form and controls definition) -- Event Handlers (button clicks, selections, menu actions) -- Structure (folder tree CSV parsing) - -**Comments:** -- Inline comments explain non-obvious logic: `# Groups rows that share the same Users + Permissions` -- Block comments precede major sections: `# -- Top bar --`, `# -- Site list (ListView with columns) --` -- Section separators use dashes: `# ── Profile Management ─────────────────────────────────` -- Descriptive comments in complex functions explain algorithm: `# Recursively collects subfolders up to $MaxDepth levels deep` -- No JSDoc/TSDoc style - pure text comments - -## Import Organization - -**Module Imports:** -- `Add-Type -AssemblyName` for .NET assemblies at script start: - - `System.Windows.Forms` for UI controls - - `System.Drawing` for colors and fonts -- `Import-Module PnP.PowerShell` dynamically when needed in background runspace blocks -- No explicit order beyond UI assemblies first - -## Error Handling - -**Patterns:** -- Broad `try-catch` blocks with minimal logging: `try { ... } catch {}` -- Silent error suppression common: empty catch blocks swallow exceptions -- Explicit error capture in key operations: `catch { $Sync.Error = $_.Exception.Message }` -- Error logging via `Write-Log` with color coding: - - Red for critical failures: `Write-Log "Erreur: $message" "Red"` - - Yellow for informational messages: `Write-Log "Processing..." "Yellow"` - - DarkGray for skipped items: `Write-Log "Skipped: $reason" "DarkGray"` -- Exception messages extracted and logged: `$_.Exception.Message` -- Validation checks return boolean: `if ([string]::IsNullOrWhiteSpace(...)) { return $false }` - -## Logging - -**Framework:** Native `Write-Log` function + UI RichTextBox display - -**Patterns:** -```powershell -function Write-Log { - param([string]$Message, [string]$Color = "LightGreen") - if ($script:LogBox -and !$script:LogBox.IsDisposed) { - # Append to UI with timestamp and color - $script:LogBox.AppendText("$(Get-Date -Format 'HH:mm:ss') - $Message`n") - } - Write-Host $Message # Also output to console -} -``` - -**Logging locations:** -- Long-running operations log to RichTextBox in real-time via background runspace queue -- Background functions use custom `BgLog` helper that queues messages: `function BgLog([string]$m, [string]$c="LightGreen")` -- Colors indicate message type: LightGreen (success), Yellow (info), Cyan (detail), DarkGray (skip), Red (error) -- Timestamps added automatically: `HH:mm:ss` format - -## Validation - -**Input Validation:** -- Null/whitespace checks: `[string]::IsNullOrWhiteSpace($variable)` -- Array/collection size checks: `$array.Count -gt 0`, `$items -and $items.Count -gt 0` -- Index bounds validation: `if ($idx -lt 0 -or $idx -ge $array.Count) { return }` -- UI MessageBox dialogs for user-facing errors: `[System.Windows.Forms.MessageBox]::Show(...)` -- Function-level validation via `Validate-Inputs` pattern - -## String Handling - -**HTML Escaping:** -- Custom `EscHtml` function escapes special characters for HTML generation: - ```powershell - function EscHtml([string]$s) { - return $s -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"' - } - ``` -- Applied to all user-supplied data rendered in HTML reports - -**String Interpolation:** -- Double-quoted strings with `$variable` expansion: `"URL: $url"` -- Format operator for complex strings: `"Template '{0}' saved" -f $name` -- Localization helper function `T` for i18n strings: `T "profile"`, `T "btn.save"` - -## Function Design - -**Size:** Functions range from 5-50 lines for utilities, 100+ lines for complex operations - -**Parameters:** -- Explicit type declarations required -- Optional parameters use default values -- Switch parameters for boolean flags: `[switch]$IncludeSubsites` -- Complex objects passed as reference (arrays, hashtables) - -**Return Values:** -- Functions return results or arrays: `return @()`, `return $data` -- Boolean results for validation: `return $true` / `return $false` -- PSCustomObject for structured data: `[PSCustomObject]@{ Name = ...; Value = ... }` -- Void operations often silent or logged - -## Object/Struct Patterns - -**PSCustomObject for Data:** -```powershell -[PSCustomObject]@{ - name = "value" - clientId = "value" - capturedAt = (Get-Date -Format 'dd/MM/yyyy HH:mm') - options = @{ structure = $true; permissions = $true } -} -``` - -**Hashtable for Mutable State:** -```powershell -$script:_pkl = @{ - AllSites = @() - CheckedUrls = [System.Collections.Generic.HashSet[string]]::new() - SortCol = 0 - SortAsc = $true -} -``` - -**Synchronized Hashtable for Thread-Safe State:** -```powershell -$sync = [hashtable]::Synchronized(@{ - Done = $false - Error = $null - Queue = [System.Collections.Generic.Queue[object]]::new() -}) -``` - -## Class/Type Usage - -- Minimal custom classes; primarily uses `[PSCustomObject]` -- Extensive use of .NET types: - - `[System.Windows.Forms.*]` for UI controls - - `[System.Drawing.Color]` for colors - - `[System.Drawing.Font]` for typography - - `[System.Drawing.Point]`, `[System.Drawing.Size]` for positioning - - `[System.Collections.Generic.HashSet[string]]` for efficient lookups - - `[System.Collections.Generic.Queue[object]]` for message queues - - `[System.Management.Automation.Runspaces.*]` for background execution - -## Date/Time Formatting - -**Consistent format:** `dd/MM/yyyy HH:mm` (European format) -- Used for timestamps in reports and logs -- Timestamps in logs: `HH:mm:ss` -- Storage/file metrics: `dd/MM/yyyy HH:mm` - -## Performance Patterns - -**Batch Operations:** -- Form updates wrapped in `BeginUpdate()` / `EndUpdate()` to prevent flickering -- ListView population optimized: clear, populate, sort in batch - -**Threading:** -- Long-running PnP operations execute in separate runspace -- Main UI thread communicates via synchronized hashtable + timer polling -- Async UI updates via `Timer` with `DoEvents()` for responsiveness - ---- - -*Convention analysis: 2026-04-02* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md deleted file mode 100644 index 234eb9a..0000000 --- a/.planning/codebase/INTEGRATIONS.md +++ /dev/null @@ -1,149 +0,0 @@ -# External Integrations - -**Analysis Date:** 2026-04-02 - -## APIs & External Services - -**SharePoint Online:** -- Service: Microsoft SharePoint Online (via Microsoft 365) -- What it's used for: Site management, permission auditing, file search, storage metrics, templating, bulk operations - - SDK/Client: PnP.PowerShell module - - Auth: Azure AD interactive login (ClientId required) - - Connection method: `Connect-PnPOnline -Url -Interactive -ClientId ` - - Search: SharePoint Search API using KQL (keyword query language) via `Submit-PnPSearchQuery` - -**Azure AD:** -- Service: Microsoft Entra ID (formerly Azure Active Directory) -- What it's used for: User authentication and app registration - - SDK/Client: PnP.PowerShell (handles auth flow) - - Auth: Interactive browser-based login - - App Registration: Required with delegated permissions configured - - No service principal or client secret used (interactive auth only) - -## Data Storage - -**Databases:** -- None detected - Application uses file-based storage only - -**File Storage:** -- Service: Local filesystem only -- Connection: Configured data folder for JSON files -- Client: PowerShell native file I/O -- Configuration: `Sharepoint_Settings.json` stores dataFolder path - -**Caching:** -- Service: None detected -- In-memory collections used during session (synchronized hashtables for runspace communication) - -## Authentication & Identity - -**Auth Provider:** -- Azure AD (Microsoft Entra ID) - - Implementation: Interactive browser-based OAuth 2.0 flow - - No client secrets or certificates - - User must have access to target SharePoint tenant - - App registration required with delegated permissions - -**Registration Process:** -- User creates Azure AD App Registration -- Client ID stored in profile for reuse -- Helper script available: `Register-PnPEntraIDAppForInteractiveLogin` (via PnP.PowerShell) -- Result file: Temporary JSON stored in system temp folder, user copies Client ID manually - -## Monitoring & Observability - -**Error Tracking:** -- None detected - Errors written to UI log box via `Write-Log` function -- Location: UI RichTextBox control in application - -**Logs:** -- Approach: In-app console logging - - Function: `Write-Log $Message [Color]` writes timestamped messages to UI log box - - Colors: LightGreen (default), Red (errors), Yellow (KQL queries), DarkOrange (dry-run operations) - - File location: `C:\Users\SebastienQUEROL\Documents\projets\Sharepoint\Sharepoint_ToolBox.ps1` (lines 6-17) - -## CI/CD & Deployment - -**Hosting:** -- Not applicable - Desktop application (local execution) - -**CI Pipeline:** -- None detected - -**Execution Model:** -- Direct script execution: `.\Sharepoint_Toolbox.ps1` -- No installation/setup required beyond PowerShell and PnP.PowerShell module - -## Environment Configuration - -**Required env vars:** -- None required - All configuration stored in JSON files -- User inputs via GUI: Client ID, Tenant URL, Site URL - -**Secrets location:** -- Not applicable - Interactive auth uses no stored secrets -- User manages Client ID (non-sensitive app identifier) -- Session credentials handled by Azure AD auth flow (in-memory only) - -**Configuration files:** -- `Sharepoint_Settings.json` - Data folder, language preference -- `Sharepoint_Export_profiles.json` - Saved connection profiles (Tenant URL, Client ID) -- `Sharepoint_Templates.json` - Captured site templates - -## Webhooks & Callbacks - -**Incoming:** -- None detected - -**Outgoing:** -- None detected - -## Search & Query Integration - -**SharePoint Search API:** -- Usage: File search across libraries using KQL -- Location: `Sharepoint_ToolBox.ps1` lines 4744-4773 (search query building) -- Function: `Submit-PnPSearchQuery -Query $kql` -- Pagination: Automatic via PnP.PowerShell -- Client-side filtering: Regex filters applied after results fetched -- Query example: Supports file extension, name/path patterns, creation/modification date ranges, author filters, max result limits - -## Export & Report Formats - -**Output Formats:** -- CSV: PowerShell `Export-Csv` cmdlet (UTF-8 encoding, no type info) -- HTML: Custom HTML generation with: - - Interactive tables (sorting, filtering by column) - - Collapsible sections (durable state via CSS/JS) - - Charts and metrics visualization - - Inline styling (no external CSS file) - -**Export Functions:** -- `Export-PermissionsToHTML` (line 1389) -- `Export-StorageToHTML` (line 1621) -- `Export-SearchResultsToHTML` (line 2112) -- `Export-DuplicatesToHTML` (line 2235) -- `Export-TransferVerifyToHTML` (line 2412) - -## Bulk Import Formats - -**CSV Input:** -- Bulk member add: Expects columns for site, group, user email -- Bulk site creation: Site name, alias, owner email, description -- Bulk file transfer: Source site/path, destination site/path -- Folder structure: Library name, folder path, permissions - -**Parsing:** -- PowerShell `Import-Csv` - Standard CSV parsing -- Headers used as property names - -## API Rate Limiting - -**SharePoint Online:** -- No explicit rate limiting handling detected -- Assumes PnP.PowerShell handles throttling internally -- Pagination used for large result sets (PageSize 2000 for list items) - ---- - -*Integration audit: 2026-04-02* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md deleted file mode 100644 index 06be8ee..0000000 --- a/.planning/codebase/STACK.md +++ /dev/null @@ -1,103 +0,0 @@ -# Technology Stack - -**Analysis Date:** 2026-04-02 - -## Languages - -**Primary:** -- PowerShell 5.1+ - All application logic, UI, and SharePoint integration - -## Runtime - -**Environment:** -- Windows PowerShell 5.1+ or PowerShell Core 7.0+ -- .NET Framework (built-in with PowerShell) - -**Execution Model:** -- Desktop application (WinForms GUI) -- Synchronous runspace threading for async operations without blocking UI - -## Frameworks - -**UI:** -- System.Windows.Forms (native .NET, bundled with PowerShell) -- System.Drawing (native .NET for graphics and colors) - -**SharePoint/Cloud Integration:** -- PnP.PowerShell (latest version) - All SharePoint Online API interactions -- Azure AD App Registration for authentication (required) - -**Testing:** -- No dedicated test framework detected (manual testing assumed) - -**Build/Dev:** -- No build system (single .ps1 script file executed directly) - -## Key Dependencies - -**Critical:** -- PnP.PowerShell - Required for all SharePoint Online operations - - Location: Installed via `Install-Module PnP.PowerShell` - - Used for: Site enumeration, permissions scanning, storage metrics, file search, templating, bulk operations - - Connection: Interactive authentication via Azure AD App (ClientId required) - -**Infrastructure:** -- System.Windows.Forms - Desktop UI framework -- System.Drawing - UI graphics and rendering -- Microsoft.SharePoint.Client - Underlying SharePoint CSOM (via PnP.PowerShell) - -## Configuration - -**Environment:** -- `Sharepoint_Settings.json` - User preferences (data folder location, language) -- `Sharepoint_Export_profiles.json` - Saved connection profiles (Tenant URL, Client ID) -- `Sharepoint_Templates.json` - Site structure templates (captured and reapplied) - -**Build:** -- Single executable: `Sharepoint_ToolBox.ps1` -- Launched directly: `.\Sharepoint_Toolbox.ps1` - -**Localization:** -- File: `lang/fr.json` - French translations -- Default: English (en) -- Loaded dynamically at runtime - -## Platform Requirements - -**Development:** -- Windows Operating System (WinForms is Windows-only) -- PowerShell 5.1+ -- Internet connection for Azure AD authentication -- Access to SharePoint Online tenant - -**Production:** -- Windows 10/11 (or Windows Server 2016+) -- PowerShell 5.1 minimum -- Azure AD tenant with properly configured app registration -- Network access to target SharePoint Online sites - -## Data Persistence - -**Local Storage:** -- JSON files in configurable data folder (default: `Sharepoint_Export_profiles.json`, `Sharepoint_Templates.json`, `Sharepoint_Settings.json`) -- CSV exports of reports and bulk operation results -- HTML reports with interactive UI - -**No external databases** - All storage is file-based and local - -## Authentication - -**Method:** -- Azure AD Interactive Login (user-initiated browser-based auth) -- Client ID (App Registration ID) required -- No client secrets or certificates (interactive auth flow only) -- PnP.PowerShell handles Azure AD token acquisition - -## Known Versions - -- PowerShell: 5.1 (minimum requirement stated in README) -- PnP.PowerShell: Not pinned (latest version recommended) - ---- - -*Stack analysis: 2026-04-02* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md deleted file mode 100644 index 45de744..0000000 --- a/.planning/codebase/STRUCTURE.md +++ /dev/null @@ -1,249 +0,0 @@ -# Codebase Structure - -**Analysis Date:** 2026-04-02 - -## Directory Layout - -``` -Sharepoint-Toolbox/ -├── .planning/ # GSD planning documentation -├── .git/ # Git repository -├── .gitea/ # Gitea configuration -├── .claude/ # Claude IDE configuration -├── examples/ # CSV example files for bulk operations -│ ├── bulk_add_members.csv # Template: add users to groups -│ ├── bulk_create_sites.csv # Template: create multiple sites -│ ├── bulk_transfer.csv # Template: transfer site ownership -│ └── folder_structure.csv # Template: create folder hierarchies -├── lang/ # Language/localization files -│ └── fr.json # French language translations -├── Sharepoint_ToolBox.ps1 # Main application (6408 lines) -├── Sharepoint_Settings.json # User settings (data folder, language preference) -├── Sharepoint_Export_profiles.json # Saved connection profiles (generated at runtime) -├── Sharepoint_Templates.json # Saved site templates (generated at runtime) -├── README.md # Documentation and feature overview -├── TODO.md # Future feature roadmap -└── SPToolbox-logo.png # Application logo -``` - -## Directory Purposes - -**`examples/`:** -- Purpose: Reference CSV templates for bulk operations -- Contains: CSV example files demonstrating column structure for bulk tasks -- Key files: - - `bulk_add_members.csv`: User email and group name mappings - - `bulk_create_sites.csv`: Site title, URL, type, language - - `bulk_transfer.csv`: Source site, target owner email - - `folder_structure.csv`: Folder paths to create under libraries -- Non-versioned: May contain user data after operations -- Access: Referenced by bulk operation dialogs; templates shown in UI - -**`lang/`:** -- Purpose: Store language packs for UI localization -- Contains: JSON files with key-value pairs for UI text -- Key files: - - `fr.json`: Complete French translations for all UI elements, buttons, messages -- Naming: `.json` (e.g., `en.json`, `fr.json`) -- Loading: `Load-Language` function (line 2933) reads and caches translation dict -- Integration: `T("key")` function (line 2908) looks up translations at runtime - -**`.planning/`:** -- Purpose: GSD (GitHub Sync & Deploy) planning and analysis documents -- Contains: Generated documentation for architecture, structure, conventions, concerns -- Generated: By GSD mapping tools; not manually edited -- Committed: Yes, tracked in version control - -## Key File Locations - -**Entry Points:** - -- `Sharepoint_ToolBox.ps1` (lines 1-6408): Single monolithic PowerShell script - - Execution: `.ps1` file run directly or sourced from PowerShell ISE/terminal - - Initialization: Lines 6386-6408 load settings, language, profiles, then show main form - - Exit: Triggered by form close or exception; automatic cleanup of runspaces - -- Main GUI Form instantiation: `Sharepoint_ToolBox.ps1` line 2992 - - Creates WinForms.Form object - - Registers all event handlers - - Shows dialog via `[void]$form.ShowDialog()` at line 6405 - -**Configuration:** - -- `Sharepoint_Settings.json`: User preferences - - Structure: `{ "dataFolder": "...", "lang": "en" }` - - Loaded by: `Load-Settings` (line 136) - - Saved by: `Save-Settings` (line 147) - - Auto-created: If missing, defaults to English + script root directory - -- `Sharepoint_Export_profiles.json`: Connection profiles (auto-created) - - Structure: `{ "profiles": [ { "name": "Prod", "clientId": "...", "tenantUrl": "..." }, ... ] }` - - Loaded by: `Load-Profiles` (line 57) - - Saved by: `Save-Profiles` (line 68) - - Location: Determined by `Get-ProfilesFilePath` (line 50) - same as settings - -- `Sharepoint_Templates.json`: Captured site templates (auto-created) - - Structure: `{ "templates": [ { "name": "...", "libraries": [...], "groups": [...], ... }, ... ] }` - - Loaded by: `Load-Templates` (line 484) - - Saved by: `Save-Templates` (line 495) - - Location: Same folder as profiles/settings - -**Core Logic:** - -- Permissions Report: `Sharepoint_ToolBox.ps1` lines 1784-2001 - - `Generate-PnPSitePermissionRpt` (line 1852): Main permission scanning function - - `Get-PnPWebPermission` (line 1944): Recursive site/subsite scanning - - `Get-PnPListPermission` (line 1912): Library and list enumeration - - `Get-PnPFolderPermission` (line 1882): Folder-level permission scanning - - `Get-PnPPermissions` (line 1786): Individual role assignment extraction - -- Storage Metrics: `Sharepoint_ToolBox.ps1` lines 2002-2110 - - `Get-SiteStorageMetrics` (line 2004): Main storage scan function - - Nested `Collect-FolderStorage` (line 2010): Recursive folder traversal - - Nested `Collect-WebStorage` (line 2034): Per-web storage collection - -- File Search: `Sharepoint_ToolBox.ps1` lines 2112-2233 - - Search API integration via PnP.PowerShell - - KQL (Keyword Query Language) filter construction - - Client-side regex filtering after API retrieval - -- Template Management: `Sharepoint_ToolBox.ps1` lines 475-1360 - - `Show-TemplateManager` (line 542): Template dialog - - Capture state machine in dialog event handlers - - Template persistence via JSON serialization - -- Duplicate Detection: `Sharepoint_ToolBox.ps1` lines 2235-2408 - - File mode: Search API with grouping by filename - - Folder mode: Direct library enumeration + comparison - - HTML export with grouped UI - -**Testing:** - -- No automated test framework present -- Manual testing via GUI interaction -- Examples folder (`examples/`) provides test data templates - -**Localization:** - -- `lang/fr.json`: French translations - - Format: JSON object with `"_name"` and `"_code"` metadata + translation keys - - Loading: `Load-Language` (line 2933) parses JSON into `$script:LangDict` - - Usage: `T("key")` replaces hardcoded English strings with translations - - UI Update: `Update-UILanguage` (line 2951) updates all registered controls - -## Naming Conventions - -**Files:** - -- Main application: `Sharepoint_ToolBox.ps1` (PascalCase with underscore separator) -- Settings/data: `Sharepoint_.json` (e.g., `Sharepoint_Settings.json`) -- Generated exports: `__.` or `__.` - - Permissions: `Permissions__.csv/html` - - Storage: `Storage__.csv/html` - - Search: `FileSearch_.csv/html` - - Duplicates: `Duplicates__.csv/html` -- Language files: `.json` (lowercase, e.g., `fr.json`) -- Example files: `_.csv` (lowercase with underscore, e.g., `bulk_add_members.csv`) - -**PowerShell Functions:** - -- Public functions: `Verb-Noun` naming (e.g., `Generate-PnPSitePermissionRpt`, `Get-SiteStorageMetrics`) -- Private/internal functions: Prefixed with `_` or grouped in regions (e.g., `_Pkl-Sort`, `_Pkl-Repopulate`) -- Event handlers: Declared inline in `.Add_Click` or `.Add_TextChanged` blocks; not named separately -- Nested functions (inside others): CamelCase with parent context (e.g., `Collect-FolderStorage` inside `Get-SiteStorageMetrics`) - -**Variables:** - -- Script-scope state: `$script:` (e.g., `$script:AllPermissions`, `$script:DataFolder`) -- Local function scope: camelCase (e.g., `$result`, `$dlg`, `$lists`) -- Control references: descriptive with type suffix (e.g., `$txtClientId`, `$btnPermRun`, `$cboProfile`) -- Control variables stored in script scope: Prefixed `$script:` for access across event handlers -- Temporary arrays: `$` (e.g., `$sites`, `$folders`, `$results`) - -**Types:** - -- Region markers: `#region ===== =====` (e.g., `#region ===== GUI =====`) -- Comments: Double hash for section comments (e.g., `# ── Label helper ──`) - -**Exports:** - -- HTML: Class names like `permission-table`, `storage-tree`, `duplicate-group` -- CSV: Column headers match object property names (e.g., `Title`, `URL`, `Permissions`) - -## Where to Add New Code - -**New Feature:** - -1. Create a new region section in `Sharepoint_ToolBox.ps1`: - ```powershell - #region ===== [Feature Name] ===== - function [Verb]-[Feature](...) { ... } - #endregion - ``` - -2. Primary code locations: - - Core logic: After line 2408 (after duplicates region, before Transfer region) - - PnP interaction: Own `#region` mirroring storage/permissions pattern - - HTML export helper: Create function like `Export-[Feature]ToHTML` in dedicated region - -3. Add UI tab or button: - - Create new TabPage in `$tabs` (around line 3113+) - - Register event handler for execution button in Event Handlers region (line 4068+) - - Add label in French translation file (`lang/fr.json`) - -4. Add menu item if needed: - - Modify MenuStrip construction around line 3001-3027 - - Register handler in Event Handlers region - -5. Persist settings: - - Add properties to `Sharepoint_Settings.json` structure - - Update `Load-Settings` (line 136) to include new fields - - Update `Save-Settings` (line 147) to serialize new fields - -**New Component/Module:** - -- Keep as internal functions (no separate files) -- If complexity exceeds 500 lines, consider refactoring into regions -- Pattern: All code stays in single `Sharepoint_ToolBox.ps1` file -- Dependencies: Use script-scope variables for shared state - -**Utilities:** - -- Shared helpers: `Shared Helpers` region (line 4-46) - - Add new helper function here if used by multiple features - - Examples: `Write-Log`, `Format-Bytes`, `EscHtml`, `Validate-Inputs` - -- UI control factories: Lines 3119-3146 - - Add new `New-` helper for frequently used UI patterns - - Examples: `New-Group`, `New-Check`, `New-Radio`, `New-ActionBtn` - -- Internationalization: `Internationalization` region (line 2732-2989) - - Add new translation keys to `lang/fr.json` - - Update `T()` function if new resolution logic needed - -## Special Directories - -**`examples/`:** -- Purpose: CSV templates for user reference -- Generated: No; committed as examples -- Committed: Yes, tracked in version control -- Accessed by: Bulk operation dialogs (not directly imported by code; users download manually) -- Content: Non-executable; user-facing documentation - -**`.planning/`:** -- Purpose: GSD-generated codebase analysis and planning documents -- Generated: Yes; created by GSD mapping tools during `/gsd:map-codebase` -- Committed: Yes; documents are version-controlled -- Accessed by: `/gsd:plan-phase` and `/gsd:execute-phase` commands for context -- Content: Markdown documents describing architecture, structure, conventions, concerns, stack, integrations - -**Generated Files (Runtime):** - -Files created at runtime, not part of initial repository: -- `Sharepoint_Export_profiles.json`: User-created connection profiles -- `Sharepoint_Templates.json`: User-captured site templates -- Export reports: `*_.(csv|html)` files in output folder (default: script root or user-selected) - ---- - -*Structure analysis: 2026-04-02* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md deleted file mode 100644 index 19ccf1f..0000000 --- a/.planning/codebase/TESTING.md +++ /dev/null @@ -1,256 +0,0 @@ -# Testing Patterns - -**Analysis Date:** 2026-04-02 - -## Test Framework - -**Status:** No automated testing framework detected - -**Infrastructure:** Not applicable -- No test runner (Jest, Vitest, Pester) -- No test configuration files -- No test suite in codebase -- No CI/CD pipeline test stage configured - -## Testing Approach - -**Current Testing Model:** Manual testing via GUI - -**Test Methods:** -- **GUI Testing:** All functionality tested through WinForms UI - - Manual interaction with controls and dialogs - - Visual verification of results in generated reports - - Log output observation in RichTextBox -- **Report Validation:** HTML and CSV exports manually reviewed for correctness -- **API Integration:** Manual testing of PnP.PowerShell operations against live SharePoint tenant -- **Regression Testing:** Ad-hoc manual verification of features after changes - -## Code Organization for Testing - -**Testability Patterns:** Limited - -The monolithic single-file architecture (`Sharepoint_ToolBox.ps1` at 6408 lines) makes isolated unit testing challenging. Key observations: - -**Tight Coupling to UI:** -- Core business logic embedded in event handlers -- Heavy reliance on global `$script:` scope for state -- Example: `Load-Profiles` reads from `Get-ProfilesFilePath`, which is file-system dependent -- Site picker functionality (`Show-SitePicker`) spawns background runspace but depends on form being instantiated - -**Hard Dependencies:** -- PnP.PowerShell module imported dynamically in background runspace blocks -- File system access (profiles, templates, settings) not abstracted -- SharePoint connection state implicit in PnP connection context - -**Areas with Better Isolation:** -- Pure utility functions like `Format-Bytes`, `EscHtml` could be unit tested -- Data transformation functions like `Merge-PermissionRows` accept input arrays and return structured output -- HTML generation in `Export-PermissionsToHTML` and `Export-StorageToHTML` could be tested against expected markup - -## Background Runspace Pattern - -**Async Execution Model:** -Most long-running operations execute in separate PowerShell runspace to prevent UI blocking: - -```powershell -# 1. Create synchronized hashtable for communication -$sync = [hashtable]::Synchronized(@{ - Done = $false - Error = $null - Result = $null - Queue = [System.Collections.Generic.Queue[object]]::new() -}) - -# 2. Define background script block (has access to passed parameters only) -$bgScript = { - param($Url, $ClientId, $Sync) - try { - Import-Module PnP.PowerShell -ErrorAction Stop - Connect-PnPOnline -Url $Url -Interactive -ClientId $ClientId - # Perform work - $Sync.Result = $data - } catch { - $Sync.Error = $_.Exception.Message - } finally { - $Sync.Done = $true - } -} - -# 3. Launch in runspace -$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() -$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open() -$ps = [System.Management.Automation.PowerShell]::Create() -$ps.Runspace = $rs -[void]$ps.AddScript($bgScript) -[void]$ps.AddArgument($url) -$hnd = $ps.BeginInvoke() - -# 4. Poll completion with timer -$tmr = New-Object System.Windows.Forms.Timer -$tmr.Interval = 300 -$tmr.Add_Tick({ - if ($sync.Done) { - [void]$ps.EndInvoke($hnd) - $rs.Close(); $rs.Dispose() - # Update UI with $sync.Result - } -}) -$tmr.Start() -``` - -**Used for:** -- Site picker loading: `Show-SitePicker` (lines 212-471) -- Template capture: Background job in template manager (lines 900+) -- Site creation: Background job in bulk creation (lines 1134+) -- Permission/storage export: Operations triggered from event handlers -- File search: Background search execution - -## Message Queue Pattern - -**For logging from background runspaces:** - -```powershell -# Background function enqueues messages -function BgLog([string]$m, [string]$c="LightGreen") { - $Sync.Queue.Enqueue([PSCustomObject]@{ Text=$m; Color=$c }) -} - -# Main thread timer dequeues and displays -$tmr.Add_Tick({ - while ($sync.Queue.Count -gt 0) { - $msg = $sync.Queue.Dequeue() - _Tpl-Log -Box $textBox -Msg $msg.Text -Color $msg.Color - } -}) -``` - -**Rationale:** Avoids cross-thread UI access violations by queueing messages from worker thread. - -## Common Testing Patterns in Code - -**Null/Existence Checks:** -```powershell -# Before using objects -if ($script:LogBox -and !$script:LogBox.IsDisposed) { ... } -if ($data -and $data.Count -gt 0) { ... } -if ([string]::IsNullOrWhiteSpace($value)) { return $false } -``` - -**Error Logging in Loops:** -```powershell -# Catch errors in data processing, log, continue -foreach ($item in $items) { - try { - # Process item - } catch { - BgLog " Skipped: $($_.Exception.Message)" "DarkGray" - } -} -``` - -**Validation Before Operations:** -```powershell -function Validate-Inputs { - if ([string]::IsNullOrWhiteSpace($script:txtClientId.Text)) { - [System.Windows.Forms.MessageBox]::Show(...) - return $false - } - return $true -} - -# Called before starting long operations -if (-not (Validate-Inputs)) { return } -``` - -## Data Flow Testing Approach - -**For Feature Development:** - -1. **Manual Test Cases** (observed pattern, not formalized): - - Permissions Export: - - Select site with multiple libraries - - Choose CSV format - - Verify CSV contains all libraries and permissions - - Test HTML format in browser for interactivity - - - Storage Metrics: - - Run with `PerLibrary` flag - - Verify folder hierarchy is captured - - Test recursive subsite inclusion - - Validate byte calculations - - - Template Capture/Apply: - - Capture from source site - - Verify JSON structure - - Create new site from template - - Verify structure, permissions, settings applied - - - File Search: - - Test regex patterns - - Verify date filtering - - Test large result sets (pagination) - - Check CSV/HTML output - -2. **Visual Verification:** - - Log output reviewed in RichTextBox for progress - - Generated HTML reports tested in multiple browsers - - CSV files opened in Excel for format verification - -## Fragility Points & Testing Considerations - -**PnP Connection Management:** -- No connection pooling; each operation may create new connection -- Interactive auth prompt appears per runspace -- **Risk:** Auth failures not consistently handled -- **Testing Need:** Mock PnP module or use test tenant - -**HTML Generation:** -- String concatenation for large HTML documents (lines 1475+) -- Inline CSS for styling -- JavaScript for interactivity -- **Risk:** Complex HTML fragments prone to markup errors -- **Testing Need:** Validate HTML structure and JavaScript functionality - -**JSON Persistence:** -- Profiles, templates, settings stored in JSON -- ConvertTo-Json/-From-Json without depth specification can truncate -- **Risk:** Nested objects may not round-trip correctly -- **Testing Need:** Validate all object types persist/restore - -**Background Runspace Cleanup:** -- Runspace and PowerShell objects must be disposed -- Timer must be stopped and disposed -- **Risk:** Resource leaks if exception occurs before cleanup -- **Testing Need:** Verify cleanup in error paths - -## Suggested Testing Improvements - -**Unit Testing:** -1. Extract pure functions (no UI/file system dependencies) - - `Format-Bytes`, `EscHtml`, `Merge-PermissionRows` - - HTML generation functions - -2. Use Pester framework for PowerShell unit tests: - ```powershell - Describe "Format-Bytes" { - It "Formats bytes to GB" { - Format-Bytes (1GB * 2) | Should -Be "2 GB" - } - } - ``` - -3. Mock file system and PnP operations for integration tests - -**Integration Testing:** -1. Use test SharePoint tenant for functional testing -2. Automate report generation and validation -3. Script common user workflows - -**Regression Testing:** -1. Maintain test suite of sites with known structures -2. Generate reports, compare outputs -3. Run before major releases - ---- - -*Testing analysis: 2026-04-02* diff --git a/.planning/config.json b/.planning/config.json deleted file mode 100644 index 578f403..0000000 --- a/.planning/config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "mode": "yolo", - "granularity": "standard", - "parallelization": true, - "commit_docs": true, - "model_profile": "balanced", - "workflow": { - "research": true, - "plan_check": true, - "verifier": true, - "nyquist_validation": true, - "_auto_chain_active": false - } -} \ No newline at end of file diff --git a/.planning/debug/site-picker-parsing-error.md b/.planning/debug/site-picker-parsing-error.md deleted file mode 100644 index a081b8b..0000000 --- a/.planning/debug/site-picker-parsing-error.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -status: awaiting_human_verify -trigger: "SitePickerDialog shows 'Must specify valid information for parsing in the string' error when trying to load sites after a successful tenant connection." -created: 2026-04-07T00:00:00Z -updated: 2026-04-07T00:00:00Z ---- - -## Current Focus - -hypothesis: ROOT CAUSE CONFIRMED — two bugs in SiteListService.GetSitesAsync -test: code reading confirmed via PnP source -expecting: fixing both issues will resolve the error -next_action: apply fix to SiteListService.cs - -## Symptoms - -expected: After connecting to a SharePoint tenant (https://contoso.sharepoint.com format), clicking "Select Sites" opens SitePickerDialog and loads the list of tenant sites. -actual: SitePickerDialog opens but shows error "Must specify valid information for parsing in the string" instead of loading sites. -errors: "Must specify valid information for parsing in the string" — this is an ArgumentException thrown by CSOM when it tries to parse an empty string as a site URL cursor -reproduction: 1) Launch app 2) Add profile with valid tenant URL 3) Connect 4) Authenticate 5) Click Select Sites 6) Error appears in StatusText -started: First time testing this flow after Phase 6 wiring was added. - -## Eliminated - -- hypothesis: Error comes from PnP's AuthenticationManager.GetContextAsync URI parsing - evidence: GetContextAsync line 1090 does new Uri(siteUrl) which is valid for "https://contoso-admin.sharepoint.com" - timestamp: 2026-04-07 - -- hypothesis: Error from MSAL constructing auth URL with empty component - evidence: MSAL uses organizations authority or tenant-specific, both valid; no empty strings involved - timestamp: 2026-04-07 - -- hypothesis: UriFormatException from new Uri("") in our own code - evidence: No Uri.Parse or new Uri() calls in SiteListService or SessionManager - timestamp: 2026-04-07 - -## Evidence - -- timestamp: 2026-04-07 - checked: PnP Framework 1.18.0 GetContextAsync source (line 1090) - found: Calls new Uri(siteUrl) — valid for admin URL - implication: Error not from GetContextAsync itself - -- timestamp: 2026-04-07 - checked: PnP TenantExtensions.GetSiteCollections source - found: Uses GetSitePropertiesFromSharePointByFilters with StartIndex = null (for first page); OLD commented-out approach used GetSitePropertiesFromSharePoint(null, includeDetail) — note: null, not "" - implication: SiteListService passes "" which is wrong — should be null for first page - -- timestamp: 2026-04-07 - checked: Error message "Must specify valid information for parsing in the string" - found: This is ArgumentException thrown by Enum.Parse or string cursor parsing when given "" (empty string); CSOM's GetSitePropertiesFromSharePoint internally parses the startIndex string as a URL/cursor; passing "" triggers parse failure - implication: Direct cause of exception confirmed - -- timestamp: 2026-04-07 - checked: How PnP creates admin context from regular context - found: PnP uses clientContext.Clone(adminSiteUrl) — clones existing authenticated context to admin URL without triggering new auth flow - implication: SiteListService creates a SECOND AuthenticationManager and triggers second interactive login unnecessarily; should use Clone instead - -## Resolution - -root_cause: | - SiteListService.GetSitesAsync has two bugs: - - BUG 1 (direct cause of error): Line 50 calls tenant.GetSitePropertiesFromSharePoint("", true) - with empty string "". CSOM expects null for the first page (no previous cursor), not "". - Passing "" causes CSOM to attempt parsing it as a URL cursor, throwing - ArgumentException: "Must specify valid information for parsing in the string." - - BUG 2 (design problem): GetSitesAsync creates a separate TenantProfile for the admin URL - and calls SessionManager.GetOrCreateContextAsync(adminProfile) which creates a NEW - AuthenticationManager with interactive login. This triggers a SECOND browser auth flow - just to access the admin URL. The correct approach is to clone the existing authenticated - context to the admin URL using clientContext.Clone(adminUrl), which reuses the same tokens. - -fix: | - 1. Replace GetOrCreateContextAsync(adminProfile) with GetOrCreateContextAsync(profile) to - get the regular context, then clone it to the admin URL. - 2. Replace GetSitePropertiesFromSharePointByFilters with proper pagination (StartIndex=null). - - The admin URL context is obtained via: adminCtx = ctx.Clone(adminUrl) - The site listing uses: GetSitePropertiesFromSharePointByFilters with proper filter object. - -verification: | - Build succeeds (0 errors). 144 tests pass, 0 failures. - Fix addresses both root causes: - 1. No longer calls GetOrCreateContextAsync with admin profile — uses Clone() instead - 2. Uses GetSitePropertiesFromSharePointByFilters (modern API) instead of GetSitePropertiesFromSharePoint("") -files_changed: - - SharepointToolbox/Services/SiteListService.cs diff --git a/.planning/milestones/v1.0-MILESTONE-AUDIT.md b/.planning/milestones/v1.0-MILESTONE-AUDIT.md deleted file mode 100644 index 62ad64c..0000000 --- a/.planning/milestones/v1.0-MILESTONE-AUDIT.md +++ /dev/null @@ -1,155 +0,0 @@ -# Milestone Audit: SharePoint Toolbox v2 — v1 Release - -**Audited:** 2026-04-07 -**Milestone:** v1 (5 phases, 42 requirements) -**Verdict:** PASSED — all requirements satisfied, all phases integrated, build and tests green - ---- - -## Phase Verification Summary - -| Phase | Status | Score | Verification | -|-------|--------|-------|-------------| -| 01 — Foundation | PASSED | 11/11 | 01-VERIFICATION.md | -| 02 — Permissions | HUMAN_NEEDED | 7/7 automated | 02-VERIFICATION.md (2 human items pending) | -| 03 — Storage & File Ops | **MISSING** | No VERIFICATION.md | Summaries exist for all 8 plans; integration checker confirmed all wiring | -| 04 — Bulk Ops & Provisioning | HUMAN_NEEDED | 12/12 automated | 04-VERIFICATION.md (7 human items pending) | -| 05 — Distribution & Hardening | HUMAN_NEEDED | 6/6 automated | 05-VERIFICATION.md (2 human items pending) | - -### Gap: Phase 03 Missing Verification - -Phase 03 has no `03-VERIFICATION.md` file. All 8 plan summaries exist and confirm code was delivered. The integration checker independently verified: -- All 3 Phase 3 service interfaces (IStorageService, ISearchService, IDuplicatesService) registered in DI -- All 5 export services registered and wired to ViewModels -- All 4 Phase 3 tabs (Storage, Search, Duplicates + exports) wired in MainWindow -- 13 Phase 3 requirements (STOR-01–05, SRCH-01–04, DUPL-01–03) covered - -**Recommendation:** Run a retroactive phase verification for Phase 03 or accept integration checker evidence as sufficient. - ---- - -## Requirements Coverage - -All 42 v1 requirements are marked complete in REQUIREMENTS.md with phase traceability: - -| Category | IDs | Count | Status | -|----------|-----|-------|--------| -| Foundation | FOUND-01 to FOUND-12 | 12 | All SATISFIED | -| Permissions | PERM-01 to PERM-07 | 7 | All SATISFIED | -| Storage | STOR-01 to STOR-05 | 5 | All SATISFIED | -| File Search | SRCH-01 to SRCH-04 | 4 | All SATISFIED | -| Duplicates | DUPL-01 to DUPL-03 | 3 | All SATISFIED | -| Templates | TMPL-01 to TMPL-04 | 4 | All SATISFIED | -| Folder Structure | FOLD-01 to FOLD-02 | 2 | All SATISFIED | -| Bulk Operations | BULK-01 to BULK-05 | 5 | All SATISFIED | -| **Total** | | **42** | **42/42 mapped and complete** | - -**Orphaned requirements:** None -**Unmapped requirements:** None - ---- - -## Cross-Phase Integration - -Integration checker ran full verification. Results: - -| Check | Status | -|-------|--------| -| DI wiring (all 5 phases) | PASS — all services registered in App.xaml.cs | -| MainWindow tabs (10 tabs) | PASS — all declared and wired from DI | -| FeatureViewModelBase inheritance (10 VMs) | PASS | -| SessionManager usage (9 ViewModels + SiteListService) | PASS | -| ExecuteQueryRetryHelper (9 CSOM services, 40+ call sites) | PASS | -| SharePointPaginationHelper (2 services using list enumeration) | PASS | -| TranslationSource localization (15 XAML files, 170 bindings) | PASS | -| TenantSwitchedMessage propagation | PASS | -| Export chain completeness (all features) | PASS | -| Build | PASS — 0 warnings, 0 errors | -| Tests | PASS — 134 passed, 22 skipped (live CSOM), 0 failed | -| EN/FR key parity | PASS — 199/199 keys | - -**Orphaned code:** `FeatureTabBase.xaml` — Phase 1 placeholder, now superseded by full tab views. Harmless dead code. - ---- - -## Tech Debt & Deferred Items - -### From Phase Verifications - -| Item | Source | Severity | Description | -|------|--------|----------|-------------| -| Hardcoded export button text | Phase 2 | Info | `PermissionsView.xaml` uses `Content="Export CSV"` / `"Export HTML"` instead of `rad.csv.perms` / `rad.html.perms` localization keys. French users see English button labels. | -| Missing Designer.cs property | Phase 2 | Info | `Strings.Designer.cs` lacks `tab_permissions` typed accessor. Runtime binding via `TranslationSource` works fine. | -| No invalid-row highlighting | Phase 4 | Warning | `BulkMembersView.xaml`, `BulkSitesView.xaml`, `FolderStructureView.xaml` show IsValid as text column but lack `RowStyle` + `DataTrigger` for visual red highlighting on invalid rows. | -| FeatureTabBase dead code | Phase 1→all | Info | `Views/Controls/FeatureTabBase.xaml` is no longer imported by any tab view after all phases replaced stubs. | -| Cancel test locale mismatch | Phase 3 (03-08) | Info | `FeatureViewModelBaseTests.CancelCommand_DuringOperation_SetsStatusMessageToCancelled` asserts `.Contains("cancel")` but app returns French string "Opération annulée". Pre-existing; deferred. | - -### Deferred v2 Requirements - -These are explicitly out of scope for v1 and tracked in REQUIREMENTS.md: -- UACC-01/02: User access audit across sites -- SIMP-01/02/03: Simplified plain-language permission reports -- VIZZ-01/02/03: Storage metrics graphs (pie/bar chart) - ---- - -## Human Verification Backlog - -11 items across 3 phases require human confirmation (runtime UI/locale checks that cannot be automated): - -### Phase 2 (2 items) -1. Full Permissions tab UI visual checkpoint (layout, disabled states, French locale) -2. Export button localization decision (accept hardcoded English or bind to resx keys) - -### Phase 4 (7 items) -1. Application launches with all 10 tabs visible -2. Bulk Members — Load Example populates DataGrid with 7 rows -3. Bulk Sites — semicolon CSV auto-detection works -4. Invalid row display in DataGrid (IsValid=False, Errors column) -5. Confirmation dialog appears before bulk operations -6. Transfer tab — two-step browse flow (SitePickerDialog → FolderBrowserDialog) -7. Templates tab — 5 capture checkboxes visible and checked by default - -### Phase 5 (2 items) -1. Clean-machine EXE launch (no .NET runtime installed) -2. French locale runtime rendering (diacritics display correctly in all tabs) - ---- - -## Build & Test Summary - -| Metric | Value | -|--------|-------| -| Build | 0 errors, 0 warnings | -| Tests passed | 134 | -| Tests skipped | 22 (live CSOM — expected) | -| Tests failed | 0 | -| EN locale keys | 199 | -| FR locale keys | 199 | -| Published EXE | 200.9 MB self-contained | -| Phases complete | 5/5 | -| Requirements satisfied | 42/42 | - ---- - -## Verdict - -**PASSED** — The milestone has achieved its definition of done: - -1. All 42 v1 requirements are implemented with real code and verified by phase-level checks -2. All cross-phase integration points are wired (DI, messaging, shared infrastructure) -3. Build compiles cleanly with zero warnings -4. 134 automated tests pass with zero failures -5. Self-contained 200.9 MB EXE produced successfully -6. Full EN/FR locale parity (199 keys each) - -**Remaining actions before shipping:** -- [ ] Complete 11 human verification items (UI visual checks, clean-machine launch) -- [ ] Decide on Phase 03 retroactive verification (or accept integration check as sufficient) -- [ ] Address 3 Warning-level tech debt items (invalid-row highlighting in bulk DataGrids) -- [ ] Optionally clean up FeatureTabBase dead code and fix cancel test locale mismatch - ---- - -*Audited: 2026-04-07* -*Auditor: Claude (milestone audit)* diff --git a/.planning/milestones/v1.0-REQUIREMENTS.md b/.planning/milestones/v1.0-REQUIREMENTS.md deleted file mode 100644 index af80179..0000000 --- a/.planning/milestones/v1.0-REQUIREMENTS.md +++ /dev/null @@ -1,177 +0,0 @@ -# Requirements Archive: v1.0 MVP - -**Archived:** 2026-04-07 -**Status:** SHIPPED - -For current requirements, see `.planning/REQUIREMENTS.md`. - ---- - -# Requirements: SharePoint Toolbox v2 - -**Defined:** 2026-04-02 -**Core Value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application. - -## v1 Requirements - -Requirements for initial release. Each maps to roadmap phases. - -### Foundation - -- [x] **FOUND-01**: Application built with C#/WPF (.NET 10 LTS) using MVVM architecture -- [x] **FOUND-02**: Multi-tenant profile registry — user can create, rename, delete, and switch between tenant profiles (tenant URL, client ID, display name) -- [x] **FOUND-03**: Multi-tenant session caching — user stays authenticated across tenant switches without re-logging in (MSAL token cache per tenant) -- [x] **FOUND-04**: Interactive Azure AD OAuth login via browser — no client secrets or certificates stored -- [x] **FOUND-05**: All long-running operations report progress to the UI in real-time -- [x] **FOUND-06**: User can cancel any long-running operation mid-execution -- [x] **FOUND-07**: All errors surface to the user with actionable messages — no silent failures -- [x] **FOUND-08**: Structured logging for diagnostics (Serilog or equivalent) -- [x] **FOUND-09**: Localization system supporting English and French with dynamic language switching -- [x] **FOUND-10**: JSON-based local storage for profiles, settings, and templates (compatible with current app's format for migration) -- [x] **FOUND-11**: Self-contained single EXE distribution — no .NET runtime dependency for end users -- [x] **FOUND-12**: Configurable data output folder for exports - -### Permissions - -- [x] **PERM-01**: User can scan permissions on a single SharePoint site with configurable depth -- [x] **PERM-02**: User can scan permissions across multiple selected sites in one operation -- [x] **PERM-03**: Permissions scan includes owners, members, guests, external users, and broken inheritance -- [x] **PERM-04**: User can choose to include or exclude inherited permissions -- [x] **PERM-05**: User can export permissions report to CSV (raw data) -- [x] **PERM-06**: User can export permissions report to interactive HTML (sortable, filterable, groupable by user) -- [x] **PERM-07**: SharePoint 5,000-item list view threshold handled via pagination — no silent failures on large libraries - -### Storage - -- [x] **STOR-01**: User can view storage consumption per library on a site -- [x] **STOR-02**: User can view storage consumption per site with configurable folder depth -- [x] **STOR-03**: Storage metrics include total size, version size, item count, and last modified date -- [x] **STOR-04**: User can export storage metrics to CSV -- [x] **STOR-05**: User can export storage metrics to interactive HTML with collapsible tree view - -### File Search - -- [x] **SRCH-01**: User can search files across sites using multiple criteria (extension, name/regex, dates, creator, editor) -- [x] **SRCH-02**: User can configure maximum search results (up to 50,000) -- [x] **SRCH-03**: User can export search results to CSV -- [x] **SRCH-04**: User can export search results to interactive HTML (sortable, filterable) - -### Duplicate Detection - -- [x] **DUPL-01**: User can scan for duplicate files by name, size, creation date, modification date -- [x] **DUPL-02**: User can scan for duplicate folders by name, subfolder count, file count -- [x] **DUPL-03**: User can export duplicate report to HTML with grouped display and visual indicators - -### Site Templates - -- [x] **TMPL-01**: User can capture site structure (libraries, folders, permission groups, logo, settings) as a template -- [x] **TMPL-02**: User can apply template to create new Communication or Teams site -- [x] **TMPL-03**: Templates persist locally as JSON -- [x] **TMPL-04**: User can manage templates (create, rename, delete) - -### Folder Structure - -- [x] **FOLD-01**: User can create folder structures on a site from a CSV template -- [x] **FOLD-02**: Example CSV templates provided for common structures - -### Bulk Operations - -- [x] **BULK-01**: User can transfer files and folders between sites with progress tracking -- [x] **BULK-02**: User can add members to groups in bulk from CSV -- [x] **BULK-03**: User can create multiple sites in bulk from CSV -- [x] **BULK-04**: All bulk operations support cancellation mid-execution -- [x] **BULK-05**: Bulk operation errors are reported per-item (not silently skipped) - -## v2 Requirements - -Deferred to after v1 parity is confirmed. New features from project goals. - -### User Access Audit - -- **UACC-01**: User can export all SharePoint/Teams accesses a specific user has across selected sites -- **UACC-02**: Export includes direct assignments, group memberships, and inherited access - -### Simplified Permissions - -- **SIMP-01**: User can toggle plain-language permission labels (e.g., "Can edit files" instead of "Contribute") -- **SIMP-02**: Permissions report includes summary counts and color coding for untrained readers -- **SIMP-03**: Configurable detail level (simple/detailed) for reports - -### Storage Visualization - -- **VIZZ-01**: Storage Metrics tab includes a graph showing space by file type -- **VIZZ-02**: User can toggle between pie/donut chart and bar chart views -- **VIZZ-03**: Graph updates when storage scan completes - -## Out of Scope - -| Feature | Reason | -|---------|--------| -| Cross-platform (Mac/Linux) | WPF is Windows-only; not justified for current user base | -| Real-time monitoring / alerts | Requires background service, webhooks — turns desktop tool into a service | -| Automated remediation (auto-revoke) | Liability risk; one wrong rule destroys client access | -| SQLite / database storage | Breaks single-EXE distribution; JSON sufficient | -| Cloud sync / shared profiles | Requires server infrastructure — out of scope for local tool | -| AI-powered recommendations | Competes with Microsoft's own Copilot roadmap | -| Content migration between tenants | Separate product category (ShareGate territory) | -| Mobile app | Desktop admin tool | -| OAuth with client secrets/certificates | Interactive login only — no stored credentials | -| Version history management | Deep separate problem; surface totals in storage metrics only | - -## Traceability - -Which phases cover which requirements. Updated during roadmap creation. - -| Requirement | Phase | Status | -|-------------|-------|--------| -| FOUND-01 | Phase 1 | Complete | -| FOUND-02 | Phase 1 | Complete | -| FOUND-03 | Phase 1 | Complete | -| FOUND-04 | Phase 1 | Complete | -| FOUND-05 | Phase 1 | Complete | -| FOUND-06 | Phase 1 | Complete | -| FOUND-07 | Phase 1 | Complete | -| FOUND-08 | Phase 1 | Complete | -| FOUND-09 | Phase 1 | Complete | -| FOUND-10 | Phase 1 | Complete | -| FOUND-11 | Phase 5 | Complete | -| FOUND-12 | Phase 1 | Complete | -| PERM-01 | Phase 2 | Complete | -| PERM-02 | Phase 2 | Complete | -| PERM-03 | Phase 2 | Complete | -| PERM-04 | Phase 2 | Complete | -| PERM-05 | Phase 2 | Complete | -| PERM-06 | Phase 2 | Complete | -| PERM-07 | Phase 2 | Complete | -| STOR-01 | Phase 3 | Complete | -| STOR-02 | Phase 3 | Complete | -| STOR-03 | Phase 3 | Complete | -| STOR-04 | Phase 3 | Complete | -| STOR-05 | Phase 3 | Complete | -| SRCH-01 | Phase 3 | Complete | -| SRCH-02 | Phase 3 | Complete | -| SRCH-03 | Phase 3 | Complete | -| SRCH-04 | Phase 3 | Complete | -| DUPL-01 | Phase 3 | Complete | -| DUPL-02 | Phase 3 | Complete | -| DUPL-03 | Phase 3 | Complete | -| TMPL-01 | Phase 4 | Complete | -| TMPL-02 | Phase 4 | Complete | -| TMPL-03 | Phase 4 | Complete | -| TMPL-04 | Phase 4 | Complete | -| FOLD-01 | Phase 4 | Complete | -| FOLD-02 | Phase 4 | Complete | -| BULK-01 | Phase 4 | Complete | -| BULK-02 | Phase 4 | Complete | -| BULK-03 | Phase 4 | Complete | -| BULK-04 | Phase 4 | Complete | -| BULK-05 | Phase 4 | Complete | - -**Coverage:** -- v1 requirements: 42 total -- Mapped to phases: 42 -- Unmapped: 0 - ---- -*Requirements defined: 2026-04-02* -*Last updated: 2026-04-02 after roadmap creation — all 42 v1 requirements mapped* diff --git a/.planning/milestones/v1.0-ROADMAP.md b/.planning/milestones/v1.0-ROADMAP.md deleted file mode 100644 index c7a76c4..0000000 --- a/.planning/milestones/v1.0-ROADMAP.md +++ /dev/null @@ -1,147 +0,0 @@ -# Roadmap: SharePoint Toolbox v2 - -## Overview - -A full C#/WPF rewrite of a 6,400-line PowerShell-based SharePoint Online administration tool. The -project delivers a self-contained Windows desktop application that lets MSP administrators audit -and manage permissions, storage, and site provisioning across multiple client tenants from a single -tool. Foundation infrastructure (multi-tenant auth, async patterns, error handling, DI) must be -solid before any feature work begins — all 10 identified pitfalls in the existing codebase map -entirely to Phase 1. Subsequent phases deliver complete, verifiable feature areas in dependency -order: permissions first (validates auth and pagination), then storage and search (reuse those -patterns), then bulk and provisioning operations (highest write-risk features last), then -hardening and packaging. - -## Phases - -**Phase Numbering:** -- Integer phases (1, 2, 3): Planned milestone work -- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) - -Decimal phases appear between their surrounding integers in numeric order. - -- [x] **Phase 1: Foundation** - WPF shell, multi-tenant auth, DI, async patterns, error handling, logging, localization, JSON persistence (completed 2026-04-02) -- [x] **Phase 2: Permissions** - Permissions scan (single and multi-site), CSV and HTML report export -- [x] **Phase 3: Storage and File Operations** - Storage metrics, file search, and duplicate detection (completed 2026-04-02) -- [x] **Phase 4: Bulk Operations and Provisioning** - Bulk member/site/transfer operations, site templates, folder structure provisioning (completed 2026-04-03) -- [x] **Phase 5: Distribution and Hardening** - Self-contained EXE packaging, end-to-end validation, FR locale completeness (completed 2026-04-03) - -## Phase Details - -### Phase 1: Foundation -**Goal**: The application shell runs, users can authenticate to multiple tenants and switch between them without re-logging in, all long-running operations are cancellable and report progress, all errors surface visibly, and the infrastructure patterns that prevent the existing app's 10 known pitfalls are in place before any feature work begins. -**Depends on**: Nothing (first phase) -**Requirements**: FOUND-01, FOUND-02, FOUND-03, FOUND-04, FOUND-05, FOUND-06, FOUND-07, FOUND-08, FOUND-09, FOUND-10, FOUND-12 -**Success Criteria** (what must be TRUE): - 1. User can create, rename, delete, and switch between tenant profiles via the UI — each profile stores tenant URL, client ID, and display name in a JSON file - 2. User can authenticate to a tenant via interactive browser login and the session persists across tenant switches without re-entering credentials (MSAL token cache per tenant) - 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**: 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. -**Depends on**: Phase 1 -**Requirements**: PERM-01, PERM-02, PERM-03, PERM-04, PERM-05, PERM-06, PERM-07 -**Success Criteria** (what must be TRUE): - 1. User can select one site or multiple sites and run a permissions scan that returns owners, members, guests, external users, and broken inheritance items - 2. User can choose configurable scan depth and whether to include or exclude inherited permissions before running - 3. User can export the permissions results to a CSV file with all raw permission data - 4. User can export the permissions results to an interactive HTML report where rows are sortable, filterable, and groupable by user - 5. Scanning a library with more than 5,000 items completes successfully — the tool paginates automatically and does not silently truncate or fail -**Plans**: 7 plans - -Plans: -- [x] 02-01-PLAN.md — Wave 0: test scaffolds (PermissionsService, ViewModel, classification, CSV, HTML export tests) + PermissionEntryHelper -- [x] 02-02-PLAN.md — Core models + PermissionsService scan engine (PermissionEntry, ScanOptions, IPermissionsService, PermissionsService) -- [x] 02-03-PLAN.md — SiteListService: tenant admin site listing for multi-site picker (ISiteListService, SiteListService, SiteInfo) -- [x] 02-04-PLAN.md — Export services: CsvExportService (with row merging) + HtmlExportService (self-contained HTML) -- [x] 02-05-PLAN.md — Localization: 15 Phase 2 EN/FR keys in Strings.resx, Strings.fr.resx, Strings.Designer.cs -- [x] 02-06-PLAN.md — PermissionsViewModel + SitePickerDialog (XAML + code-behind) -- [x] 02-07-PLAN.md — DI wiring + PermissionsView XAML + MainWindow tab replacement + visual checkpoint - -### Phase 3: Storage and File Operations -**Goal**: Users can view and export storage metrics per site and library, search for files across sites using multiple criteria, and detect duplicate files and folders — all with consistent export options and no silent failures on large datasets. -**Depends on**: Phase 2 -**Requirements**: STOR-01, STOR-02, STOR-03, STOR-04, STOR-05, SRCH-01, SRCH-02, SRCH-03, SRCH-04, DUPL-01, DUPL-02, DUPL-03 -**Success Criteria** (what must be TRUE): - 1. User can view storage consumption per library and per site (with configurable folder depth), including total size, version size, item count, and last modified date - 2. User can export storage metrics to CSV and to an interactive HTML with a collapsible tree view - 3. User can search for files across sites using at least extension, name/regex, date range, creator, and editor as criteria — with a configurable result cap up to 50,000 items - 4. User can export file search results to CSV and to an interactive sortable/filterable HTML - 5. User can scan for duplicate files (by name, size, creation date, modification date) and duplicate folders (by name, subfolder count, file count) and export the results to an HTML with grouped display -**Plans**: 8 plans - -Plans: -- [ ] 03-01-PLAN.md — Wave 0: test scaffolds + models (StorageNode, SearchResult, DuplicateGroup/Item, options) + interfaces (IStorageService, ISearchService, IDuplicatesService) + export stubs -- [ ] 03-02-PLAN.md — StorageService: CSOM StorageMetrics scan engine (recursive folder tree, library-level aggregation) -- [ ] 03-03-PLAN.md — Storage export services: StorageCsvExportService + StorageHtmlExportService (collapsible tree HTML) -- [ ] 03-04-PLAN.md — SearchService (KQL pagination, client-side Regex) + DuplicatesService (composite key grouping, CAML folder scan) -- [ ] 03-05-PLAN.md — Search and Duplicate export services: SearchCsvExportService + SearchHtmlExportService + DuplicatesHtmlExportService -- [ ] 03-06-PLAN.md — Localization: all Phase 3 EN/FR keys for Storage, File Search, and Duplicates tabs -- [ ] 03-07-PLAN.md — StorageViewModel + StorageView XAML + DI wiring (Storage tab replaces FeatureTabBase stub) -- [ ] 03-08-PLAN.md — SearchViewModel + SearchView + DuplicatesViewModel + DuplicatesView + DI wiring + visual checkpoint - -### Phase 4: Bulk Operations and Provisioning -**Goal**: Users can execute bulk write operations (member additions, site creation, file transfer) with per-item error reporting and cancellation, capture site structures as reusable templates, apply templates to create new sites, and provision folder structures from CSV — all without silent partial failures. -**Depends on**: Phase 3 -**Requirements**: BULK-01, BULK-02, BULK-03, BULK-04, BULK-05, TMPL-01, TMPL-02, TMPL-03, TMPL-04, FOLD-01, FOLD-02 -**Success Criteria** (what must be TRUE): - 1. User can transfer files and folders between sites with real-time progress tracking and can cancel mid-operation — transferred items are confirmed and failures are reported per-item - 2. User can add members to groups in bulk from a CSV file — each row that fails is reported individually, not silently skipped - 3. User can create multiple sites in bulk from a CSV file with per-site error reporting and mid-operation cancellation - 4. User can capture an existing site's structure (libraries, folders, permission groups, logo, settings) as a named template stored in JSON, then apply that template to create a new Communication or Teams site - 5. User can manage saved templates (create, rename, delete) and create folder structures on a target site from a CSV template -**Plans**: 10 plans - -Plans: -- [ ] 04-01-PLAN.md — Dependencies + core models + interfaces + BulkOperationRunner + test scaffolds -- [ ] 04-02-PLAN.md — CsvValidationService + TemplateRepository with unit tests -- [ ] 04-03-PLAN.md — FileTransferService (CSOM MoveCopyUtil, conflict policies) -- [ ] 04-04-PLAN.md — BulkMemberService (Graph SDK batch + CSOM fallback) -- [ ] 04-05-PLAN.md — BulkSiteService (PnP Framework site creation) -- [ ] 04-06-PLAN.md — TemplateService + FolderStructureService -- [ ] 04-07-PLAN.md — Localization + shared dialogs + example CSV resources -- [ ] 04-08-PLAN.md — TransferViewModel + TransferView -- [ ] 04-09-PLAN.md — BulkMembersViewModel + BulkSitesViewModel + FolderStructureViewModel + Views -- [ ] 04-10-PLAN.md — TemplatesViewModel + TemplatesView + DI registration + MainWindow wiring + visual checkpoint - -### Phase 5: Distribution and Hardening -**Goal**: The application ships as a single self-contained EXE that runs on a machine with no .NET runtime installed, all previously identified reliability constraints are verified end-to-end (5,000-item pagination, JSON corruption recovery, throttling retry, cancellation), and the French locale is complete and tested. -**Depends on**: Phase 4 -**Requirements**: FOUND-11 -**Success Criteria** (what must be TRUE): - 1. Running the published EXE on a clean machine with no .NET runtime installed launches the application and all features function correctly - 2. The application recovers gracefully when a SharePoint API call is throttled (429/503) — the user sees a retry progress message and the operation eventually completes or surfaces a clear failure - 3. The French locale is complete for all UI strings — no English fallback text appears when the language is set to French - 4. A scan against a library with more than 5,000 items returns complete, correct results with no silent truncation verified against a known dataset -**Plans**: 3 plans - -Plans: -- [ ] 05-01-PLAN.md — Helper visibility changes + retry/pagination/locale unit tests -- [ ] 05-02-PLAN.md — FR diacritic corrections + self-contained publish configuration -- [ ] 05-03-PLAN.md — Full test suite verification + publish smoke test + human checkpoint - -## Progress - -**Execution Order:** -Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 - -| Phase | Plans Complete | Status | Completed | -|-------|----------------|--------|-----------| -| 1. Foundation | 8/8 | Complete | 2026-04-02 | -| 2. Permissions | 7/7 | Complete | 2026-04-02 | -| 3. Storage and File Operations | 8/8 | Complete | 2026-04-02 | -| 4. Bulk Operations and Provisioning | 10/10 | Complete | 2026-04-03 | -| 5. Distribution and Hardening | 3/3 | Complete | 2026-04-03 | diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-01-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation/01-01-PLAN.md deleted file mode 100644 index 3a97e4b..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-01-PLAN.md +++ /dev/null @@ -1,226 +0,0 @@ ---- -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). - - - -After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` - diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-01-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation/01-01-SUMMARY.md deleted file mode 100644 index bebea4c..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-01-SUMMARY.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -phase: 01-foundation -plan: 01 -subsystem: infra -tags: [wpf, dotnet10, msal, pnp-framework, serilog, xunit, generic-host, csharp] - -# Dependency graph -requires: [] -provides: - - WPF .NET 10 solution scaffold (SharepointToolbox.slnx) - - Generic Host entry point with [STAThread] Main and Serilog rolling file sink - - All NuGet packages pre-wired (CommunityToolkit.Mvvm, MSAL, PnP.Framework, Serilog) - - xUnit test project with 7 stub test files (0 failed, 7 skipped) -affects: - - 01-02 (folder structure builds on this scaffold) - - 01-03 (ProfileService/SettingsService tests stubbed here) - - 01-04 (MsalClientFactory/SessionManager tests stubbed here) - - 01-05 (TranslationSource/LoggingIntegration tests stubbed here) - - 01-06 (FeatureViewModelBase tests stubbed here) - -# Tech tracking -tech-stack: - added: - - CommunityToolkit.Mvvm 8.4.2 - - Microsoft.Extensions.Hosting 10.0.0 - - Microsoft.Identity.Client 4.83.3 - - Microsoft.Identity.Client.Extensions.Msal 4.83.3 - - Microsoft.Identity.Client.Broker 4.82.1 - - PnP.Framework 1.18.0 - - Serilog 4.3.1 - - Serilog.Sinks.File 7.0.0 - - Serilog.Extensions.Hosting 10.0.0 - - Moq 4.20.72 (test project) - - xunit 2.9.3 (test project) - patterns: - - Generic Host entry point via static [STAThread] Main (not Application.Run override) - - App.xaml demoted from ApplicationDefinition to Page (enables custom Main) - - PublishTrimmed=false enforced to support PnP.Framework + MSAL reflection usage - - net10.0-windows + UseWPF=true in both main and test projects for compatibility - -key-files: - created: - - SharepointToolbox.slnx - - SharepointToolbox/SharepointToolbox.csproj - - SharepointToolbox/App.xaml - - SharepointToolbox/App.xaml.cs - - SharepointToolbox/MainWindow.xaml - - SharepointToolbox/MainWindow.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 - modified: [] - -key-decisions: - - "Upgraded MSAL from 4.83.1 to 4.83.3 — Extensions.Msal 4.83.3 requires MSAL >= 4.83.3; minor patch bump with no behavioral difference" - - "Test project targets net10.0-windows with UseWPF=true — required to reference main WPF project; plain net10.0 is framework-incompatible" - - "Solution uses .slnx format (new .NET 10 XML solution format) — dotnet new sln creates .slnx in .NET 10 SDK, fully supported" - -patterns-established: - - "Generic Host + [STAThread] Main: App.xaml.cs owns static Main, App.xaml has no StartupUri, App.xaml is Page not ApplicationDefinition" - - "Stub test pattern: [Fact(Skip = reason)] with plan reference — ensures test suite passes from day one while tracking future implementation" - -requirements-completed: - - FOUND-01 - -# Metrics -duration: 4min -completed: 2026-04-02 ---- - -# Phase 1 Plan 01: Solution Scaffold Summary - -**WPF .NET 10 solution with Generic Host entry point, all NuGet packages (MSAL 4.83.3, PnP.Framework 1.18.0, Serilog 4.3.1), and xUnit test project with 7 stub tests (0 failures)** - -## Performance - -- **Duration:** 4 min -- **Started:** 2026-04-02T09:58:26Z -- **Completed:** 2026-04-02T10:02:35Z -- **Tasks:** 2 -- **Files modified:** 14 - -## Accomplishments - -- Solution scaffold compiles with 0 errors and 0 warnings on dotnet build -- Generic Host entry point correctly wired with [STAThread] Main, App.xaml demoted from ApplicationDefinition to Page -- All 9 NuGet packages added with compatible versions; PublishTrimmed=false enforced -- xUnit test project references main project; dotnet test shows 7 skipped, 0 failed - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: Create solution and WPF project with all NuGet packages** - `f469804` (feat) -2. **Task 2: Create xUnit test project with stub test files** - `eac34e3` (feat) - -**Plan metadata:** (docs commit follows) - -## Files Created/Modified - -- `SharepointToolbox.slnx` - Solution file with both projects -- `SharepointToolbox/SharepointToolbox.csproj` - WPF .NET 10 with all packages, PublishTrimmed=false, StartupObject -- `SharepointToolbox/App.xaml` - StartupUri removed, App.xaml as Page not ApplicationDefinition -- `SharepointToolbox/App.xaml.cs` - [STAThread] Main with Host.CreateDefaultBuilder + Serilog rolling file sink -- `SharepointToolbox/MainWindow.xaml` + `MainWindow.xaml.cs` - Default WPF template (replaced in plan 01-06) -- `SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` - xUnit + Moq, net10.0-windows, references main project -- 7 stub test files across Services/, Auth/, ViewModels/, Localization/, Integration/ - -## Decisions Made - -- Upgraded MSAL from 4.83.1 to 4.83.3 — Extensions.Msal 4.83.3 pulls MSAL >= 4.83.3 as a transitive dependency; pinning 4.83.1 caused NU1605 downgrade error. Minor patch bump, no behavioral change. -- Test project targets net10.0-windows with UseWPF=true — framework incompatibility prevented `dotnet add reference` with net10.0; WPF test host is required anyway for any UI-layer testing. -- Solution file is .slnx (new .NET 10 XML format) — dotnet new sln in .NET 10 SDK creates .slnx by default; fully functional with dotnet build/test. - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 1 - Bug] MSAL version bumped from 4.83.1 to 4.83.3** -- **Found during:** Task 1 (NuGet package installation) -- **Issue:** `Microsoft.Identity.Client.Extensions.Msal 4.83.3` requires `Microsoft.Identity.Client >= 4.83.3`; plan specified 4.83.1 causing NU1605 downgrade error and failed restore -- **Fix:** Updated MSAL pin to 4.83.3 to satisfy transitive dependency constraint -- **Files modified:** SharepointToolbox/SharepointToolbox.csproj -- **Verification:** `dotnet restore` succeeded; build 0 errors -- **Committed in:** f469804 (Task 1 commit) - -**2. [Rule 3 - Blocking] Test project changed to net10.0-windows + UseWPF=true** -- **Found during:** Task 2 (adding project reference to test project) -- **Issue:** `dotnet add reference` rejected with "incompatible targeted frameworks" — net10.0 test cannot reference net10.0-windows WPF project -- **Fix:** Updated test project TargetFramework to net10.0-windows and added UseWPF=true -- **Files modified:** SharepointToolbox.Tests/SharepointToolbox.Tests.csproj -- **Verification:** `dotnet test` succeeded; 7 skipped, 0 failed -- **Committed in:** eac34e3 (Task 2 commit) - ---- - -**Total deviations:** 2 auto-fixed (1 bug — version conflict, 1 blocking — framework incompatibility) -**Impact on plan:** Both fixes required for the build to succeed. No scope creep. MSAL functionality identical at 4.83.3. - -## Issues Encountered - -- dotnet new wpf rejects `-f net10.0-windows` as framework flag (only accepts short TFM like `net10.0`) but the generated csproj correctly sets `net10.0-windows`. Template limitation, not a runtime issue. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness - -- Solution scaffold ready for plan 01-02 (folder structure and namespace layout) -- All packages pre-installed — subsequent plans add code, not packages -- Test infrastructure wired — stub files will be implemented in their respective plans (01-03 through 01-06) - ---- -*Phase: 01-foundation* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-02-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation/01-02-PLAN.md deleted file mode 100644 index 9250850..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-02-PLAN.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -phase: 01-foundation -plan: 02 -type: execute -wave: 2 -depends_on: - - 01-01 -files_modified: - - SharepointToolbox/Core/Models/TenantProfile.cs - - SharepointToolbox/Core/Models/OperationProgress.cs - - SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs - - SharepointToolbox/Core/Messages/LanguageChangedMessage.cs - - SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs - - SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs - - SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs -autonomous: true -requirements: - - FOUND-05 - - FOUND-06 - - FOUND-07 - - FOUND-08 -must_haves: - truths: - - "OperationProgress record is usable by all feature services for IProgress reporting" - - "TenantSwitchedMessage and LanguageChangedMessage are broadcast-ready via WeakReferenceMessenger" - - "SharePointPaginationHelper iterates past 5,000 items using ListItemCollectionPosition" - - "ExecuteQueryRetryHelper surfaces retry events as IProgress messages" - - "LogPanelSink writes to a RichTextBox-targeted dispatcher-safe callback" - artifacts: - - path: "SharepointToolbox/Core/Models/OperationProgress.cs" - provides: "Shared progress record used by all feature services" - contains: "record OperationProgress" - - path: "SharepointToolbox/Core/Models/TenantProfile.cs" - provides: "Profile model matching JSON schema" - contains: "TenantUrl" - - path: "SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs" - provides: "CSOM list pagination wrapping CamlQuery + ListItemCollectionPosition" - contains: "ListItemCollectionPosition" - - path: "SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs" - provides: "Retry wrapper for CSOM calls with throttle detection" - contains: "ExecuteQueryRetryAsync" - - path: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs" - provides: "Custom Serilog sink that writes to UI log panel" - contains: "ILogEventSink" - key_links: - - from: "SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs" - to: "Microsoft.SharePoint.Client.ListItemCollectionPosition" - via: "PnP.Framework CSOM" - pattern: "ListItemCollectionPosition" - - from: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs" - to: "Application.Current.Dispatcher" - via: "InvokeAsync for thread safety" - pattern: "Dispatcher.InvokeAsync" ---- - - -Build the Core layer — models, messages, and infrastructure helpers — that every subsequent plan depends on. These are the contracts: no business logic, just types and patterns. - -Purpose: All feature phases import OperationProgress, TenantProfile, the pagination helper, and the retry helper. Getting these right here means no rework in Phases 2-4. -Output: Core/Models, Core/Messages, Core/Helpers, Infrastructure/Logging directories with 7 files. - - - -@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md -@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md - - - -@.planning/PROJECT.md -@.planning/ROADMAP.md -@.planning/phases/01-foundation/01-CONTEXT.md -@.planning/phases/01-foundation/01-RESEARCH.md -@.planning/phases/01-foundation/01-01-SUMMARY.md - - - - - - Task 1: Core models and WeakReferenceMessenger messages - - SharepointToolbox/Core/Models/TenantProfile.cs, - SharepointToolbox/Core/Models/OperationProgress.cs, - SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs, - SharepointToolbox/Core/Messages/LanguageChangedMessage.cs - - - Create directories: `Core/Models/`, `Core/Messages/` - - **TenantProfile.cs** - ```csharp - namespace SharepointToolbox.Core.Models; - - public class TenantProfile - { - public string Name { get; set; } = string.Empty; - public string TenantUrl { get; set; } = string.Empty; - public string ClientId { get; set; } = string.Empty; - } - ``` - Note: Plain class (not record) — mutable for JSON deserialization with System.Text.Json. Field names `Name`, `TenantUrl`, `ClientId` must match existing JSON schema exactly (case-insensitive by default in STJ but preserve casing for compatibility). - - **OperationProgress.cs** - ```csharp - namespace SharepointToolbox.Core.Models; - - public record OperationProgress(int Current, int Total, string Message) - { - public static OperationProgress Indeterminate(string message) => - new(0, 0, message); - } - ``` - - **TenantSwitchedMessage.cs** - ```csharp - using CommunityToolkit.Mvvm.Messaging.Messages; - using SharepointToolbox.Core.Models; - - namespace SharepointToolbox.Core.Messages; - - public sealed class TenantSwitchedMessage : ValueChangedMessage - { - public TenantSwitchedMessage(TenantProfile profile) : base(profile) { } - } - ``` - - **LanguageChangedMessage.cs** - ```csharp - using CommunityToolkit.Mvvm.Messaging.Messages; - - namespace SharepointToolbox.Core.Messages; - - public sealed class LanguageChangedMessage : ValueChangedMessage - { - public LanguageChangedMessage(string cultureCode) : base(cultureCode) { } - } - ``` - - Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` to verify no errors. - - - cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5 - - Build succeeds. Four files created. TenantProfile fields match JSON schema. OperationProgress is a record with Indeterminate factory. - - - - Task 2: SharePointPaginationHelper, ExecuteQueryRetryHelper, LogPanelSink - - SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs, - SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs, - SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs - - - Create directories: `Core/Helpers/`, `Infrastructure/Logging/` - - **SharePointPaginationHelper.cs** - ```csharp - using Microsoft.SharePoint.Client; - using SharepointToolbox.Core.Models; - - namespace SharepointToolbox.Core.Helpers; - - public static class SharePointPaginationHelper - { - /// - /// Enumerates all items in a SharePoint list, bypassing the 5,000-item threshold. - /// Uses CamlQuery with RowLimit=2000 and ListItemCollectionPosition for pagination. - /// Never call ExecuteQuery directly on a list — always use this helper. - /// - public static async IAsyncEnumerable GetAllItemsAsync( - ClientContext ctx, - List list, - CamlQuery? baseQuery = null, - CancellationToken ct = default) - { - var query = baseQuery ?? CamlQuery.CreateAllItemsQuery(); - query.ViewXml = BuildPagedViewXml(query.ViewXml, rowLimit: 2000); - query.ListItemCollectionPosition = null; - - do - { - ct.ThrowIfCancellationRequested(); - var items = list.GetItems(query); - ctx.Load(items); - await ctx.ExecuteQueryAsync(); - - foreach (var item in items) - yield return item; - - query.ListItemCollectionPosition = items.ListItemCollectionPosition; - } - while (query.ListItemCollectionPosition != null); - } - - private static string BuildPagedViewXml(string? existingXml, int rowLimit) - { - // Inject or replace RowLimit in existing CAML, or create minimal view - if (string.IsNullOrWhiteSpace(existingXml)) - return $"{rowLimit}"; - - // Simple replacement approach — adequate for Phase 1 - if (existingXml.Contains("", StringComparison.OrdinalIgnoreCase)) - { - return System.Text.RegularExpressions.Regex.Replace( - existingXml, @"]*>\d+", - $"{rowLimit}", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - } - return existingXml.Replace("", $"{rowLimit}", - StringComparison.OrdinalIgnoreCase); - } - } - ``` - - **ExecuteQueryRetryHelper.cs** - ```csharp - using Microsoft.SharePoint.Client; - using SharepointToolbox.Core.Models; - - namespace SharepointToolbox.Core.Helpers; - - public static class ExecuteQueryRetryHelper - { - private const int MaxRetries = 5; - - /// - /// Executes a SharePoint query with automatic retry on throttle (429/503). - /// Surfaces retry events via progress for user visibility ("Throttled — retrying in 30s…"). - /// - public static async Task ExecuteQueryRetryAsync( - ClientContext ctx, - IProgress? progress = null, - CancellationToken ct = default) - { - int attempt = 0; - while (true) - { - ct.ThrowIfCancellationRequested(); - try - { - await ctx.ExecuteQueryAsync(); - return; - } - catch (Exception ex) when (IsThrottleException(ex) && attempt < MaxRetries) - { - attempt++; - int delaySeconds = (int)Math.Pow(2, attempt) * 5; // 10, 20, 40, 80, 160s - progress?.Report(OperationProgress.Indeterminate( - $"Throttled by SharePoint — retrying in {delaySeconds}s (attempt {attempt}/{MaxRetries})…")); - await Task.Delay(TimeSpan.FromSeconds(delaySeconds), ct); - } - } - } - - private static bool IsThrottleException(Exception ex) - { - var msg = ex.Message; - return msg.Contains("429") || msg.Contains("503") || - msg.Contains("throttl", StringComparison.OrdinalIgnoreCase); - } - } - ``` - - **LogPanelSink.cs** - ```csharp - using Serilog.Core; - using Serilog.Events; - using System.Windows; - using System.Windows.Documents; - using System.Windows.Media; - using System.Windows.Controls; - - namespace SharepointToolbox.Infrastructure.Logging; - - /// - /// Custom Serilog sink that writes timestamped, color-coded entries to a WPF RichTextBox. - /// Format: HH:mm:ss [LEVEL] Message — green=info/success, orange=warning, red=error. - /// All writes dispatch to the UI thread via Application.Current.Dispatcher. - /// - public class LogPanelSink : ILogEventSink - { - private readonly RichTextBox _richTextBox; - - public LogPanelSink(RichTextBox richTextBox) - { - _richTextBox = richTextBox; - } - - public void Emit(LogEvent logEvent) - { - var message = logEvent.RenderMessage(); - var timestamp = logEvent.Timestamp.ToString("HH:mm:ss"); - var level = logEvent.Level.ToString().ToUpperInvariant()[..4]; // INFO, WARN, ERRO, FATL - var text = $"{timestamp} [{level}] {message}"; - var color = GetColor(logEvent.Level); - - Application.Current?.Dispatcher.InvokeAsync(() => - { - var para = new Paragraph(new Run(text) { Foreground = new SolidColorBrush(color) }) - { - Margin = new Thickness(0) - }; - _richTextBox.Document.Blocks.Add(para); - _richTextBox.ScrollToEnd(); - }); - } - - private static Color GetColor(LogEventLevel level) => level switch - { - LogEventLevel.Warning => Colors.Orange, - LogEventLevel.Error or LogEventLevel.Fatal => Colors.Red, - _ => Colors.LimeGreen - }; - } - ``` - - Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` to verify no errors. - - - cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5 - - Build succeeds. SharePointPaginationHelper uses ListItemCollectionPosition. ExecuteQueryRetryHelper detects throttle exceptions. LogPanelSink dispatches to UI thread via Dispatcher.InvokeAsync. - - - - - -- `dotnet build SharepointToolbox.sln` passes with 0 errors -- `SharepointToolbox/Core/Models/TenantProfile.cs` contains `TenantUrl` (not `TenantURL` or `Url`) to match JSON schema -- `SharePointPaginationHelper.cs` contains `ListItemCollectionPosition` and loop condition checking for null -- `ExecuteQueryRetryHelper.cs` contains exponential backoff and progress reporting -- `LogPanelSink.cs` contains `Dispatcher.InvokeAsync` - - - -All 7 Core/Infrastructure files created and compiling. Models match JSON schema field names. Pagination helper correctly loops until ListItemCollectionPosition is null. - - - -After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` - diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-02-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation/01-02-SUMMARY.md deleted file mode 100644 index 0caa999..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-02-SUMMARY.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -phase: 01-foundation -plan: 02 -subsystem: core -tags: [wpf, dotnet10, csom, pnp-framework, serilog, sharepoint, pagination, retry, messaging, csharp] - -# Dependency graph -requires: - - 01-01 (solution scaffold, NuGet packages) -provides: - - TenantProfile model matching JSON schema (Name/TenantUrl/ClientId) - - OperationProgress record with Indeterminate factory for IProgress pattern - - TenantSwitchedMessage and LanguageChangedMessage broadcast-ready via WeakReferenceMessenger - - SharePointPaginationHelper: async iterator bypassing 5k item limit via ListItemCollectionPosition - - ExecuteQueryRetryHelper: exponential backoff on 429/503 with IProgress surfacing - - LogPanelSink: custom Serilog ILogEventSink writing to RichTextBox via Dispatcher.InvokeAsync -affects: - - 01-03 (ProfileService uses TenantProfile) - - 01-04 (MsalClientFactory uses TenantProfile.ClientId/TenantUrl) - - 01-05 (TranslationSource sends LanguageChangedMessage; LoggingIntegration uses LogPanelSink) - - 01-06 (FeatureViewModelBase uses OperationProgress + IProgress pattern) - - 02-xx (all SharePoint feature services use pagination and retry helpers) - -# Tech tracking -tech-stack: - added: [] - patterns: - - IAsyncEnumerable with [EnumeratorCancellation] for correct WithCancellation support - - ListItemCollectionPosition loop (do/while until null) for CSOM pagination past 5k items - - Exponential backoff: delay = 2^attempt * 5s (10, 20, 40, 80, 160s) up to MaxRetries=5 - - WeakReferenceMessenger messages via ValueChangedMessage base class - - Dispatcher.InvokeAsync for thread-safe UI writes from Serilog background thread - -key-files: - created: - - 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 - modified: [] - -key-decisions: - - "TenantProfile is a plain class (not record) — mutable for System.Text.Json deserialization; fields Name/TenantUrl/ClientId match existing JSON schema casing" - - "SharePointPaginationHelper uses [EnumeratorCancellation] on ct parameter — required for correct cancellation forwarding when callers use WithCancellation(ct)" - - "ExecuteQueryRetryHelper uses catch-when filter with IsThrottleException — matches 429/503 status codes and 'throttl' text in message, covers PnP.Framework exception surfaces" - -requirements-completed: - - FOUND-05 - - FOUND-06 - - FOUND-07 - - FOUND-08 - -# Metrics -duration: 1min -completed: 2026-04-02 ---- - -# Phase 1 Plan 02: Core Models, Messages, and Infrastructure Helpers Summary - -**7 Core/Infrastructure files providing typed contracts (TenantProfile, OperationProgress, messages, CSOM pagination helper, throttle-aware retry helper, RichTextBox Serilog sink) — 0 errors, 0 warnings** - -## Performance - -- **Duration:** 1 min -- **Started:** 2026-04-02T10:04:59Z -- **Completed:** 2026-04-02T10:06:00Z -- **Tasks:** 2 -- **Files modified:** 7 - -## Accomplishments - -- All 7 Core/Infrastructure files created and compiling with 0 errors, 0 warnings -- TenantProfile fields match JSON schema exactly (Name/TenantUrl/ClientId) -- OperationProgress record with Indeterminate factory, usable by all feature services via IProgress -- TenantSwitchedMessage and LanguageChangedMessage correctly inherit ValueChangedMessage for WeakReferenceMessenger broadcast -- SharePointPaginationHelper iterates past 5,000 items using ListItemCollectionPosition do/while loop; RowLimit=2000 -- ExecuteQueryRetryHelper surfaces retry events via IProgress with exponential backoff (10s, 20s, 40s, 80s, 160s) -- LogPanelSink writes color-coded, timestamped entries to RichTextBox via Dispatcher.InvokeAsync for thread safety - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: Core models and WeakReferenceMessenger messages** - `ddb216b` (feat) -2. **Task 2: SharePointPaginationHelper, ExecuteQueryRetryHelper, LogPanelSink** - `c297801` (feat) - -**Plan metadata:** (docs commit follows) - -## Files Created/Modified - -- `SharepointToolbox/Core/Models/TenantProfile.cs` - Plain class; Name/TenantUrl/ClientId match JSON schema -- `SharepointToolbox/Core/Models/OperationProgress.cs` - Record with Indeterminate factory; IProgress contract -- `SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs` - ValueChangedMessage; WeakReferenceMessenger broadcast -- `SharepointToolbox/Core/Messages/LanguageChangedMessage.cs` - ValueChangedMessage; WeakReferenceMessenger broadcast -- `SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs` - Async iterator; ListItemCollectionPosition loop; [EnumeratorCancellation] -- `SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs` - Retry on 429/503/throttle; exponential backoff; IProgress surfacing -- `SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs` - ILogEventSink; Dispatcher.InvokeAsync; color-coded by level - -## Decisions Made - -- TenantProfile is a plain mutable class (not a record) — System.Text.Json deserialization requires a parameterless constructor and settable properties; field names match the existing JSON schema exactly to avoid serialization mismatches. -- SharePointPaginationHelper.GetAllItemsAsync decorates `ct` with `[EnumeratorCancellation]` — without this attribute, cancellation tokens passed via `WithCancellation()` on the async enumerable are silently ignored. This is a correctness requirement for callers who use the cancellation pattern. -- ExecuteQueryRetryHelper.IsThrottleException checks for "429", "503", and "throttl" (case-insensitive) — PnP.Framework surfaces HTTP errors in the exception message rather than a dedicated exception type; this covers all known throttle surfaces. - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 2 - Missing critical functionality] Added [EnumeratorCancellation] attribute to SharePointPaginationHelper** -- **Found during:** Task 2 (dotnet build) -- **Issue:** CS8425 warning — async iterator with `CancellationToken ct` parameter missing `[EnumeratorCancellation]`; without it, cancellation via `WithCancellation(ct)` on the `IAsyncEnumerable` is silently dropped, breaking cancellation for all callers -- **Fix:** Added `using System.Runtime.CompilerServices;` and `[EnumeratorCancellation]` attribute on the `ct` parameter -- **Files modified:** `SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs` -- **Verification:** Build 0 warnings, 0 errors after fix -- **Committed in:** c297801 (Task 2 commit) - ---- - -**Total deviations:** 1 auto-fixed (Rule 2 — missing critical functionality for correct cancellation behavior) -**Impact on plan:** Fix required for correct operation. One line change, no scope creep. - -## Issues Encountered - -None beyond the auto-fixed deviation above. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness - -- All contracts in place for plan 01-03 (ProfileService uses TenantProfile) -- All contracts in place for plan 01-04 (MsalClientFactory uses TenantProfile.ClientId/TenantUrl) -- All contracts in place for plan 01-05 (LoggingIntegration uses LogPanelSink; LanguageChangedMessage for TranslationSource) -- All contracts in place for plan 01-06 (FeatureViewModelBase uses OperationProgress + IProgress) -- All Phase 2+ SharePoint feature services can use pagination and retry helpers immediately - ---- -*Phase: 01-foundation* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-03-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation/01-03-PLAN.md deleted file mode 100644 index b5854d2..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-03-PLAN.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -phase: 01-foundation -plan: 03 -type: execute -wave: 3 -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. - - - -After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md` - diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-03-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation/01-03-SUMMARY.md deleted file mode 100644 index 6eca6ad..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-03-SUMMARY.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -phase: 01-foundation -plan: 03 -subsystem: persistence -tags: [dotnet10, csharp, system-text-json, semaphoreslim, write-then-replace, unit-tests, xunit] - -# Dependency graph -requires: - - 01-01 (solution scaffold, test project) - - 01-02 (TenantProfile model) -provides: - - ProfileRepository: file I/O for profiles JSON with SemaphoreSlim write lock and write-then-replace - - ProfileService: CRUD (GetProfiles/AddProfile/RenameProfile/DeleteProfile) with input validation - - SettingsRepository: file I/O for settings JSON with same write-then-replace safety pattern - - SettingsService: GetSettings/SetLanguage/SetDataFolder with supported-language validation - - AppSettings model: DataFolder + Lang with camelCase JSON compatibility -affects: - - 01-04 (MsalClientFactory may use ProfileService for tenant list) - - 01-05 (TranslationSource uses SettingsService for lang) - - 01-06 (FeatureViewModelBase may use ProfileService/SettingsService) - - all feature plans (profile and settings are the core data contracts) - -# Tech tracking -tech-stack: - added: [] - patterns: - - Write-then-replace: write to .tmp, validate JSON round-trip via JsonDocument.Parse, then File.Move(overwrite:true) - - SemaphoreSlim(1,1) for async exclusive write access on per-repository basis - - System.Text.Json with PropertyNamingPolicy.CamelCase for schema-compatible serialization - - PropertyNameCaseInsensitive=true for deserialization to handle both old and new JSON - - TDD with IDisposable temp file pattern for isolated unit tests - -key-files: - created: - - SharepointToolbox/Core/Models/AppSettings.cs - - 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 - modified: [] - -key-decisions: - - "Explicit System.IO using required in WPF project — WPF temp build project does not include System.IO in implicit usings; all file I/O classes need explicit namespace import" - - "SettingsService validates only 'en' and 'fr' — matches app's supported locales; throws ArgumentException for any other code" - - "LoadAsync on corrupt JSON throws InvalidDataException (not silent empty) — explicit failure is safer than silently discarding user data" - -patterns-established: - - "Write-then-replace: all file persistence uses .tmp write + JsonDocument.Parse validation + File.Move(overwrite:true) to protect against crash-corruption" - - "IDisposable test pattern: unit tests use Path.GetTempFileName() + Dispose() for clean isolated file I/O tests" - -requirements-completed: - - FOUND-02 - - FOUND-10 - - FOUND-12 - -# Metrics -duration: 8min -completed: 2026-04-02 ---- - -# Phase 1 Plan 03: Persistence Layer Summary - -**ProfileRepository + SettingsRepository with write-then-replace safety, ProfileService + SettingsService with validation, 18 unit tests covering round-trips, corrupt-file recovery, concurrency, and JSON schema compatibility** - -## Performance - -- **Duration:** 8 min -- **Started:** 2026-04-02T10:09:13Z -- **Completed:** 2026-04-02T10:17:00Z -- **Tasks:** 2 -- **Files modified:** 7 - -## Accomplishments - -- ProfileRepository and SettingsRepository both implement write-then-replace (tmp file → JSON validation → File.Move) with SemaphoreSlim(1,1) preventing concurrent write corruption -- JSON serialization uses camelCase (PropertyNamingPolicy.CamelCase) — preserves existing user data field names: `profiles`, `name`, `tenantUrl`, `clientId`, `dataFolder`, `lang` -- ProfileService provides full CRUD with input validation (Name not empty, TenantUrl valid absolute URL, ClientId not empty) -- SettingsService validates language codes against supported set (en/fr only), allows empty dataFolder -- All 18 unit tests pass (10 ProfileServiceTests + 8 SettingsServiceTests); no skips - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: ProfileRepository and ProfileService with write-then-replace** - `769196d` (feat) -2. **Task 2: SettingsRepository and SettingsService** - `ac3fa5c` (feat) - -**Plan metadata:** (docs commit follows) - -## Files Created/Modified - -- `SharepointToolbox/Core/Models/AppSettings.cs` - AppSettings model; DataFolder + Lang with camelCase JSON -- `SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs` - File I/O; SemaphoreSlim; write-then-replace; camelCase -- `SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs` - Same pattern as ProfileRepository for settings -- `SharepointToolbox/Services/ProfileService.cs` - CRUD on profiles; validates Name/TenantUrl/ClientId; throws KeyNotFoundException -- `SharepointToolbox/Services/SettingsService.cs` - Get/SetLanguage/SetDataFolder; validates language codes -- `SharepointToolbox.Tests/Services/ProfileServiceTests.cs` - 10 tests: round-trip, missing file, corrupt JSON, concurrency, schema keys -- `SharepointToolbox.Tests/Services/SettingsServiceTests.cs` - 8 tests: defaults, round-trip, JSON keys, tmp file, language/folder persistence - -## Decisions Made - -- Explicit `using System.IO;` required in WPF main project — the WPF temp build project does not include `System.IO` in its implicit usings, unlike the standard non-WPF SDK. All repositories need explicit namespace imports. -- `SettingsService.SetLanguageAsync` validates only "en" and "fr" using a case-insensitive `HashSet`. Other codes throw `ArgumentException` immediately. -- `LoadAsync` on corrupt JSON throws `InvalidDataException` (not silent empty list/default) — this is an explicit safety decision: silently discarding corrupt data could mask accidental overwrites. - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 3 - Blocking] Added explicit System.IO using to WPF project files** -- **Found during:** Task 1 (dotnet test — first GREEN attempt) -- **Issue:** WPF temporary build project does not include `System.IO` in its implicit usings. `File`, `Path`, `Directory`, `IOException`, `InvalidDataException` all unresolved in the main project and test project. -- **Fix:** Added `using System.IO;` at the top of ProfileRepository.cs, SettingsRepository.cs, ProfileServiceTests.cs, and SettingsServiceTests.cs -- **Files modified:** All 4 implementation and test files -- **Verification:** Build succeeded with 0 errors, 18/18 tests pass -- **Committed in:** 769196d and ac3fa5c (inline with respective task commits) - ---- - -**Total deviations:** 1 auto-fixed (Rule 3 — blocking build issue) -**Impact on plan:** One-line fix per file, no logic changes, no scope creep. - -## Issues Encountered - -None beyond the auto-fixed deviation above. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness - -- ProfileService and SettingsService ready for injection in plan 01-04 (MsalClientFactory may need tenant list from ProfileService) -- SettingsService.SetLanguageAsync ready for TranslationSource in plan 01-05 -- Both services follow the same constructor injection pattern — ready for DI container registration in plan 01-06 or 01-07 -- JSON schema contracts locked: field names are tested and verified camelCase - ---- -*Phase: 01-foundation* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-04-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation/01-04-PLAN.md deleted file mode 100644 index 0c2c856..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-04-PLAN.md +++ /dev/null @@ -1,266 +0,0 @@ ---- -phase: 01-foundation -plan: 04 -type: execute -wave: 4 -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. - - - -After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md` - diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-04-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation/01-04-SUMMARY.md deleted file mode 100644 index dca8f13..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-04-SUMMARY.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -phase: 01-foundation -plan: 04 -subsystem: auth -tags: [dotnet10, csharp, msal, msal-cache-helper, pnp-framework, sharepoint, csom, unit-tests, xunit, semaphoreslim, tdd] - -# Dependency graph -requires: - - 01-01 (solution scaffold, NuGet packages — Microsoft.Identity.Client, Microsoft.Identity.Client.Extensions.Msal, PnP.Framework) - - 01-02 (TenantProfile model with ClientId/TenantUrl fields) - - 01-03 (ProfileService/SettingsService — injection pattern) -provides: - - MsalClientFactory: per-ClientId IPublicClientApplication with MsalCacheHelper persistent cache - - MsalClientFactory.GetCacheHelper(clientId): exposes MsalCacheHelper for PnP tokenCacheCallback wiring - - SessionManager: singleton owning all live ClientContext instances with IsAuthenticated/GetOrCreateContextAsync/ClearSessionAsync -affects: - - 01-05 (TranslationSource/app setup — SessionManager ready for DI registration) - - 01-06 (FeatureViewModelBase — SessionManager is the auth gateway for all feature commands) - - 02-xx (all SharePoint feature services call SessionManager.GetOrCreateContextAsync) - -# Tech tracking -tech-stack: - added: [] - patterns: - - MsalClientFactory: per-clientId Dictionary + SemaphoreSlim(1,1) for concurrent-safe lazy creation - - MsalCacheHelper stored per-clientId alongside PCA — exposed via GetCacheHelper() for PnP tokenCacheCallback wiring - - SessionManager: per-tenantUrl Dictionary + SemaphoreSlim(1,1); NormalizeUrl (TrimEnd + ToLowerInvariant) for key consistency - - PnP tokenCacheCallback pattern: cacheHelper.RegisterCache(tokenCache) wires persistent cache to PnP's internal MSAL token cache - - ArgumentException.ThrowIfNullOrEmpty on all public method entry points requiring string arguments - -key-files: - created: - - SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs - - SharepointToolbox/Services/SessionManager.cs - - SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs - - SharepointToolbox.Tests/Auth/SessionManagerTests.cs - modified: [] - -key-decisions: - - "MsalClientFactory stores both IPublicClientApplication and MsalCacheHelper per clientId — GetCacheHelper() exposes helper for PnP's tokenCacheCallback; PnP creates its own internal PCA so we cannot pass ours directly" - - "SessionManager uses tokenCacheCallback to wire MsalCacheHelper to PnP's token cache — both PCA and PnP share the same persistent msal_{clientId}.cache file, preventing token duplication" - - "CacheDirectory is a constructor parameter with a no-arg default — enables test isolation without real %AppData% writes" - - "Interactive login test marked Skip in unit test suite — GetOrCreateContextAsync integration requires browser/WAM flow that cannot run in CI" - -patterns-established: - - "Auth token cache wiring: Always call MsalClientFactory.GetOrCreateAsync first, then use GetCacheHelper() in PnP's tokenCacheCallback — ensures per-clientId cache isolation" - - "SessionManager is the single source of truth for ClientContext: callers must not store returned contexts" - -requirements-completed: - - FOUND-03 - - FOUND-04 - -# Metrics -duration: 4min -completed: 2026-04-02 ---- - -# Phase 1 Plan 04: Authentication Layer Summary - -**Per-tenant MSAL PCA with MsalCacheHelper persistent cache (one file per clientId in %AppData%) and SessionManager singleton owning all live PnP ClientContext instances — per-tenant isolation verified by 12 unit tests** - -## Performance - -- **Duration:** 4 min -- **Started:** 2026-04-02T10:20:49Z -- **Completed:** 2026-04-02T10:25:05Z -- **Tasks:** 2 -- **Files modified:** 4 - -## Accomplishments - -- MsalClientFactory creates one IPublicClientApplication per unique clientId (never shared across tenants); SemaphoreSlim prevents duplicate creation under concurrent calls -- MsalCacheHelper registered on each PCA's UserTokenCache; persistent cache files at `%AppData%\SharepointToolbox\auth\msal_{clientId}.cache` -- SessionManager is the sole holder of ClientContext instances; IsAuthenticated/ClearSessionAsync/GetOrCreateContextAsync with full argument validation -- ClearSessionAsync calls ctx.Dispose() and removes from internal dictionary; idempotent for unknown tenants -- 12 unit tests pass (4 MsalClientFactory + 8 SessionManager), 1 integration test correctly skipped -- PnP tokenCacheCallback pattern established: `cacheHelper.RegisterCache(tokenCache)` wires the factory-managed helper to PnP's internal MSAL token cache - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: MsalClientFactory — per-ClientId PCA with MsalCacheHelper** - `0295519` (feat) -2. **Task 2: SessionManager — singleton ClientContext holder** - `158aab9` (feat) - -**Plan metadata:** (docs commit follows) - -## Files Created/Modified - -- `SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs` - Per-clientId PCA + MsalCacheHelper; CacheDirectory constructor param; GetCacheHelper() for PnP wiring -- `SharepointToolbox/Services/SessionManager.cs` - Singleton; IsAuthenticated/GetOrCreateContextAsync/ClearSessionAsync; NormalizeUrl; tokenCacheCallback wiring -- `SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs` - 4 unit tests: same-instance, different-instances, concurrent-safe, AppData path; IDisposable temp dir cleanup -- `SharepointToolbox.Tests/Auth/SessionManagerTests.cs` - 8 unit tests + 1 skipped: IsAuthenticated before/after, ClearSessionAsync idempotency, ArgumentException on null/empty TenantUrl and ClientId - -## Decisions Made - -- `MsalClientFactory` stores `MsalCacheHelper` per clientId alongside the `IPublicClientApplication`. Added `GetCacheHelper(clientId)` to expose it. This is required because PnP.Framework's `CreateWithInteractiveLogin` creates its own internal PCA — we cannot pass our PCA to PnP directly. The `tokenCacheCallback` (`Action`) is the bridge: we call `cacheHelper.RegisterCache(tokenCache)` so PnP's internal cache uses the same persistent file. -- `CacheDirectory` is a public constructor parameter with a no-arg default pointing to `%AppData%\SharepointToolbox\auth`. Tests inject a temp directory to avoid real AppData writes and ensure cleanup. -- Interactive login test (`GetOrCreateContextAsync_CreatesContext`) is marked `[Fact(Skip = "Requires interactive MSAL — integration test only")]`. Browser/WAM flow cannot run in automated unit tests. - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 2 - Missing Critical] Added GetCacheHelper() to MsalClientFactory** -- **Found during:** Task 2 (SessionManager implementation) -- **Issue:** Plan's skeleton used a non-existent PnP overload that accepts `IPublicClientApplication` directly. PnP.Framework 1.18.0's `CreateWithInteractiveLogin` does not accept a PCA parameter — only `tokenCacheCallback: Action`. Without `GetCacheHelper()`, there was no way to wire the same MsalCacheHelper to PnP's internal token cache. -- **Fix:** Added `_helpers` dictionary to `MsalClientFactory`, stored `MsalCacheHelper` alongside PCA, exposed via `GetCacheHelper(clientId)`. `SessionManager` calls `GetOrCreateAsync` first, then `GetCacheHelper`, then uses it in `tokenCacheCallback`. -- **Files modified:** `SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs`, `SharepointToolbox/Services/SessionManager.cs` -- **Verification:** 12/12 unit tests pass, 0 build warnings -- **Committed in:** 158aab9 (Task 2 commit) - ---- - -**Total deviations:** 1 auto-fixed (Rule 2 — PnP API surface mismatch required bridge method) -**Impact on plan:** The key invariant is preserved: MsalClientFactory is called first, the per-clientId MsalCacheHelper is wired to PnP before any token acquisition. One method added to factory, no scope creep. - -## Issues Encountered - -None beyond the auto-fixed deviation above. - -## User Setup Required - -None — MSAL cache files are created on demand in %AppData%. No external service configuration required. - -## Next Phase Readiness - -- `SessionManager` ready for DI registration in plan 01-05 or 01-06 (singleton lifetime) -- `MsalClientFactory` ready for DI (singleton lifetime) -- Auth layer complete: every SharePoint operation in Phases 2-4 can call `SessionManager.GetOrCreateContextAsync(profile)` to get a live `ClientContext` -- Per-tenant isolation (one PCA + cache file per ClientId) confirmed by unit tests — token bleed between MSP client tenants is prevented - ---- -*Phase: 01-foundation* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-05-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation/01-05-PLAN.md deleted file mode 100644 index f53c59e..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-05-PLAN.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -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. - - - -After completion, create `.planning/phases/01-foundation/01-05-SUMMARY.md` - diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-05-SUMMARY.md b/.planning/milestones/v1.0-phases/01-foundation/01-05-SUMMARY.md deleted file mode 100644 index 111e81a..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-05-SUMMARY.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -phase: 01-foundation -plan: 05 -subsystem: localization -tags: [wpf, dotnet10, serilog, localization, resx, i18n, csharp, tdd] - -# Dependency graph -requires: - - 01-02 (LogPanelSink, LanguageChangedMessage) -provides: - - TranslationSource singleton with INotifyPropertyChanged indexer for runtime culture switching - - Strings.resx with 27 Phase 1 EN UI strings - - Strings.fr.resx with same 27 keys stubbed for Phase 5 FR translation - - LoggingIntegrationTests verifying Serilog rolling file sink and LogPanelSink type -affects: - - 01-06 (MainWindow.xaml binds via TranslationSource.Instance[key]) - - 01-07 (SettingsViewModel sets TranslationSource.Instance.CurrentCulture) - - 02-xx (all feature views use localized strings via TranslationSource) - -# Tech tracking -tech-stack: - added: [] - patterns: - - TranslationSource singleton with INotifyPropertyChanged empty-string key (signals all bindings refresh) - - WPF binding: Source={x:Static loc:TranslationSource.Instance}, Path=[key] - - ResourceManager from Strings.Designer.cs — manually maintained for dotnet build (no ResXFileCodeGenerator at build time) - - EmbeddedResource Update (not Include) in SDK-style project — avoids NETSDK1022 duplicate error - - IDisposable test teardown with TranslationSource culture reset — prevents test pollution - -key-files: - created: - - SharepointToolbox/Localization/TranslationSource.cs - - SharepointToolbox/Localization/Strings.resx - - SharepointToolbox/Localization/Strings.fr.resx - - SharepointToolbox/Localization/Strings.Designer.cs - modified: - - SharepointToolbox/SharepointToolbox.csproj - - SharepointToolbox/App.xaml.cs - - SharepointToolbox.Tests/Localization/TranslationSourceTests.cs - - SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs - -key-decisions: - - "Strings.Designer.cs is maintained manually — ResXFileCodeGenerator is a VS-only tool; dotnet build requires the designer file to pre-exist; only the ResourceManager accessor is needed (no per-key typed properties)" - - "EmbeddedResource uses Update not Include — SDK-style projects auto-include all .resx as EmbeddedResource; using Include causes NETSDK1022 duplicate error" - - "System.IO using added explicitly in test project — xunit test project implicit usings do not cover System.IO; consistent with existing pattern in main project" - -requirements-completed: - - FOUND-08 - - FOUND-09 - -# Metrics -duration: 4min -completed: 2026-04-02 ---- - -# Phase 1 Plan 05: Logging Infrastructure and Dynamic Localization Summary - -**TranslationSource singleton + EN/FR resx + Serilog integration tests — 26 tests pass (24 Unit, 2 Integration), 0 errors, 0 warnings** - -## Performance - -- **Duration:** 4 min -- **Started:** 2026-04-02T10:14:23Z -- **Completed:** 2026-04-02T10:18:08Z -- **Tasks:** 2 -- **Files modified:** 8 - -## Accomplishments - -- TranslationSource singleton implements INotifyPropertyChanged; indexer `[key]` uses ResourceManager for runtime culture switching without restart -- PropertyChanged fires with `string.Empty` PropertyName on culture change — WPF re-evaluates all bindings to TranslationSource.Instance -- Missing key returns `[key]` placeholder — prevents NullReferenceException in WPF bindings -- Same-culture assignment is no-op — equality check prevents spurious PropertyChanged events -- Strings.resx: 27 Phase 1 UI strings (EN): app, toolbar, tab, button, settings, profile, status, and error keys -- Strings.fr.resx: same 27 keys, EN values stubbed, marked `` -- Strings.Designer.cs: ResourceManager accessor for dotnet build compatibility (no VS ResXFileCodeGenerator dependency) -- LoggingIntegrationTests: verifies Serilog creates a rolling log file and writes message content; verifies LogPanelSink implements ILogEventSink -- App.xaml.cs: comment added documenting deferred LogPanelSink DI registration (plan 01-06) - -## Task Commits - -1. **Task 1 RED: Failing tests for TranslationSource** - `8a58140` (test) -2. **Task 1 GREEN: TranslationSource + resx files implementation** - `a287ed8` (feat) -3. **Task 2: Serilog integration tests + App.xaml.cs comment** - `1c532d1` (feat) - -## Files Created/Modified - -- `SharepointToolbox/Localization/TranslationSource.cs` — Singleton; INotifyPropertyChanged indexer; empty-string PropertyChanged; culture equality guard -- `SharepointToolbox/Localization/Strings.resx` — 27 Phase 1 EN string resources -- `SharepointToolbox/Localization/Strings.fr.resx` — 27 keys stubbed with EN values; Phase 5 FR completion -- `SharepointToolbox/Localization/Strings.Designer.cs` — ResourceManager accessor; manually maintained for dotnet build -- `SharepointToolbox/SharepointToolbox.csproj` — EmbeddedResource Update metadata for resx files -- `SharepointToolbox/App.xaml.cs` — LogPanelSink registration comment deferred to plan 01-06 -- `SharepointToolbox.Tests/Localization/TranslationSourceTests.cs` — 6 unit tests; IDisposable teardown for culture reset -- `SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs` — 2 integration tests; temp dir cleanup in Dispose - -## Decisions Made - -- Strings.Designer.cs maintained manually: `ResXFileCodeGenerator` is a Visual Studio design-time tool not available in `dotnet build`. The designer file only needs the `ResourceManager` property accessor — no per-key typed properties are needed since TranslationSource uses `ResourceManager.GetString(key, culture)` directly. -- `EmbeddedResource Update` instead of `Include`: SDK-style projects implicitly include all `.resx` files as `EmbeddedResource`. Using `Include` causes `NETSDK1022` duplicate build error. `Update` sets metadata on the already-included item. -- Explicit `System.IO` using in test file: test project implicit usings do not cover `System.IO`; consistent with the established pattern for the main project (prior decision FOUND-10). - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 3 - Blocking] Added explicit System.IO using to LoggingIntegrationTests.cs** -- **Found during:** Task 2 (dotnet build) -- **Issue:** CS0103 — `Path`, `Directory`, `File` not found; test project implicit usings do not include System.IO -- **Fix:** Added `using System.IO;` to the test file -- **Files modified:** `SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs` -- **Verification:** Build 0 errors, 0 warnings after fix -- **Committed in:** 1c532d1 (Task 2 commit) - -**2. [Rule 3 - Blocking] Used EmbeddedResource Update (not Include) for resx metadata** -- **Found during:** Task 1 GREEN (dotnet build) -- **Issue:** NETSDK1022 duplicate EmbeddedResource — SDK auto-includes all .resx files; explicit Include causes duplicate error -- **Fix:** Changed `` to `` in SharepointToolbox.csproj -- **Files modified:** `SharepointToolbox/SharepointToolbox.csproj` -- **Verification:** Build 0 errors, 0 warnings after fix -- **Committed in:** a287ed8 (Task 1 commit) - -**3. [Rule 3 - Blocking] Created Strings.Designer.cs manually** -- **Found during:** Task 1 GREEN (dotnet build) -- **Issue:** `Strings` class does not exist in context — ResXFileCodeGenerator is VS-only, not run by dotnet build CLI -- **Fix:** Created Strings.Designer.cs with ResourceManager accessor manually; only the `ResourceManager` property is needed (TranslationSource uses it directly) -- **Files modified:** `SharepointToolbox/Localization/Strings.Designer.cs` -- **Verification:** Build 0 errors after fix; TranslationSourceTests pass -- **Committed in:** a287ed8 (Task 1 commit) - ---- - -**Total deviations:** 3 auto-fixed (all Rule 3 — blocking build issues) -**Impact on plan:** All fixes minor; no scope creep; no behavior change from plan intent. - -## Issues Encountered - -None beyond the auto-fixed deviations above. - -## User Setup Required - -None — no external service configuration required. - -## Next Phase Readiness - -- TranslationSource.Instance ready for WPF XAML binding in plan 01-06 (MainWindow) -- All 27 Phase 1 UI string keys defined in EN resx -- FR resx keyset matches EN — Phase 5 can add translations without key changes -- Serilog rolling file sink verified working; LogPanelSink type verified ILogEventSink-compatible -- Plan 01-06 can proceed immediately - -## Self-Check: PASSED - -- FOUND: SharepointToolbox/Localization/TranslationSource.cs -- FOUND: SharepointToolbox/Localization/Strings.resx -- FOUND: SharepointToolbox/Localization/Strings.fr.resx -- FOUND: SharepointToolbox/Localization/Strings.Designer.cs -- FOUND: SharepointToolbox.Tests/Localization/TranslationSourceTests.cs -- FOUND: SharepointToolbox.Tests/Integration/LoggingIntegrationTests.cs -- FOUND: .planning/phases/01-foundation/01-05-SUMMARY.md -- Commit 8a58140: test(01-05): add failing tests for TranslationSource singleton -- Commit a287ed8: feat(01-05): implement TranslationSource singleton + EN/FR resx files -- Commit 1c532d1: feat(01-05): add Serilog integration tests and App.xaml.cs LogPanelSink comment - ---- -*Phase: 01-foundation* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/01-foundation/01-06-PLAN.md b/.planning/milestones/v1.0-phases/01-foundation/01-06-PLAN.md deleted file mode 100644 index 8fece7e..0000000 --- a/.planning/milestones/v1.0-phases/01-foundation/01-06-PLAN.md +++ /dev/null @@ -1,515 +0,0 @@ ---- -phase: 01-foundation -plan: 06 -type: execute -wave: 5 -depends_on: - - 01-03 - - 01-04 - - 01-05 -files_modified: - - SharepointToolbox/Core/Messages/ProgressUpdatedMessage.cs - - SharepointToolbox/ViewModels/FeatureViewModelBase.cs - - SharepointToolbox/ViewModels/MainWindowViewModel.cs - - SharepointToolbox/ViewModels/ProfileManagementViewModel.cs - - SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs - - SharepointToolbox/Views/Controls/FeatureTabBase.xaml - - SharepointToolbox/Views/Controls/FeatureTabBase.xaml.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" - - "All 7 stub feature tabs use FeatureTabBase UserControl — ProgressBar + TextBlock + Cancel button shown only when IsRunning" - - "StatusBar middle item shows live operation status text (ProgressStatus from ProgressUpdatedMessage), not static ConnectionStatus" - 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/Controls/FeatureTabBase.xaml" - provides: "Reusable UserControl with ProgressBar + TextBlock + Cancel button strip" - contains: "ProgressBar" - - 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" - - from: "SharepointToolbox/ViewModels/MainWindowViewModel.cs" - to: "SharepointToolbox/Core/Messages/ProgressUpdatedMessage.cs" - via: "Messenger.Register in OnActivated — updates ProgressStatus + ProgressPercentage" - pattern: "ProgressUpdatedMessage" - - from: "SharepointToolbox/Views/MainWindow.xaml StatusBar middle item" - to: "SharepointToolbox/ViewModels/MainWindowViewModel.cs ProgressStatus" - via: "Binding Content={Binding ProgressStatus}" - pattern: "ProgressStatus" - - from: "SharepointToolbox/Views/MainWindow.xaml stub TabItems" - to: "SharepointToolbox/Views/Controls/FeatureTabBase.xaml" - via: "TabItem Content contains " - pattern: "FeatureTabBase" ---- - - -Build the WPF shell — MainWindow XAML + all ViewModels. Wire LogPanelSink to the RichTextBox. Implement FeatureViewModelBase with the canonical async pattern. Create FeatureTabBase UserControl (per-tab progress/cancel strip). 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. FeatureTabBase gives Phase 2+ a XAML template to extend rather than stub TextBlocks. -Output: Runnable WPF application showing the shell with placeholder tabs (using FeatureTabBase), log panel, and status bar with live operation text. - - - -@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 text | progress % -// Per-tab layout: ProgressBar + TextBlock + Button "Cancel" — shown only when IsRunning (CONTEXT.md Gray Areas, locked) - - - - - - - Task 1: FeatureViewModelBase + unit tests - - SharepointToolbox/Core/Messages/ProgressUpdatedMessage.cs, - 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: FeatureTabBase UserControl, MainWindowViewModel, shell ViewModels, and MainWindow XAML - - SharepointToolbox/Views/Controls/FeatureTabBase.xaml, - SharepointToolbox/Views/Controls/FeatureTabBase.xaml.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 - - - Create `Views/Controls/`, `ViewModels/Tabs/`, and `Views/` directories. - - **FeatureTabBase.xaml** — UserControl that every stub feature tab uses as its Content. - This gives Phase 2+ a concrete XAML template to replace rather than a bare TextBlock. - The progress/cancel strip is Visibility-bound to IsRunning per the locked CONTEXT.md decision. - - ```xml - - - - - - - - - - - - - - - - - - - - {HtmlEncode(node.Name)}" - : $"{HtmlEncode(node.Name)}"; - - string lastMod = node.LastModified.HasValue - ? node.LastModified.Value.ToString("yyyy-MM-dd") - : string.Empty; - - sb.AppendLine($""" - - {nameCell} - {HtmlEncode(node.SiteTitle)} - {node.TotalFileCount:N0} - {FormatSize(node.TotalSizeBytes)} - {FormatSize(node.VersionSizeBytes)} - {lastMod} - - """); - - if (hasChildren) - { - sb.AppendLine($""); - sb.AppendLine(""); - foreach (var child in node.Children) - { - RenderChildNode(sb, child); - } - sb.AppendLine("
"); - sb.AppendLine(""); - } - } - - private void RenderChildNode(StringBuilder sb, StorageNode node) - { - bool hasChildren = node.Children.Count > 0; - int myIdx = hasChildren ? ++_togIdx : 0; - - string indent = $"margin-left:{(node.IndentLevel + 1) * 16}px"; - string nameCell = hasChildren - ? $"{HtmlEncode(node.Name)}" - : $"{HtmlEncode(node.Name)}"; - - string lastMod = node.LastModified.HasValue - ? node.LastModified.Value.ToString("yyyy-MM-dd") - : string.Empty; - - sb.AppendLine($""" - - {nameCell} - {HtmlEncode(node.SiteTitle)} - {node.TotalFileCount:N0} - {FormatSize(node.TotalSizeBytes)} - {FormatSize(node.VersionSizeBytes)} - {lastMod} - - """); - - if (hasChildren) - { - sb.AppendLine($""); - sb.AppendLine(""); - foreach (var child in node.Children) - { - RenderChildNode(sb, child); - } - sb.AppendLine("
"); - sb.AppendLine(""); - } - } - - private static string FormatSize(long bytes) - { - if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB"; - if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB"; - if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB"; - return $"{bytes} B"; - } - - private static string HtmlEncode(string value) - => System.Net.WebUtility.HtmlEncode(value ?? string.Empty); -} -``` - -**Verification:** - -```bash -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~StorageHtmlExportServiceTests" -x -``` - -Expected: 3 tests pass - -## Verification - -```bash -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~StorageCsvExportServiceTests|FullyQualifiedName~StorageHtmlExportServiceTests" -x -``` - -Expected: 6 tests pass, 0 fail - -## Commit Message -feat(03-03): implement StorageCsvExportService and StorageHtmlExportService - -## Output - -After completion, create `.planning/phases/03-storage/03-03-SUMMARY.md` diff --git a/.planning/milestones/v1.0-phases/03-storage/03-03-SUMMARY.md b/.planning/milestones/v1.0-phases/03-storage/03-03-SUMMARY.md deleted file mode 100644 index 52f4e37..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-03-SUMMARY.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -phase: 03-storage -plan: "03" -subsystem: export -tags: [csv, html, storage, export, utf8-bom, collapsible-tree] - -requires: - - phase: 03-02 - provides: StorageService and StorageNode model with VersionSizeBytes derived property - -provides: - - StorageCsvExportService.BuildCsv — flat UTF-8 BOM CSV with 6-column header - - StorageHtmlExportService.BuildHtml — self-contained HTML with toggle(i) collapsible tree - - WriteAsync variants for both exporters - -affects: - - 03-07 (StorageViewModel wires export buttons to these services) - - 03-08 (StorageView integrates export UX) - -tech-stack: - added: [] - patterns: - - "RFC 4180 Csv() quoting helper — same pattern as Phase 2 CsvExportService" - - "HtmlEncode via System.Net.WebUtility.HtmlEncode" - - "toggle(i) + sf-{i} ID pattern for collapsible HTML rows" - - "_togIdx counter reset at BuildHtml start for unique IDs per call" - - "Explicit System.IO using required in WPF project (established pattern)" - -key-files: - created: [] - modified: - - SharepointToolbox/Services/Export/StorageCsvExportService.cs - - SharepointToolbox/Services/Export/StorageHtmlExportService.cs - -key-decisions: - - "Explicit System.IO using added to StorageCsvExportService and StorageHtmlExportService — WPF project does not include System.IO in implicit usings (existing project pattern)" - -patterns-established: - - "toggle(i) JS with sf-{i} row IDs for collapsible HTML export — reuse in SearchHtmlExportService (03-05)" - -requirements-completed: - - STOR-04 - - STOR-05 - -duration: 2min -completed: 2026-04-02 ---- - -# Phase 03 Plan 03: Storage Export Services — CSV and Collapsible-Tree HTML Summary - -**StorageCsvExportService (UTF-8 BOM flat CSV) and StorageHtmlExportService (self-contained collapsible-tree HTML) replace stubs — 6 tests pass** - -## Performance - -- **Duration:** 2 min -- **Started:** 2026-04-02T13:29:04Z -- **Completed:** 2026-04-02T13:30:43Z -- **Tasks:** 2 -- **Files modified:** 2 - -## Accomplishments - -- StorageCsvExportService.BuildCsv produces UTF-8 BOM CSV with header row: Library, Site, Files, Total Size (MB), Version Size (MB), Last Modified using RFC 4180 quoting -- StorageHtmlExportService.BuildHtml produces self-contained HTML with inline CSS/JS, toggle(i) function, and collapsible subfolder rows (sf-{i} IDs), ported from PS Export-StorageToHTML -- All 6 tests pass (3 CSV + 3 HTML) - -## Task Commits - -1. **Task 1: Implement StorageCsvExportService** - `94ff181` (feat) -2. **Task 2: Implement StorageHtmlExportService** - `eafaa15` (feat) - -## Files Created/Modified - -- `SharepointToolbox/Services/Export/StorageCsvExportService.cs` - Full BuildCsv implementation replacing string.Empty stub -- `SharepointToolbox/Services/Export/StorageHtmlExportService.cs` - Full BuildHtml implementation with collapsible tree rendering - -## Decisions Made - -- Explicit `System.IO` using added to both files — WPF project does not include System.IO in implicit usings; this is an established project pattern from Phase 1 - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 3 - Blocking] Added explicit System.IO using to StorageCsvExportService** -- **Found during:** Task 1 (StorageCsvExportService implementation) -- **Issue:** CS0103 — `File` not found; WPF project lacks System.IO in implicit usings -- **Fix:** Added `using System.IO;` at top of file -- **Files modified:** SharepointToolbox/Services/Export/StorageCsvExportService.cs -- **Verification:** Build succeeded, 3 CSV tests pass -- **Committed in:** `94ff181` (Task 1 commit) - -**2. [Rule 3 - Blocking] Added explicit System.IO using to StorageHtmlExportService** -- **Found during:** Task 2 (StorageHtmlExportService implementation) -- **Issue:** Same CS0103 pattern — File.WriteAllTextAsync requires System.IO -- **Fix:** Added `using System.IO;` preemptively before compilation -- **Files modified:** SharepointToolbox/Services/Export/StorageHtmlExportService.cs -- **Verification:** Build succeeded, 3 HTML tests pass -- **Committed in:** `eafaa15` (Task 2 commit) - ---- - -**Total deviations:** 2 auto-fixed (2 blocking — same root cause: WPF project implicit usings) -**Impact on plan:** Both fixes necessary for compilation. No scope creep. Consistent with established project pattern. - -## Issues Encountered - -The `-x` flag passed in the plan's dotnet test command is not a valid MSBuild switch. Omitting it works correctly — documented for future plans. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness - -- StorageCsvExportService and StorageHtmlExportService ready for use by StorageViewModel (Plan 03-07) -- Both services have WriteAsync variants for file-system output -- No blockers for Wave 2 parallel execution (03-04, 03-06 can proceed independently) - -## Self-Check: PASSED - -All files and commits verified present. - ---- -*Phase: 03-storage* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/03-storage/03-04-PLAN.md b/.planning/milestones/v1.0-phases/03-storage/03-04-PLAN.md deleted file mode 100644 index 0dc61bd..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-04-PLAN.md +++ /dev/null @@ -1,572 +0,0 @@ ---- -phase: 03 -plan: 04 -title: SearchService and DuplicatesService — KQL Pagination and Duplicate Grouping -status: pending -wave: 2 -depends_on: - - 03-01 -files_modified: - - SharepointToolbox/Services/SearchService.cs - - SharepointToolbox/Services/DuplicatesService.cs -autonomous: true -requirements: - - SRCH-01 - - SRCH-02 - - DUPL-01 - - DUPL-02 - -must_haves: - truths: - - "SearchService implements ISearchService and builds KQL from all SearchOptions fields (extension, dates, creator, editor, library)" - - "SearchService paginates StartRow += 500 and stops when StartRow > 50,000 (platform cap) or MaxResults reached" - - "SearchService filters out _vti_history/ paths from results" - - "SearchService applies client-side Regex filter when SearchOptions.Regex is non-empty" - - "DuplicatesService implements IDuplicatesService for both Mode=Files (Search API) and Mode=Folders (CAML FSObjType=1)" - - "DuplicatesService groups items by MakeKey composite key and returns only groups with count >= 2" - - "All CSOM round-trips use ExecuteQueryRetryHelper.ExecuteQueryRetryAsync" - - "Folder enumeration uses SharePointPaginationHelper.GetAllItemsAsync with FSObjType=1 CAML" - artifacts: - - path: "SharepointToolbox/Services/SearchService.cs" - provides: "KQL search engine with pagination (SRCH-01/02)" - exports: ["SearchService"] - - path: "SharepointToolbox/Services/DuplicatesService.cs" - provides: "Duplicate detection for files and folders (DUPL-01/02)" - exports: ["DuplicatesService"] - key_links: - - from: "SearchService.cs" - to: "KeywordQuery + SearchExecutor" - via: "Microsoft.SharePoint.Client.Search.Query" - pattern: "KeywordQuery" - - from: "DuplicatesService.cs" - to: "SharePointPaginationHelper.GetAllItemsAsync" - via: "folder enumeration" - pattern: "SharePointPaginationHelper\\.GetAllItemsAsync" - - from: "DuplicatesService.cs" - to: "MakeKey" - via: "composite key grouping" - pattern: "MakeKey" ---- - -# Plan 03-04: SearchService and DuplicatesService — KQL Pagination and Duplicate Grouping - -## Goal - -Implement `SearchService` (KQL-based file search with 500-row pagination and 50,000 hard cap) and `DuplicatesService` (file duplicates via Search API + folder duplicates via CAML `FSObjType=1`). Both services are wave 2 — they depend only on the models and interfaces from Plan 03-01, not on StorageService. - -## Context - -`Microsoft.SharePoint.Client.Search.dll` is available as a transitive dependency of PnP.Framework 1.18.0. The namespace is `Microsoft.SharePoint.Client.Search.Query`. The search pattern requires calling `executor.ExecuteQuery(kq)` to register the query, then `ExecuteQueryRetryHelper.ExecuteQueryRetryAsync` to execute it — calling `ctx.ExecuteQuery()` directly afterward is incorrect and must be avoided. - -`DuplicatesService` for folders uses `SharePointPaginationHelper.GetAllItemsAsync` with `FSObjType=1` CAML. The CAML field name is `FSObjType` (not `FileSystemObjectType`) — using the wrong name returns zero results silently. - -The `MakeKey` composite key logic tested in Plan 03-01 `DuplicatesServiceTests` must match exactly what `DuplicatesService` implements. - -## Tasks - -### Task 1: Implement SearchService - -**File:** `SharepointToolbox/Services/SearchService.cs` - -**Action:** Create - -**Why:** SRCH-01 (multi-criteria search) and SRCH-02 (configurable max results up to 50,000). - -```csharp -using Microsoft.SharePoint.Client; -using Microsoft.SharePoint.Client.Search.Query; -using SharepointToolbox.Core.Helpers; -using SharepointToolbox.Core.Models; -using System.Text.RegularExpressions; - -namespace SharepointToolbox.Services; - -/// -/// File search using SharePoint KQL Search API. -/// Port of PS Search-SPOFiles pattern (PS lines 4747-4987). -/// Pagination: 500 rows per batch, hard cap StartRow=50,000 (SharePoint Search boundary). -/// -public class SearchService : ISearchService -{ - private const int BatchSize = 500; - private const int MaxStartRow = 50_000; - - public async Task> SearchFilesAsync( - ClientContext ctx, - SearchOptions options, - IProgress progress, - CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - string kql = BuildKql(options); - ValidateKqlLength(kql); - - Regex? regexFilter = null; - if (!string.IsNullOrWhiteSpace(options.Regex)) - { - regexFilter = new Regex(options.Regex, - RegexOptions.IgnoreCase | RegexOptions.Compiled, - TimeSpan.FromSeconds(2)); - } - - var allResults = new List(); - int startRow = 0; - int maxResults = Math.Min(options.MaxResults, MaxStartRow); - - do - { - ct.ThrowIfCancellationRequested(); - - var kq = new KeywordQuery(ctx) - { - QueryText = kql, - StartRow = startRow, - RowLimit = BatchSize, - TrimDuplicates = false - }; - kq.SelectProperties.AddRange(new[] - { - "Title", "Path", "Author", "LastModifiedTime", - "FileExtension", "Created", "ModifiedBy", "Size" - }); - - var executor = new SearchExecutor(ctx); - ClientResult clientResult = executor.ExecuteQuery(kq); - await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct); - - var table = clientResult.Value - .FirstOrDefault(t => t.TableType == KnownTableTypes.RelevantResults); - if (table == null || table.RowCount == 0) break; - - foreach (System.Collections.Hashtable row in table.ResultRows) - { - var dict = row.Cast() - .ToDictionary(e => e.Key.ToString()!, e => e.Value ?? (object)string.Empty); - - // Skip SharePoint version history paths - string path = Str(dict, "Path"); - if (path.Contains("/_vti_history/", StringComparison.OrdinalIgnoreCase)) - continue; - - var result = ParseRow(dict); - - // Client-side Regex filter on file name - if (regexFilter != null) - { - string fileName = System.IO.Path.GetFileName(result.Path); - if (!regexFilter.IsMatch(fileName) && !regexFilter.IsMatch(result.Title)) - continue; - } - - allResults.Add(result); - if (allResults.Count >= maxResults) goto done; - } - - progress.Report(new OperationProgress(allResults.Count, maxResults, - $"Retrieved {allResults.Count:N0} results…")); - - startRow += BatchSize; - } - while (startRow <= MaxStartRow && allResults.Count < maxResults); - - done: - return allResults; - } - - // ── Extension point: bypassing the 50,000-item cap ─────────────────────── - // - // The StartRow approach has a hard ceiling at 50,000 (SharePoint Search boundary). - // To go beyond it, replace the StartRow loop with a DocId cursor: - // - // 1. Add "DocId" to SelectProperties. - // 2. Add query.SortList.Add("DocId", SortDirection.Ascending). - // 3. First page KQL: unchanged. - // Subsequent pages: append "AND DocId>{lastDocId}" to the KQL (StartRow stays 0). - // 4. Track lastDocId = Convert.ToInt64(lastRow["DocId"]) after each batch. - // 5. Stop when batch.RowCount < BatchSize. - // - // Caveats: - // - DocId is per-site-collection; for multi-site searches, maintain a separate - // cursor per ClientContext (site URL). - // - The search index can shift between batches (new items indexed mid-scan); - // the DocId cursor is safer than StartRow but cannot guarantee zero drift. - // - DocId is not returned by default — it must be in SelectProperties. - // - // This is deliberately not implemented here because SRCH-02 caps results at 50,000, - // which the StartRow approach already covers exactly (100 pages × 500 rows). - // Implement the DocId cursor if the cap needs to be lifted in a future version. - - // ── KQL builder ─────────────────────────────────────────────────────────── - - internal static string BuildKql(SearchOptions opts) - { - var parts = new List { "ContentType:Document" }; - - if (opts.Extensions.Length > 0) - { - var extParts = opts.Extensions - .Select(e => $"FileExtension:{e.TrimStart('.').ToLowerInvariant()}"); - parts.Add($"({string.Join(" OR ", extParts)})"); - } - if (opts.CreatedAfter.HasValue) - parts.Add($"Created>={opts.CreatedAfter.Value:yyyy-MM-dd}"); - if (opts.CreatedBefore.HasValue) - parts.Add($"Created<={opts.CreatedBefore.Value:yyyy-MM-dd}"); - if (opts.ModifiedAfter.HasValue) - parts.Add($"Write>={opts.ModifiedAfter.Value:yyyy-MM-dd}"); - if (opts.ModifiedBefore.HasValue) - parts.Add($"Write<={opts.ModifiedBefore.Value:yyyy-MM-dd}"); - if (!string.IsNullOrEmpty(opts.CreatedBy)) - parts.Add($"Author:\"{opts.CreatedBy}\""); - if (!string.IsNullOrEmpty(opts.ModifiedBy)) - parts.Add($"ModifiedBy:\"{opts.ModifiedBy}\""); - if (!string.IsNullOrEmpty(opts.Library) && !string.IsNullOrEmpty(opts.SiteUrl)) - parts.Add($"Path:\"{opts.SiteUrl.TrimEnd('/')}/{opts.Library.TrimStart('/')}*\""); - - return string.Join(" AND ", parts); - } - - private static void ValidateKqlLength(string kql) - { - // SharePoint Search KQL text hard cap is 4096 characters - if (kql.Length > 4096) - throw new InvalidOperationException( - $"KQL query exceeds 4096-character SharePoint Search limit ({kql.Length} chars). " + - "Reduce the number of extension filters."); - } - - // ── Row parser ──────────────────────────────────────────────────────────── - - private static SearchResult ParseRow(IDictionary row) - { - static string Str(IDictionary r, string key) => - r.TryGetValue(key, out var v) ? v?.ToString() ?? string.Empty : string.Empty; - - static DateTime? Date(IDictionary r, string key) - { - var s = Str(r, key); - return DateTime.TryParse(s, out var dt) ? dt : (DateTime?)null; - } - - static long ParseSize(IDictionary r, string key) - { - var raw = Str(r, key); - var digits = Regex.Replace(raw, "[^0-9]", ""); - return long.TryParse(digits, out var v) ? v : 0L; - } - - return new SearchResult - { - Title = Str(row, "Title"), - Path = Str(row, "Path"), - FileExtension = Str(row, "FileExtension"), - Created = Date(row, "Created"), - LastModified = Date(row, "LastModifiedTime"), - Author = Str(row, "Author"), - ModifiedBy = Str(row, "ModifiedBy"), - SizeBytes = ParseSize(row, "Size") - }; - } - - private static string Str(IDictionary r, string key) => - r.TryGetValue(key, out var v) ? v?.ToString() ?? string.Empty : string.Empty; -} -``` - -**Verification:** - -```bash -dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SearchServiceTests" -x -``` - -Expected: 0 build errors; CSOM tests skip, no compile errors - -### Task 2: Implement DuplicatesService - -**File:** `SharepointToolbox/Services/DuplicatesService.cs` - -**Action:** Create - -**Why:** DUPL-01 (file duplicates via Search API) and DUPL-02 (folder duplicates via CAML pagination). - -```csharp -using Microsoft.SharePoint.Client; -using Microsoft.SharePoint.Client.Search.Query; -using SharepointToolbox.Core.Helpers; -using SharepointToolbox.Core.Models; - -namespace SharepointToolbox.Services; - -/// -/// Duplicate file and folder detection. -/// Files: Search API (same KQL engine as SearchService) + client-side composite key grouping. -/// Folders: CSOM CAML FSObjType=1 via SharePointPaginationHelper + composite key grouping. -/// Port of PS Find-DuplicateFiles / Find-DuplicateFolders (PS lines 4942-5036). -/// -public class DuplicatesService : IDuplicatesService -{ - private const int BatchSize = 500; - private const int MaxStartRow = 50_000; - - public async Task> ScanDuplicatesAsync( - ClientContext ctx, - DuplicateScanOptions options, - IProgress progress, - CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - List allItems; - - if (options.Mode == "Folders") - allItems = await CollectFolderItemsAsync(ctx, options, progress, ct); - else - allItems = await CollectFileItemsAsync(ctx, options, progress, ct); - - progress.Report(OperationProgress.Indeterminate($"Grouping {allItems.Count:N0} items by duplicate key…")); - - var groups = allItems - .GroupBy(item => MakeKey(item, options)) - .Where(g => g.Count() >= 2) - .Select(g => new DuplicateGroup - { - GroupKey = g.Key, - Name = g.First().Name, - Items = g.ToList() - }) - .OrderByDescending(g => g.Items.Count) - .ThenBy(g => g.Name) - .ToList(); - - return groups; - } - - // ── File collection via Search API ──────────────────────────────────────── - - private static async Task> CollectFileItemsAsync( - ClientContext ctx, - DuplicateScanOptions options, - IProgress progress, - CancellationToken ct) - { - // KQL: all documents, optionally scoped to a library - var kqlParts = new List { "ContentType:Document" }; - if (!string.IsNullOrEmpty(options.Library)) - kqlParts.Add($"Path:\"{ctx.Url.TrimEnd('/')}/{options.Library.TrimStart('/')}*\""); - string kql = string.Join(" AND ", kqlParts); - - var allItems = new List(); - int startRow = 0; - - do - { - ct.ThrowIfCancellationRequested(); - - var kq = new KeywordQuery(ctx) - { - QueryText = kql, - StartRow = startRow, - RowLimit = BatchSize, - TrimDuplicates = false - }; - kq.SelectProperties.AddRange(new[] - { - "Title", "Path", "FileExtension", "Created", - "LastModifiedTime", "Size", "ParentLink" - }); - - var executor = new SearchExecutor(ctx); - ClientResult clientResult = executor.ExecuteQuery(kq); - await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct); - - var table = clientResult.Value - .FirstOrDefault(t => t.TableType == KnownTableTypes.RelevantResults); - if (table == null || table.RowCount == 0) break; - - foreach (System.Collections.Hashtable row in table.ResultRows) - { - var dict = row.Cast() - .ToDictionary(e => e.Key.ToString()!, e => e.Value ?? (object)string.Empty); - - string path = GetStr(dict, "Path"); - if (path.Contains("/_vti_history/", StringComparison.OrdinalIgnoreCase)) - continue; - - string name = System.IO.Path.GetFileName(path); - if (string.IsNullOrEmpty(name)) - name = GetStr(dict, "Title"); - - string raw = GetStr(dict, "Size"); - string digits = System.Text.RegularExpressions.Regex.Replace(raw, "[^0-9]", ""); - long size = long.TryParse(digits, out var sv) ? sv : 0L; - - DateTime? created = ParseDate(GetStr(dict, "Created")); - DateTime? modified = ParseDate(GetStr(dict, "LastModifiedTime")); - - // Derive library from ParentLink or path segments - string parentLink = GetStr(dict, "ParentLink"); - string library = ExtractLibraryFromPath(path, ctx.Url); - - allItems.Add(new DuplicateItem - { - Name = name, - Path = path, - Library = library, - SizeBytes = size, - Created = created, - Modified = modified - }); - } - - progress.Report(new OperationProgress(allItems.Count, MaxStartRow, - $"Collected {allItems.Count:N0} files…")); - - startRow += BatchSize; - } - while (startRow <= MaxStartRow); - - return allItems; - } - - // ── Folder collection via CAML ──────────────────────────────────────────── - - private static async Task> CollectFolderItemsAsync( - ClientContext ctx, - DuplicateScanOptions options, - IProgress progress, - CancellationToken ct) - { - // Load all document libraries on the site - ctx.Load(ctx.Web, - w => w.Lists.Include( - l => l.Title, l => l.Hidden, l => l.BaseType)); - await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct); - - var libs = ctx.Web.Lists - .Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary) - .ToList(); - - // Filter to specific library if requested - if (!string.IsNullOrEmpty(options.Library)) - { - libs = libs - .Where(l => l.Title.Equals(options.Library, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - - var camlQuery = new CamlQuery - { - ViewXml = """ - - - - - - 1 - - - - 2000 - - """ - }; - - var allItems = new List(); - - foreach (var lib in libs) - { - ct.ThrowIfCancellationRequested(); - progress.Report(OperationProgress.Indeterminate($"Scanning folders in {lib.Title}…")); - - await foreach (var item in SharePointPaginationHelper.GetAllItemsAsync(ctx, lib, camlQuery, ct)) - { - ct.ThrowIfCancellationRequested(); - - var fv = item.FieldValues; - string name = fv["FileLeafRef"]?.ToString() ?? string.Empty; - string fileRef = fv["FileRef"]?.ToString() ?? string.Empty; - int subCount = Convert.ToInt32(fv["FolderChildCount"] ?? 0); - int childCount = Convert.ToInt32(fv["ItemChildCount"] ?? 0); - int fileCount = Math.Max(0, childCount - subCount); - DateTime? created = fv["Created"] is DateTime cr ? cr : (DateTime?)null; - DateTime? modified = fv["Modified"] is DateTime md ? md : (DateTime?)null; - - allItems.Add(new DuplicateItem - { - Name = name, - Path = fileRef, - Library = lib.Title, - FolderCount = subCount, - FileCount = fileCount, - Created = created, - Modified = modified - }); - } - } - - return allItems; - } - - // ── Composite key builder (matches test scaffold in DuplicatesServiceTests) ── - - internal static string MakeKey(DuplicateItem item, DuplicateScanOptions opts) - { - var parts = new List { item.Name.ToLowerInvariant() }; - if (opts.MatchSize && item.SizeBytes.HasValue) parts.Add(item.SizeBytes.Value.ToString()); - if (opts.MatchCreated && item.Created.HasValue) parts.Add(item.Created.Value.Date.ToString("yyyy-MM-dd")); - if (opts.MatchModified && item.Modified.HasValue) parts.Add(item.Modified.Value.Date.ToString("yyyy-MM-dd")); - if (opts.MatchSubfolderCount && item.FolderCount.HasValue) parts.Add(item.FolderCount.Value.ToString()); - if (opts.MatchFileCount && item.FileCount.HasValue) parts.Add(item.FileCount.Value.ToString()); - return string.Join("|", parts); - } - - // ── Private utilities ───────────────────────────────────────────────────── - - private static string GetStr(IDictionary r, string key) => - r.TryGetValue(key, out var v) ? v?.ToString() ?? string.Empty : string.Empty; - - private static DateTime? ParseDate(string s) => - DateTime.TryParse(s, out var dt) ? dt : (DateTime?)null; - - private static string ExtractLibraryFromPath(string path, string siteUrl) - { - // Extract first path segment after the site URL as library name - // e.g. https://tenant.sharepoint.com/sites/MySite/Shared Documents/file.docx -> "Shared Documents" - if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(siteUrl)) - return string.Empty; - - string relative = path.StartsWith(siteUrl.TrimEnd('/'), StringComparison.OrdinalIgnoreCase) - ? path.Substring(siteUrl.TrimEnd('/').Length).TrimStart('/') - : path; - - int slash = relative.IndexOf('/'); - return slash > 0 ? relative.Substring(0, slash) : relative; - } -} -``` - -**Verification:** - -```bash -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~DuplicatesServiceTests" -x -``` - -Expected: 5 pure-logic tests pass (MakeKey), 2 CSOM stubs skip - -## Verification - -```bash -dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SearchServiceTests|FullyQualifiedName~DuplicatesServiceTests" -x -``` - -Expected: 0 build errors; 5 MakeKey tests pass; CSOM stub tests skip; no compile errors - -## Commit Message -feat(03-04): implement SearchService KQL pagination and DuplicatesService composite key grouping - -## Output - -After completion, create `.planning/phases/03-storage/03-04-SUMMARY.md` diff --git a/.planning/milestones/v1.0-phases/03-storage/03-04-SUMMARY.md b/.planning/milestones/v1.0-phases/03-storage/03-04-SUMMARY.md deleted file mode 100644 index cdfc514..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-04-SUMMARY.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -phase: 03-storage -plan: "04" -subsystem: search -tags: [csom, sharepoint-search, kql, duplicates, pagination] - -# Dependency graph -requires: - - phase: 03-01 - provides: ISearchService, IDuplicatesService, SearchOptions, DuplicateScanOptions, SearchResult, DuplicateItem, DuplicateGroup, OperationProgress models and interfaces - -provides: - - SearchService: KQL-based file search with 500-row pagination and 50,000-item hard cap - - DuplicatesService: file duplicates via Search API + folder duplicates via CAML FSObjType=1 - - MakeKey composite key logic for grouping duplicates by name+size+dates+counts - -affects: [03-05, 03-07, 03-08] - -# Tech tracking -tech-stack: - added: [] - patterns: - - "KeywordQuery + SearchExecutor pattern: executor.ExecuteQuery(kq) registers query, then ExecuteQueryRetryHelper.ExecuteQueryRetryAsync executes it" - - "StringCollection.Add loop: SelectProperties is StringCollection, not List — must add properties one-by-one" - - "StartRow pagination: += BatchSize per iteration, hard stop at MaxStartRow (50,000)" - - "goto done pattern for early exit from nested pagination loop when MaxResults reached" - -key-files: - created: - - SharepointToolbox/Services/SearchService.cs - - SharepointToolbox/Services/DuplicatesService.cs - modified: [] - -key-decisions: - - "SearchService uses SelectProperties.Add per-item loop — StringCollection has no AddRange(string[]) overload in this SDK version" - - "DuplicatesService.MakeKey internal static method matches inline test helper in DuplicatesServiceTests exactly — deliberate design to ensure test parity" - - "DuplicatesService file mode re-implements pagination inline (not delegating to SearchService) — avoids coupling between services with different result models" - -patterns-established: - - "KQL SelectProperties: Add each property in a foreach loop, never AddRange with array" - - "Search pagination: do/while with startRow <= MaxStartRow guard, break on empty table" - - "Folder CAML: FSObjType=1 (not FileSystemObjectType) — wrong name returns zero results" - -requirements-completed: [SRCH-01, SRCH-02, DUPL-01, DUPL-02] - -# Metrics -duration: 2min -completed: 2026-04-02 ---- - -# Phase 03 Plan 04: SearchService and DuplicatesService Summary - -**KQL file search with 500-row StartRow pagination (50k cap) and composite-key duplicate detection for files (Search API) and folders (CAML FSObjType=1)** - -## Performance - -- **Duration:** 2 min -- **Started:** 2026-04-02T14:09:25Z -- **Completed:** 2026-04-02T14:12:09Z -- **Tasks:** 2 -- **Files modified:** 2 created - -## Accomplishments - -- SearchService implements full KQL builder (extension, date range, creator, editor, library filters) with paginated retrieval up to 50,000 items -- DuplicatesService supports both file mode (Search API) and folder mode (CAML FSObjType=1) with client-side composite key grouping -- MakeKey logic matches the inline test scaffold from Plan 03-01 DuplicatesServiceTests — 5 pure-logic tests pass - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: Implement SearchService** - `9e3d501` (feat) -2. **Task 2: Implement DuplicatesService** - `df5f79d` (feat) - -## Files Created/Modified - -- `SharepointToolbox/Services/SearchService.cs` - KQL search with pagination, vti_history filter, regex client-side filter, KQL length validation -- `SharepointToolbox/Services/DuplicatesService.cs` - File/folder duplicate detection, MakeKey composite grouping, CAML folder enumeration - -## Decisions Made - -- `SelectProperties` is a `StringCollection` — `AddRange(string[])` does not compile. Fixed inline per-item `foreach` add loop (Rule 1 auto-fix applied during Task 1 first build). -- DuplicatesService re-implements file pagination inline rather than delegating to SearchService because result types differ (`DuplicateItem` vs `SearchResult`) and the two services have different lifecycles. -- `MakeKey` is `internal static` to match the test project's inline copy — enables verifying parity without a live CSOM context. - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 1 - Bug] StringCollection.AddRange(string[]) does not exist** -- **Found during:** Task 1 (SearchService build) -- **Issue:** `kq.SelectProperties.AddRange(new[] { ... })` — `SelectProperties` is `StringCollection` which has no `AddRange` taking `string[]`; extension method overload requires `List` receiver -- **Fix:** Replaced with `foreach` loop calling `kq.SelectProperties.Add(prop)` for each property name -- **Files modified:** `SharepointToolbox/Services/SearchService.cs`, `SharepointToolbox/Services/DuplicatesService.cs` -- **Verification:** `dotnet build` 0 errors after fix; same fix proactively applied in DuplicatesService before its first build -- **Committed in:** `9e3d501` (Task 1 commit) - ---- - -**Total deviations:** 1 auto-fixed (Rule 1 - bug) -**Impact on plan:** Minor API surface mismatch in the plan's code listing; fix is purely syntactic, no behavioral difference. - -## Issues Encountered - -- `dotnet test ... -x` flag not recognized by the `dotnet test` CLI on this machine (MSBuild switch error). Removed the flag; tests ran correctly without it. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness - -- SearchService and DuplicatesService are complete and compile cleanly -- Wave 2 is now ready for 03-05 (Search/Duplicate exports) and 03-06 (Localization) to proceed in parallel with 03-03 (Storage exports) -- 5 MakeKey tests pass; CSOM integration tests will remain skipped until a live tenant is available - ---- -*Phase: 03-storage* -*Completed: 2026-04-02* - -## Self-Check: PASSED - -- SharepointToolbox/Services/SearchService.cs: FOUND -- SharepointToolbox/Services/DuplicatesService.cs: FOUND -- .planning/phases/03-storage/03-04-SUMMARY.md: FOUND -- Commit 9e3d501 (SearchService): FOUND -- Commit df5f79d (DuplicatesService): FOUND diff --git a/.planning/milestones/v1.0-phases/03-storage/03-05-PLAN.md b/.planning/milestones/v1.0-phases/03-storage/03-05-PLAN.md deleted file mode 100644 index cc3f911..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-05-PLAN.md +++ /dev/null @@ -1,459 +0,0 @@ ---- -phase: 03 -plan: 05 -title: Search and Duplicate Export Services — CSV, Sortable HTML, and Grouped HTML -status: pending -wave: 3 -depends_on: - - 03-04 -files_modified: - - SharepointToolbox/Services/Export/SearchCsvExportService.cs - - SharepointToolbox/Services/Export/SearchHtmlExportService.cs - - SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs -autonomous: true -requirements: - - SRCH-03 - - SRCH-04 - - DUPL-03 - -must_haves: - truths: - - "SearchCsvExportService.BuildCsv produces a UTF-8 BOM CSV with header: File Name, Extension, Path, Created, Created By, Modified, Modified By, Size (bytes)" - - "SearchHtmlExportService.BuildHtml produces a self-contained HTML with sortable columns (click-to-sort JS) and a filter/search input" - - "DuplicatesHtmlExportService.BuildHtml produces a self-contained HTML with one card per group, showing item paths, and an ok/diff badge indicating group size" - - "SearchExportServiceTests: all 6 tests pass" - - "DuplicatesHtmlExportServiceTests: all 3 tests pass" - artifacts: - - path: "SharepointToolbox/Services/Export/SearchCsvExportService.cs" - provides: "CSV exporter for SearchResult list (SRCH-03)" - exports: ["SearchCsvExportService"] - - path: "SharepointToolbox/Services/Export/SearchHtmlExportService.cs" - provides: "Sortable/filterable HTML exporter for SearchResult list (SRCH-04)" - exports: ["SearchHtmlExportService"] - - path: "SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs" - provides: "Grouped HTML exporter for DuplicateGroup list (DUPL-03)" - exports: ["DuplicatesHtmlExportService"] - key_links: - - from: "SearchHtmlExportService.cs" - to: "sortTable JS" - via: "inline script" - pattern: "sort" - - from: "DuplicatesHtmlExportService.cs" - to: "group card HTML" - via: "per-DuplicateGroup rendering" - pattern: "group" ---- - -# Plan 03-05: Search and Duplicate Export Services — CSV, Sortable HTML, and Grouped HTML - -## Goal - -Replace the three stub export implementations created in Plan 03-01 with real ones. `SearchCsvExportService` produces a UTF-8 BOM CSV. `SearchHtmlExportService` ports the PS `Export-SearchToHTML` pattern (PS lines 2112-2233) with sortable columns and a live filter input. `DuplicatesHtmlExportService` ports the PS `Export-DuplicatesToHTML` pattern (PS lines 2235-2406) with grouped cards and ok/diff badges. - -## Context - -Test files `SearchExportServiceTests.cs` and `DuplicatesHtmlExportServiceTests.cs` already exist from Plan 03-01 and currently fail because stubs return `string.Empty`. This plan makes them pass. - -All HTML exports are self-contained (no external CDN or CSS links) using the same `Segoe UI` font stack and `#0078d4` color palette established in Phase 2. - -## Tasks - -### Task 1: Implement SearchCsvExportService and SearchHtmlExportService - -**Files:** -- `SharepointToolbox/Services/Export/SearchCsvExportService.cs` -- `SharepointToolbox/Services/Export/SearchHtmlExportService.cs` - -**Action:** Modify (replace stubs with full implementation) - -**Why:** SRCH-03 (CSV export) and SRCH-04 (sortable/filterable HTML export). - -```csharp -// SharepointToolbox/Services/Export/SearchCsvExportService.cs -using SharepointToolbox.Core.Models; -using System.Text; - -namespace SharepointToolbox.Services.Export; - -/// -/// Exports SearchResult list to a UTF-8 BOM CSV file. -/// Header matches the column order in SearchHtmlExportService for consistency. -/// -public class SearchCsvExportService -{ - public string BuildCsv(IReadOnlyList results) - { - var sb = new StringBuilder(); - - // Header - sb.AppendLine("File Name,Extension,Path,Created,Created By,Modified,Modified By,Size (bytes)"); - - foreach (var r in results) - { - sb.AppendLine(string.Join(",", - Csv(IfEmpty(System.IO.Path.GetFileName(r.Path), r.Title)), - Csv(r.FileExtension), - Csv(r.Path), - r.Created.HasValue ? Csv(r.Created.Value.ToString("yyyy-MM-dd")) : string.Empty, - Csv(r.Author), - r.LastModified.HasValue ? Csv(r.LastModified.Value.ToString("yyyy-MM-dd")) : string.Empty, - Csv(r.ModifiedBy), - r.SizeBytes.ToString())); - } - - return sb.ToString(); - } - - public async Task WriteAsync(IReadOnlyList results, string filePath, CancellationToken ct) - { - var csv = BuildCsv(results); - await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); - } - - private static string Csv(string value) - { - if (string.IsNullOrEmpty(value)) return string.Empty; - if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) - return $"\"{value.Replace("\"", "\"\"")}\""; - return value; - } - - private static string IfEmpty(string? value, string fallback = "") - => string.IsNullOrEmpty(value) ? fallback : value!; -} -``` - -```csharp -// SharepointToolbox/Services/Export/SearchHtmlExportService.cs -using SharepointToolbox.Core.Models; -using System.Text; - -namespace SharepointToolbox.Services.Export; - -/// -/// Exports SearchResult list to a self-contained sortable/filterable HTML report. -/// Port of PS Export-SearchToHTML (PS lines 2112-2233). -/// Columns are sortable by clicking the header. A filter input narrows rows by text match. -/// -public class SearchHtmlExportService -{ - public string BuildHtml(IReadOnlyList results) - { - var sb = new StringBuilder(); - - sb.AppendLine(""" - - - - - - SharePoint File Search Results - - - -

File Search Results

-
- - - -
- """); - - sb.AppendLine(""" - - - - - - - - - - - - - - - """); - - foreach (var r in results) - { - string fileName = System.IO.Path.GetFileName(r.Path); - if (string.IsNullOrEmpty(fileName)) fileName = r.Title; - - sb.AppendLine($""" - - - - - - - - - - - """); - } - - sb.AppendLine(" \n
File NameExtensionPathCreatedCreated ByModifiedModified BySize
{H(fileName)}{H(r.FileExtension)}{H(r.Path)}{(r.Created.HasValue ? r.Created.Value.ToString("yyyy-MM-dd") : string.Empty)}{H(r.Author)}{(r.LastModified.HasValue ? r.LastModified.Value.ToString("yyyy-MM-dd") : string.Empty)}{H(r.ModifiedBy)}{FormatSize(r.SizeBytes)}
"); - - // Inline sort + filter JS - sb.AppendLine($$""" -

Generated: {{DateTime.Now:yyyy-MM-dd HH:mm}} — {{results.Count:N0}} result(s)

- - - """); - - return sb.ToString(); - } - - public async Task WriteAsync(IReadOnlyList results, string filePath, CancellationToken ct) - { - var html = BuildHtml(results); - await File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct); - } - - private static string H(string value) => - System.Net.WebUtility.HtmlEncode(value ?? string.Empty); - - private static string FormatSize(long bytes) - { - if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB"; - if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB"; - if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB"; - return $"{bytes} B"; - } -} -``` - -**Verification:** - -```bash -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SearchExportServiceTests" -x -``` - -Expected: 6 tests pass - -### Task 2: Implement DuplicatesHtmlExportService - -**File:** `SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs` - -**Action:** Modify (replace stub with full implementation) - -**Why:** DUPL-03 — user can export duplicate report to HTML with grouped display and visual indicators. - -```csharp -// SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs -using SharepointToolbox.Core.Models; -using System.Text; - -namespace SharepointToolbox.Services.Export; - -/// -/// Exports DuplicateGroup list to a self-contained HTML with collapsible group cards. -/// Port of PS Export-DuplicatesToHTML (PS lines 2235-2406). -/// Each group gets a card showing item count badge and a table of paths. -/// -public class DuplicatesHtmlExportService -{ - public string BuildHtml(IReadOnlyList groups) - { - var sb = new StringBuilder(); - - sb.AppendLine(""" - - - - - - SharePoint Duplicate Detection Report - - - - -

Duplicate Detection Report

- """); - - sb.AppendLine($"

{groups.Count:N0} duplicate group(s) found.

"); - - for (int i = 0; i < groups.Count; i++) - { - var g = groups[i]; - int count = g.Items.Count; - string badgeClass = "badge-dup"; - - sb.AppendLine($""" -
-
- {H(g.Name)} - {count} copies -
-
- - - - - - - - - - - - - """); - - for (int j = 0; j < g.Items.Count; j++) - { - var item = g.Items[j]; - string size = item.SizeBytes.HasValue ? FormatSize(item.SizeBytes.Value) : string.Empty; - string created = item.Created.HasValue ? item.Created.Value.ToString("yyyy-MM-dd") : string.Empty; - string modified = item.Modified.HasValue ? item.Modified.Value.ToString("yyyy-MM-dd") : string.Empty; - - sb.AppendLine($""" - - - - - - - - - """); - } - - sb.AppendLine(""" - -
#LibraryPathSizeCreatedModified
{j + 1}{H(item.Library)}{H(item.Path)}{size}{created}{modified}
-
-
- """); - } - - sb.AppendLine($"

Generated: {DateTime.Now:yyyy-MM-dd HH:mm}

"); - sb.AppendLine(""); - - return sb.ToString(); - } - - public async Task WriteAsync(IReadOnlyList groups, string filePath, CancellationToken ct) - { - var html = BuildHtml(groups); - await File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct); - } - - private static string H(string value) => - System.Net.WebUtility.HtmlEncode(value ?? string.Empty); - - private static string FormatSize(long bytes) - { - if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB"; - if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB"; - if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB"; - return $"{bytes} B"; - } -} -``` - -**Verification:** - -```bash -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~DuplicatesHtmlExportServiceTests" -x -``` - -Expected: 3 tests pass - -## Verification - -```bash -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SearchExportServiceTests|FullyQualifiedName~DuplicatesHtmlExportServiceTests" -x -``` - -Expected: 9 tests pass, 0 fail - -## Commit Message -feat(03-05): implement SearchCsvExportService, SearchHtmlExportService, DuplicatesHtmlExportService - -## Output - -After completion, create `.planning/phases/03-storage/03-05-SUMMARY.md` diff --git a/.planning/milestones/v1.0-phases/03-storage/03-05-SUMMARY.md b/.planning/milestones/v1.0-phases/03-storage/03-05-SUMMARY.md deleted file mode 100644 index 28bc492..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-05-SUMMARY.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -phase: 03-storage -plan: 05 -subsystem: export -tags: [csharp, csv, html, search, duplicates, export] - -# Dependency graph -requires: - - phase: 03-01 - provides: export stubs and test scaffolds for SearchCsvExportService, SearchHtmlExportService, DuplicatesHtmlExportService - - phase: 03-04 - provides: SearchResult and DuplicateGroup models consumed by exporters -provides: - - SearchCsvExportService: UTF-8 BOM CSV with 8-column header for SearchResult list - - SearchHtmlExportService: self-contained sortable/filterable HTML report for SearchResult list - - DuplicatesHtmlExportService: grouped card HTML report for DuplicateGroup list -affects: [03-08, SearchViewModel, DuplicatesViewModel] - -# Tech tracking -tech-stack: - added: [] - patterns: - - "System.IO.File used explicitly in WPF project (no implicit using for System.IO)" - - "Self-contained HTML exports with inline CSS + JS (no external CDN dependencies)" - - "Segoe UI font stack and #0078d4 color palette consistent across all Phase 2/3 HTML exports" - -key-files: - created: [] - modified: - - SharepointToolbox/Services/Export/SearchCsvExportService.cs - - SharepointToolbox/Services/Export/SearchHtmlExportService.cs - - SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs - -key-decisions: - - "SearchCsvExportService uses UTF-8 BOM (encoderShouldEmitUTF8Identifier: true) for Excel compatibility" - - "SearchHtmlExportService result count rendered at generation time (not via JS variable) to avoid C# interpolation conflicts with JS template strings" - - "DuplicatesHtmlExportService always uses badge-dup class (red) — no ok/diff distinction needed per DUPL-03" - -patterns-established: - - "sortTable(col) JS function: uses data-sort attribute for numeric columns (Size), falls back to innerText" - - "filterTable() JS function: hides rows by adding 'hidden' class, updates result count display" - - "Group cards use toggleGroup(id) with collapsed CSS class for collapsible behavior" - -requirements-completed: [SRCH-03, SRCH-04, DUPL-03] - -# Metrics -duration: 4min -completed: 2026-04-02 ---- - -# Phase 03 Plan 05: Search and Duplicate Export Services Summary - -**SearchCsvExportService (UTF-8 BOM CSV), SearchHtmlExportService (sortable/filterable HTML), and DuplicatesHtmlExportService (grouped card HTML) — all 9 export tests pass** - -## Performance - -- **Duration:** 4 min -- **Started:** 2026-04-02T13:34:47Z -- **Completed:** 2026-04-02T13:38:47Z -- **Tasks:** 2 -- **Files modified:** 3 - -## Accomplishments - -- SearchCsvExportService: UTF-8 BOM CSV with proper 8-column header and RFC 4180 CSV escaping -- SearchHtmlExportService: self-contained HTML with click-to-sort columns and live filter input, ported from PS Export-SearchToHTML -- DuplicatesHtmlExportService: collapsible group cards with item count badges and path tables, ported from PS Export-DuplicatesToHTML - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: SearchCsvExportService + SearchHtmlExportService** - `e174a18` (feat, part of 03-07 session) -2. **Task 2: DuplicatesHtmlExportService** - `fc1ba00` (feat) - -**Plan metadata:** (see final docs commit) - -## Files Created/Modified - -- `SharepointToolbox/Services/Export/SearchCsvExportService.cs` - UTF-8 BOM CSV exporter for SearchResult list (SRCH-03) -- `SharepointToolbox/Services/Export/SearchHtmlExportService.cs` - Sortable/filterable HTML exporter for SearchResult list (SRCH-04) -- `SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs` - Grouped card HTML exporter for DuplicateGroup list (DUPL-03) - -## Decisions Made - -- `SearchCsvExportService` uses `UTF8Encoding(encoderShouldEmitUTF8Identifier: true)` for Excel compatibility — consistent with Phase 2 CsvExportService pattern -- Result count in `SearchHtmlExportService` is rendered as a C# interpolated string at generation time rather than a JS variable — avoids conflict between C# `$$"""` interpolation and JS template literal syntax -- `DuplicatesHtmlExportService` uses `badge-dup` (red) for all groups — DUPL-03 requires showing copies count; ok/diff distinction was removed from final spec - -## Deviations from Plan - -### Auto-fixed Issues - -**1. [Rule 1 - Bug] Fixed implicit `File` class resolution in WPF project** -- **Found during:** Task 1 (SearchCsvExportService and SearchHtmlExportService) -- **Issue:** `File.WriteAllTextAsync` fails to compile — WPF project does not include `System.IO` in implicit usings (established project pattern documented in STATE.md decisions) -- **Fix:** Changed `File.WriteAllTextAsync` to `System.IO.File.WriteAllTextAsync` in both services -- **Files modified:** SearchCsvExportService.cs, SearchHtmlExportService.cs -- **Verification:** Test project builds successfully; 6/6 SearchExportServiceTests pass -- **Committed in:** e174a18 (Task 1 commit, part of 03-07 session) - ---- - -**Total deviations:** 1 auto-fixed (Rule 1 — known WPF project pattern) -**Impact on plan:** Necessary correctness fix. No scope creep. - -## Issues Encountered - -- Task 1 (SearchCsvExportService + SearchHtmlExportService) was already committed in the prior `feat(03-07)` session — the plan was executed out of order. Task 2 (DuplicatesHtmlExportService) was the only remaining work in this session. -- WPF temp project (`_wpftmp.csproj`) showed pre-existing errors for `StorageView` and `ClientRuntimeContext.Url` during build attempts — these are pre-existing blockers from plan 03-07 state (StorageView untracked, not in scope for this plan). Used `dotnet build SharepointToolbox.Tests/` directly to avoid them. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness - -- All 3 export services are fully implemented and tested (9/9 tests pass) -- SearchViewModel and DuplicatesViewModel (plan 03-08) can now wire export commands to these services -- StorageView.xaml is untracked (created in 03-07 session) — needs to be committed before plan 03-08 runs - ---- -*Phase: 03-storage* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/03-storage/03-06-PLAN.md b/.planning/milestones/v1.0-phases/03-storage/03-06-PLAN.md deleted file mode 100644 index 0fe91af..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-06-PLAN.md +++ /dev/null @@ -1,301 +0,0 @@ ---- -phase: 03 -plan: 06 -title: Localization — Phase 3 EN and FR Keys -status: pending -wave: 2 -depends_on: - - 03-01 -files_modified: - - SharepointToolbox/Localization/Strings.resx - - SharepointToolbox/Localization/Strings.fr.resx - - SharepointToolbox/Localization/Strings.Designer.cs -autonomous: true -requirements: - - STOR-01 - - STOR-02 - - STOR-04 - - STOR-05 - - SRCH-01 - - SRCH-02 - - SRCH-03 - - SRCH-04 - - DUPL-01 - - DUPL-02 - - DUPL-03 - -must_haves: - truths: - - "All Phase 3 EN keys exist in Strings.resx" - - "All Phase 3 FR keys exist in Strings.fr.resx with non-empty French values" - - "Strings.Designer.cs has one static property per new key (dot-to-underscore naming: chk.per.lib -> chk_per_lib)" - - "dotnet build produces 0 errors after localization changes" - - "No existing Phase 2 or Phase 1 keys are modified or removed" - artifacts: - - path: "SharepointToolbox/Localization/Strings.resx" - provides: "English localization for Phase 3 tabs" - - path: "SharepointToolbox/Localization/Strings.fr.resx" - provides: "French localization for Phase 3 tabs" - - path: "SharepointToolbox/Localization/Strings.Designer.cs" - provides: "Strongly-typed accessors for new keys" - key_links: - - from: "Strings.Designer.cs" - to: "Strings.resx" - via: "ResourceManager.GetString" - pattern: "ResourceManager\\.GetString" ---- - -# Plan 03-06: Localization — Phase 3 EN and FR Keys - -## Goal - -Add all EN and FR localization keys needed by the Storage, File Search, and Duplicates tabs. Views in plans 03-07 and 03-08 reference these keys via `TranslationSource.Instance["key"]` XAML bindings. Keys must exist before the Views compile. - -## Context - -Strings.resx uses a manually maintained `Strings.Designer.cs` (no ResXFileCodeGenerator — confirmed in Phase 1 decisions). The naming convention converts dots to underscores: key `chk.per.lib` becomes accessor `Strings.chk_per_lib`. Both `.resx` files use `xml:space="preserve"` on each `` element. The following keys already exist and must NOT be duplicated: `tab.storage`, `tab.search`, `tab.duplicates`, `lbl.folder.depth`, `chk.max.depth`. - -> **Pre-existing keys — do not add:** The following keys are confirmed present in `Strings.resx` from Phase 2 and must be skipped when editing both `.resx` files and `Strings.Designer.cs`: -> - `grp.scan.opts` (value: "Scan Options") — already exists -> - `grp.export.fmt` (value: "Export Format") — already exists -> - `btn.cancel` (value: "Cancel") — already exists -> -> Before appending, verify with: `grep -n "grp.scan.opts\|grp.export.fmt\|btn.cancel" SharepointToolbox/Localization/Strings.resx` -> Do not add designer properties for these keys if they already exist in `Strings.Designer.cs`. - -## Tasks - -### Task 1: Add Phase 3 keys to Strings.resx, Strings.fr.resx, and Strings.Designer.cs - -**Files:** -- `SharepointToolbox/Localization/Strings.resx` -- `SharepointToolbox/Localization/Strings.fr.resx` -- `SharepointToolbox/Localization/Strings.Designer.cs` - -**Action:** Modify — append new `` elements before `` in both .resx files; append new properties before the closing `}` in Strings.Designer.cs - -**Why:** Views in plans 03-07 and 03-08 bind to these keys. Missing keys produce empty strings at runtime. - -Add these entries immediately before the closing `` tag in `Strings.resx`: - -```xml - - Per-Library Breakdown - Include Subsites - Note: deeper folder scans on large sites may take several minutes. - Generate Metrics - Open Report - Library - Site - Files - Total Size - Version Size - Last Modified - Share of Total - CSV - HTML - - Search Filters - Extension(s): - docx pdf xlsx - Name / Regex: - Ex: report.* or \.bak$ - Created after: - Created before: - Modified after: - Modified before: - Created by: - First Last or email - Modified by: - First Last or email - Library: - Optional relative path e.g. Shared Documents - Max results: - Site URL: - https://tenant.sharepoint.com/sites/MySite - Run Search - Open Results - File Name - Extension - Created - Modified - Created By - Modified By - Size - Path - CSV - HTML - - Duplicate Type - Duplicate files - Duplicate folders - Comparison Criteria - Name is always the primary criterion. Check additional criteria: - Same size - Same creation date - Same modification date - Same subfolder count - Same file count - Include subsites - All (leave empty) - Run Scan - Open Results -``` - -Add these entries immediately before the closing `` tag in `Strings.fr.resx`: - -```xml - - Détail par bibliothèque - Inclure les sous-sites - Remarque : les analyses de dossiers profondes sur les grands sites peuvent prendre plusieurs minutes. - Générer les métriques - Ouvrir le rapport - Bibliothèque - Site - Fichiers - Taille totale - Taille des versions - Dernière modification - Part du total - CSV - HTML - - Filtres de recherche - Extension(s) : - docx pdf xlsx - Nom / Regex : - Ex : rapport.* ou \.bak$ - Créé après : - Créé avant : - Modifié après : - Modifié avant : - Créé par : - Prénom Nom ou courriel - Modifié par : - Prénom Nom ou courriel - Bibliothèque : - Chemin relatif optionnel, ex. Documents partagés - Max résultats : - URL du site : - https://tenant.sharepoint.com/sites/MonSite - Lancer la recherche - Ouvrir les résultats - Nom du fichier - Extension - Créé - Modifié - Créé par - Modifié par - Taille - Chemin - CSV - HTML - - Type de doublon - Fichiers en doublon - Dossiers en doublon - Critères de comparaison - Le nom est toujours le critère principal. Cochez des critères supplémentaires : - Même taille - Même date de création - Même date de modification - Même nombre de sous-dossiers - Même nombre de fichiers - Inclure les sous-sites - Tous (laisser vide) - Lancer l'analyse - Ouvrir les résultats -``` - -Add these properties inside the `Strings` class in `Strings.Designer.cs` (before the closing `}`): - -```csharp - // Phase 3: Storage Tab - public static string chk_per_lib => ResourceManager.GetString("chk.per.lib", resourceCulture) ?? string.Empty; - public static string chk_subsites => ResourceManager.GetString("chk.subsites", resourceCulture) ?? string.Empty; - public static string stor_note => ResourceManager.GetString("stor.note", resourceCulture) ?? string.Empty; - public static string btn_gen_storage => ResourceManager.GetString("btn.gen.storage", resourceCulture) ?? string.Empty; - public static string btn_open_storage => ResourceManager.GetString("btn.open.storage", resourceCulture) ?? string.Empty; - public static string stor_col_library => ResourceManager.GetString("stor.col.library", resourceCulture) ?? string.Empty; - public static string stor_col_site => ResourceManager.GetString("stor.col.site", resourceCulture) ?? string.Empty; - public static string stor_col_files => ResourceManager.GetString("stor.col.files", resourceCulture) ?? string.Empty; - public static string stor_col_size => ResourceManager.GetString("stor.col.size", resourceCulture) ?? string.Empty; - public static string stor_col_versions => ResourceManager.GetString("stor.col.versions", resourceCulture) ?? string.Empty; - public static string stor_col_lastmod => ResourceManager.GetString("stor.col.lastmod", resourceCulture) ?? string.Empty; - public static string stor_col_share => ResourceManager.GetString("stor.col.share", resourceCulture) ?? string.Empty; - public static string stor_rad_csv => ResourceManager.GetString("stor.rad.csv", resourceCulture) ?? string.Empty; - public static string stor_rad_html => ResourceManager.GetString("stor.rad.html", resourceCulture) ?? string.Empty; - - // Phase 3: File Search Tab - public static string grp_search_filters => ResourceManager.GetString("grp.search.filters", resourceCulture) ?? string.Empty; - public static string lbl_extensions => ResourceManager.GetString("lbl.extensions", resourceCulture) ?? string.Empty; - public static string ph_extensions => ResourceManager.GetString("ph.extensions", resourceCulture) ?? string.Empty; - public static string lbl_regex => ResourceManager.GetString("lbl.regex", resourceCulture) ?? string.Empty; - public static string ph_regex => ResourceManager.GetString("ph.regex", resourceCulture) ?? string.Empty; - public static string chk_created_after => ResourceManager.GetString("chk.created.after", resourceCulture) ?? string.Empty; - public static string chk_created_before => ResourceManager.GetString("chk.created.before", resourceCulture) ?? string.Empty; - public static string chk_modified_after => ResourceManager.GetString("chk.modified.after", resourceCulture) ?? string.Empty; - public static string chk_modified_before => ResourceManager.GetString("chk.modified.before", resourceCulture) ?? string.Empty; - public static string lbl_created_by => ResourceManager.GetString("lbl.created.by", resourceCulture) ?? string.Empty; - public static string ph_created_by => ResourceManager.GetString("ph.created.by", resourceCulture) ?? string.Empty; - public static string lbl_modified_by => ResourceManager.GetString("lbl.modified.by", resourceCulture) ?? string.Empty; - public static string ph_modified_by => ResourceManager.GetString("ph.modified.by", resourceCulture) ?? string.Empty; - public static string lbl_library => ResourceManager.GetString("lbl.library", resourceCulture) ?? string.Empty; - public static string ph_library => ResourceManager.GetString("ph.library", resourceCulture) ?? string.Empty; - public static string lbl_max_results => ResourceManager.GetString("lbl.max.results", resourceCulture) ?? string.Empty; - public static string lbl_site_url => ResourceManager.GetString("lbl.site.url", resourceCulture) ?? string.Empty; - public static string ph_site_url => ResourceManager.GetString("ph.site.url", resourceCulture) ?? string.Empty; - public static string btn_run_search => ResourceManager.GetString("btn.run.search", resourceCulture) ?? string.Empty; - public static string btn_open_search => ResourceManager.GetString("btn.open.search", resourceCulture) ?? string.Empty; - public static string srch_col_name => ResourceManager.GetString("srch.col.name", resourceCulture) ?? string.Empty; - public static string srch_col_ext => ResourceManager.GetString("srch.col.ext", resourceCulture) ?? string.Empty; - public static string srch_col_created => ResourceManager.GetString("srch.col.created", resourceCulture) ?? string.Empty; - public static string srch_col_modified => ResourceManager.GetString("srch.col.modified", resourceCulture) ?? string.Empty; - public static string srch_col_author => ResourceManager.GetString("srch.col.author", resourceCulture) ?? string.Empty; - public static string srch_col_modby => ResourceManager.GetString("srch.col.modby", resourceCulture) ?? string.Empty; - public static string srch_col_size => ResourceManager.GetString("srch.col.size", resourceCulture) ?? string.Empty; - public static string srch_col_path => ResourceManager.GetString("srch.col.path", resourceCulture) ?? string.Empty; - public static string srch_rad_csv => ResourceManager.GetString("srch.rad.csv", resourceCulture) ?? string.Empty; - public static string srch_rad_html => ResourceManager.GetString("srch.rad.html", resourceCulture) ?? string.Empty; - - // Phase 3: Duplicates Tab - public static string grp_dup_type => ResourceManager.GetString("grp.dup.type", resourceCulture) ?? string.Empty; - public static string rad_dup_files => ResourceManager.GetString("rad.dup.files", resourceCulture) ?? string.Empty; - public static string rad_dup_folders => ResourceManager.GetString("rad.dup.folders", resourceCulture) ?? string.Empty; - public static string grp_dup_criteria => ResourceManager.GetString("grp.dup.criteria", resourceCulture) ?? string.Empty; - public static string lbl_dup_note => ResourceManager.GetString("lbl.dup.note", resourceCulture) ?? string.Empty; - public static string chk_dup_size => ResourceManager.GetString("chk.dup.size", resourceCulture) ?? string.Empty; - public static string chk_dup_created => ResourceManager.GetString("chk.dup.created", resourceCulture) ?? string.Empty; - public static string chk_dup_modified => ResourceManager.GetString("chk.dup.modified", resourceCulture) ?? string.Empty; - public static string chk_dup_subfolders => ResourceManager.GetString("chk.dup.subfolders", resourceCulture) ?? string.Empty; - public static string chk_dup_filecount => ResourceManager.GetString("chk.dup.filecount", resourceCulture) ?? string.Empty; - public static string chk_include_subsites => ResourceManager.GetString("chk.include.subsites", resourceCulture) ?? string.Empty; - public static string ph_dup_lib => ResourceManager.GetString("ph.dup.lib", resourceCulture) ?? string.Empty; - public static string btn_run_scan => ResourceManager.GetString("btn.run.scan", resourceCulture) ?? string.Empty; - public static string btn_open_results => ResourceManager.GetString("btn.open.results", resourceCulture) ?? string.Empty; -``` - -**Verification:** - -```bash -dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx -``` - -Expected: 0 errors - -## Verification - -```bash -dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx -dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj -x 2>&1 | tail -5 -``` - -Expected: 0 build errors; all previously passing tests still pass; no new failures - -## Commit Message -feat(03-06): add Phase 3 EN/FR localization keys for Storage, Search, and Duplicates tabs - -## Output - -After completion, create `.planning/phases/03-storage/03-06-SUMMARY.md` diff --git a/.planning/milestones/v1.0-phases/03-storage/03-06-SUMMARY.md b/.planning/milestones/v1.0-phases/03-storage/03-06-SUMMARY.md deleted file mode 100644 index 32891b3..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-06-SUMMARY.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -phase: 03-storage -plan: 06 -subsystem: ui -tags: [localization, resx, wpf, csharp, fr, en] - -# Dependency graph -requires: - - phase: 03-01 - provides: Models, interfaces, and project structure for Phase 3 tabs - -provides: - - EN and FR localization keys for Storage tab (14 keys each) - - EN and FR localization keys for File Search tab (26 keys each) - - EN and FR localization keys for Duplicates tab (14 keys each) - - Strongly-typed Strings.Designer.cs accessors for all 54 new keys - -affects: - - 03-07 (StorageViewModel/View — binds to storage keys via TranslationSource) - - 03-08 (SearchViewModel + DuplicatesViewModel + Views — binds to search/duplicates keys) - -# Tech tracking -tech-stack: - added: [] - patterns: - - "Dot-to-underscore key naming: key 'chk.per.lib' becomes accessor 'Strings.chk_per_lib'" - - "Manual Strings.Designer.cs maintenance (no ResXFileCodeGenerator — VS-only tool)" - - "Both .resx files use xml:space='preserve' on each element" - - "New keys appended before with comment block grouping by tab" - -key-files: - created: [] - modified: - - SharepointToolbox/Localization/Strings.resx - - SharepointToolbox/Localization/Strings.fr.resx - - SharepointToolbox/Localization/Strings.Designer.cs - -key-decisions: - - "Pre-existing keys grp.scan.opts, grp.export.fmt, btn.cancel verified present — not duplicated" - - "54 new designer properties follow established dot-to-underscore naming convention" - -patterns-established: - - "Phase grouping with XML comments: , , " - -requirements-completed: [STOR-01, STOR-02, STOR-04, STOR-05, SRCH-01, SRCH-02, SRCH-03, SRCH-04, DUPL-01, DUPL-02, DUPL-03] - -# Metrics -duration: 5min -completed: 2026-04-02 ---- - -# Phase 03 Plan 06: Localization — Phase 3 EN and FR Keys Summary - -**54 new EN/FR localization keys added across Storage, File Search, and Duplicates tabs with strongly-typed Strings.Designer.cs accessors using dot-to-underscore naming convention** - -## Performance - -- **Duration:** ~5 min -- **Started:** 2026-04-02T13:27:00Z -- **Completed:** 2026-04-02T13:31:33Z -- **Tasks:** 1 -- **Files modified:** 3 - -## Accomplishments -- Added 14 Storage tab keys in both EN (Strings.resx) and FR (Strings.fr.resx): per-library breakdown, subsites, note, generate/open buttons, 7 column headers, 2 radio buttons -- Added 26 File Search tab keys in both EN and FR: search filters group, extensions/regex/date filters, creator/modifier inputs, library filter, site URL, run/open buttons, 8 column headers, 2 radio buttons -- Added 14 Duplicates tab keys in both EN and FR: duplicate type radio buttons, comparison criteria group, 5 criteria checkboxes, subsites checkbox, library placeholder, run/open buttons -- Added 54 static properties to Strings.Designer.cs following established dot-to-underscore naming convention -- Build verified: 0 errors after all localization changes - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: Add Phase 3 keys to Strings.resx, Strings.fr.resx, and Strings.Designer.cs** - `938de30` (feat) - -**Plan metadata:** (to be added by final commit) - -## Files Created/Modified -- `SharepointToolbox/Localization/Strings.resx` - 54 new EN data entries for Phase 3 tabs -- `SharepointToolbox/Localization/Strings.fr.resx` - 54 new FR data entries for Phase 3 tabs -- `SharepointToolbox/Localization/Strings.Designer.cs` - 54 new static property accessors - -## Decisions Made -None - followed plan as specified. Pre-existing keys verified with git stash/pop workflow to confirm build was clean before changes, and test failures confirmed pre-existing (from export service stubs planned for 03-03/03-05). - -## Deviations from Plan - -None - plan executed exactly as written. - -**Note:** Build had a transient CS1929 error on first invocation (stale compiled artifacts). Second `dotnet build` succeeded with 0 errors. The 9 test failures are pre-existing (export service stubs from plans 03-03/03-05, verified by stashing changes). - -## Issues Encountered -- Transient build error CS1929 on first `dotnet build` invocation (stale .NET temp project files). Resolved automatically on second build. - -## User Setup Required - -None - no external service configuration required. - -## Next Phase Readiness -- All Phase 3 localization keys now present — plans 03-07 and 03-08 can use `TranslationSource.Instance["key"]` XAML bindings without missing-key issues -- Wave 3: StorageViewModel/View (03-07) is unblocked -- Wave 4: SearchViewModel + DuplicatesViewModel + Views (03-08) is unblocked - -## Self-Check: PASSED - -- FOUND: SharepointToolbox/Localization/Strings.resx -- FOUND: SharepointToolbox/Localization/Strings.fr.resx -- FOUND: SharepointToolbox/Localization/Strings.Designer.cs -- FOUND: .planning/phases/03-storage/03-06-SUMMARY.md -- FOUND: commit 938de30 - ---- -*Phase: 03-storage* -*Completed: 2026-04-02* diff --git a/.planning/milestones/v1.0-phases/03-storage/03-07-PLAN.md b/.planning/milestones/v1.0-phases/03-storage/03-07-PLAN.md deleted file mode 100644 index dd2ee03..0000000 --- a/.planning/milestones/v1.0-phases/03-storage/03-07-PLAN.md +++ /dev/null @@ -1,577 +0,0 @@ ---- -phase: 03 -plan: 07 -title: StorageViewModel + StorageView XAML + DI Wiring -status: pending -wave: 3 -depends_on: - - 03-03 - - 03-06 -files_modified: - - SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs - - SharepointToolbox/Views/Tabs/StorageView.xaml - - SharepointToolbox/Views/Tabs/StorageView.xaml.cs - - SharepointToolbox/App.xaml.cs - - SharepointToolbox/MainWindow.xaml - - SharepointToolbox/MainWindow.xaml.cs -autonomous: true -requirements: - - STOR-01 - - STOR-02 - - STOR-03 - - STOR-04 - - STOR-05 - -must_haves: - truths: - - "StorageView appears in the Storage tab (replaces FeatureTabBase stub) when the app runs" - - "User can enter a site URL, set folder depth (0 = library root, or N levels), check per-library breakdown, and click Generate Metrics" - - "DataGrid displays StorageNode rows with library name indented by IndentLevel, file count, total size, version size, last modified" - - "Export buttons are enabled after a successful scan and disabled when Results is empty" - - "Never modify ObservableCollection from a background thread — accumulate in List on background, then Dispatcher.InvokeAsync" - - "StorageViewModel never stores ClientContext — it calls ISessionManager.GetOrCreateContextAsync at operation start" - artifacts: - - path: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs" - provides: "Storage tab ViewModel (IStorageService orchestration)" - exports: ["StorageViewModel"] - - path: "SharepointToolbox/Views/Tabs/StorageView.xaml" - provides: "Storage tab XAML (DataGrid + controls)" - - path: "SharepointToolbox/Views/Tabs/StorageView.xaml.cs" - provides: "StorageView code-behind" - key_links: - - from: "StorageViewModel.cs" - to: "IStorageService.CollectStorageAsync" - via: "RunOperationAsync override" - pattern: "CollectStorageAsync" - - from: "StorageViewModel.cs" - to: "ISessionManager.GetOrCreateContextAsync" - via: "context acquisition" - pattern: "GetOrCreateContextAsync" - - from: "StorageView.xaml" - to: "StorageViewModel.Results" - via: "DataGrid ItemsSource binding" - pattern: "Results" ---- - -# Plan 03-07: StorageViewModel + StorageView XAML + DI Wiring - -## Goal - -Create the `StorageViewModel` (orchestrates `IStorageService`, export commands) and `StorageView` XAML (DataGrid with IndentLevel-based name indentation). Wire the Storage tab in `MainWindow` to replace the `FeatureTabBase` stub, register all dependencies in `App.xaml.cs`. - -## Context - -Plans 03-02 (StorageService), 03-03 (export services), and 03-06 (localization) must complete before this plan. The ViewModel follows the exact pattern from `PermissionsViewModel`: `FeatureViewModelBase` base class, `AsyncRelayCommand` for exports, `ObservableCollection` updated via `Dispatcher.InvokeAsync` from background thread. - -`MainWindow.xaml` currently has the Storage tab as: -```xml - - - -``` -This plan adds `x:Name="StorageTabItem"` to that TabItem and wires `StorageTabItem.Content` in `MainWindow.xaml.cs`. - -The `IndentConverter` value converter maps `IndentLevel` (int) → `Thickness(IndentLevel * 16, 0, 0, 0)`. It must be defined in the View or a shared Resources file. - -## Tasks - -### Task 1: Create StorageViewModel - -**File:** `SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs` - -**Action:** Create - -**Why:** Storage tab business logic — orchestrates StorageService scan, holds results, triggers exports. - -```csharp -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Windows; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Extensions.Logging; -using Microsoft.Win32; -using SharepointToolbox.Core.Messages; -using SharepointToolbox.Core.Models; -using SharepointToolbox.Services; -using SharepointToolbox.Services.Export; - -namespace SharepointToolbox.ViewModels.Tabs; - -public partial class StorageViewModel : FeatureViewModelBase -{ - private readonly IStorageService _storageService; - private readonly ISessionManager _sessionManager; - private readonly StorageCsvExportService _csvExportService; - private readonly StorageHtmlExportService _htmlExportService; - private readonly ILogger _logger; - private TenantProfile? _currentProfile; - - [ObservableProperty] - private string _siteUrl = string.Empty; - - [ObservableProperty] - private bool _perLibrary = true; - - [ObservableProperty] - private bool _includeSubsites; - - [ObservableProperty] - private int _folderDepth; - - public bool IsMaxDepth - { - get => FolderDepth >= 999; - set - { - if (value) FolderDepth = 999; - else if (FolderDepth >= 999) FolderDepth = 0; - OnPropertyChanged(); - } - } - - private ObservableCollection _results = new(); - public ObservableCollection Results - { - get => _results; - private set - { - _results = value; - OnPropertyChanged(); - ExportCsvCommand.NotifyCanExecuteChanged(); - ExportHtmlCommand.NotifyCanExecuteChanged(); - } - } - - public IAsyncRelayCommand ExportCsvCommand { get; } - public IAsyncRelayCommand ExportHtmlCommand { get; } - - public TenantProfile? CurrentProfile => _currentProfile; - - public StorageViewModel( - IStorageService storageService, - ISessionManager sessionManager, - StorageCsvExportService csvExportService, - StorageHtmlExportService htmlExportService, - ILogger logger) - : base(logger) - { - _storageService = storageService; - _sessionManager = sessionManager; - _csvExportService = csvExportService; - _htmlExportService = htmlExportService; - _logger = logger; - - ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport); - ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); - } - - /// Test constructor — omits export services. - internal StorageViewModel( - IStorageService storageService, - ISessionManager sessionManager, - ILogger logger) - : base(logger) - { - _storageService = storageService; - _sessionManager = sessionManager; - _csvExportService = null!; - _htmlExportService = null!; - _logger = logger; - - ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport); - ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); - } - - protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) - { - if (_currentProfile == null) - { - StatusMessage = "No tenant selected. Please connect to a tenant first."; - return; - } - if (string.IsNullOrWhiteSpace(SiteUrl)) - { - StatusMessage = "Please enter a site URL."; - return; - } - - var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, ct); - // Override URL to the site URL the user entered (may differ from tenant root) - ctx.Url = SiteUrl.TrimEnd('/'); - - var options = new StorageScanOptions( - PerLibrary: PerLibrary, - IncludeSubsites: IncludeSubsites, - FolderDepth: FolderDepth); - - var nodes = await _storageService.CollectStorageAsync(ctx, options, progress, ct); - - // Flatten tree to one level for DataGrid display (assign IndentLevel during flatten) - var flat = new List(); - foreach (var node in nodes) - FlattenNode(node, 0, flat); - - if (Application.Current?.Dispatcher is { } dispatcher) - { - await dispatcher.InvokeAsync(() => - { - Results = new ObservableCollection(flat); - }); - } - else - { - Results = new ObservableCollection(flat); - } - } - - protected override void OnTenantSwitched(TenantProfile profile) - { - _currentProfile = profile; - Results = new ObservableCollection(); - SiteUrl = string.Empty; - OnPropertyChanged(nameof(CurrentProfile)); - ExportCsvCommand.NotifyCanExecuteChanged(); - ExportHtmlCommand.NotifyCanExecuteChanged(); - } - - internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile; - - internal Task TestRunOperationAsync(CancellationToken ct, IProgress progress) - => RunOperationAsync(ct, progress); - - private bool CanExport() => Results.Count > 0; - - private async Task ExportCsvAsync() - { - if (Results.Count == 0) return; - var dialog = new SaveFileDialog - { - Title = "Export storage metrics to CSV", - Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*", - DefaultExt = "csv", - FileName = "storage_metrics" - }; - if (dialog.ShowDialog() != true) return; - try - { - await _csvExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None); - OpenFile(dialog.FileName); - } - catch (Exception ex) - { - StatusMessage = $"Export failed: {ex.Message}"; - _logger.LogError(ex, "CSV export failed."); - } - } - - private async Task ExportHtmlAsync() - { - if (Results.Count == 0) return; - var dialog = new SaveFileDialog - { - Title = "Export storage metrics to HTML", - Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", - DefaultExt = "html", - FileName = "storage_metrics" - }; - if (dialog.ShowDialog() != true) return; - try - { - await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None); - OpenFile(dialog.FileName); - } - catch (Exception ex) - { - StatusMessage = $"Export failed: {ex.Message}"; - _logger.LogError(ex, "HTML export failed."); - } - } - - private static void FlattenNode(StorageNode node, int level, List result) - { - node.IndentLevel = level; - result.Add(node); - foreach (var child in node.Children) - FlattenNode(child, level + 1, result); - } - - private static void OpenFile(string filePath) - { - try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); } - catch { /* ignore — file may open but this is best-effort */ } - } -} -``` - -**Verification:** - -```bash -dotnet build C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx -``` - -Expected: 0 errors - -### Task 2: Create StorageView XAML + code-behind, update DI and MainWindow wiring - -**Files:** -- `SharepointToolbox/Views/Tabs/StorageView.xaml` -- `SharepointToolbox/Views/Tabs/StorageView.xaml.cs` -- `SharepointToolbox/Views/Converters/IndentConverter.cs` (create — also adds BytesConverter and InverseBoolConverter) -- `SharepointToolbox/App.xaml` (modify — register converters as Application.Resources) -- `SharepointToolbox/App.xaml.cs` (modify — add Storage registrations) -- `SharepointToolbox/MainWindow.xaml` (modify — add x:Name to Storage TabItem) -- `SharepointToolbox/MainWindow.xaml.cs` (modify — wire StorageTabItem.Content) - -**Action:** Create / Modify - -**Why:** STOR-01/02/03/04/05 — the UI that ties the storage service to user interaction. - -```xml - - - - - - - - - - -