From 720a4197884860769c43edcae6dc0e81c340021c Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 9 Apr 2026 12:19:06 +0200 Subject: [PATCH] docs(16-report-consolidation-toggle): create phase plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .planning/ROADMAP.md | 17 +- .../16-01-PLAN.md | 279 ++++++++++++++++++ .../16-02-PLAN.md | 245 +++++++++++++++ 3 files changed, 534 insertions(+), 7 deletions(-) create mode 100644 .planning/phases/16-report-consolidation-toggle/16-01-PLAN.md create mode 100644 .planning/phases/16-report-consolidation-toggle/16-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a0b4c39..d17d342 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -44,7 +44,7 @@ ### 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) -- [ ] **Phase 16: Report Consolidation Toggle** — Export settings toggle wired to PermissionConsolidator; first user-visible consolidation behavior +- [ ] **Phase 16: Report Consolidation Toggle** (2 plans) — Export settings toggle wired to PermissionConsolidator; first user-visible consolidation behavior - [ ] **Phase 17: Group Expansion in HTML Reports** — Clickable group expansion in HTML exports with transitive membership resolution - [ ] **Phase 18: Auto-Take Ownership** — Global toggle and automatic site collection admin elevation on access denied - [ ] **Phase 19: App Registration & Removal** — Automated Entra app registration with guided fallback and clean removal @@ -62,8 +62,8 @@ 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: -- [ ] 15-01-PLAN.md — Models (LocationInfo, ConsolidatedPermissionEntry) + PermissionConsolidator service -- [ ] 15-02-PLAN.md — Unit tests (10 test cases) + full solution build verification +- [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 @@ -74,7 +74,10 @@ Plans: 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**: TBD +**Plans:** 2 plans +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 @@ -108,7 +111,7 @@ Plans: 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**: 15-01 (Models + Consolidator), 15-02 (Tests + Build Verification) +**Plans**: TBD ## Progress @@ -117,8 +120,8 @@ Plans: | 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 | 2/2 | Complete | 2026-04-09 | — | -| 16. Report Consolidation Toggle | v2.3 | 0/? | Not started | — | +| 15. Consolidation Data Model | v2.3 | 2/2 | Complete | 2026-04-09 | +| 16. Report Consolidation Toggle | v2.3 | 0/2 | Not started | — | | 17. Group Expansion in HTML Reports | v2.3 | 0/? | Not started | — | | 18. Auto-Take Ownership | v2.3 | 0/? | Not started | — | | 19. App Registration & Removal | v2.3 | 0/? | Not started | — | diff --git a/.planning/phases/16-report-consolidation-toggle/16-01-PLAN.md b/.planning/phases/16-report-consolidation-toggle/16-01-PLAN.md new file mode 100644 index 0000000..d499e43 --- /dev/null +++ b/.planning/phases/16-report-consolidation-toggle/16-01-PLAN.md @@ -0,0 +1,279 @@ +--- +phase: 16-report-consolidation-toggle +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs + - SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs + - SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml + - SharepointToolbox/Views/Tabs/PermissionsView.xaml + - SharepointToolbox/Localization/Strings.resx + - SharepointToolbox/Localization/Strings.fr.resx + - SharepointToolbox/Services/Export/UserAccessCsvExportService.cs +autonomous: true +requirements: [RPT-03] + +must_haves: + truths: + - "MergePermissions property exists on UserAccessAuditViewModel and defaults to false" + - "MergePermissions property exists on PermissionsViewModel and defaults to false (no-op placeholder)" + - "Export Options GroupBox with 'Merge duplicate permissions' checkbox is visible in both audit tabs" + - "CSV export with mergePermissions=false produces byte-identical output to current behavior" + - "CSV export with mergePermissions=true writes consolidated rows with Locations column" + artifacts: + - path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs" + provides: "MergePermissions ObservableProperty + export call site wiring" + contains: "_mergePermissions" + - path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs" + provides: "MergePermissions ObservableProperty (no-op placeholder)" + contains: "_mergePermissions" + - path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml" + provides: "Export Options GroupBox with checkbox" + contains: "Export Options" + - path: "SharepointToolbox/Views/Tabs/PermissionsView.xaml" + provides: "Export Options GroupBox with checkbox" + contains: "Export Options" + - path: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs" + provides: "Consolidated CSV export path" + contains: "mergePermissions" + key_links: + - from: "UserAccessAuditView.xaml" + to: "UserAccessAuditViewModel.MergePermissions" + via: "XAML Binding" + pattern: "IsChecked.*Binding MergePermissions" + - from: "UserAccessAuditViewModel.ExportCsvAsync" + to: "UserAccessCsvExportService.WriteSingleFileAsync" + via: "MergePermissions parameter passthrough" + pattern: "WriteSingleFileAsync.*MergePermissions" +--- + + +Add the MergePermissions toggle property to both ViewModels, wire Export Options GroupBox in both XAML views, add localization keys, and implement the consolidated CSV export path. + +Purpose: Establishes the user-facing toggle and the simpler CSV consolidation path, leaving the complex HTML rendering for Plan 02. +Output: Working toggle UI in both tabs, consolidated CSV export, non-consolidated paths unchanged. + + + +@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/16-report-consolidation-toggle/16-CONTEXT.md +@.planning/phases/16-report-consolidation-toggle/16-RESEARCH.md +@.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md + + + + +From SharepointToolbox/Core/Helpers/PermissionConsolidator.cs: +```csharp +public static class PermissionConsolidator +{ + internal static string MakeKey(UserAccessEntry e); + public static IReadOnlyList Consolidate(IReadOnlyList entries); +} +``` + +From SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs: +```csharp +public record ConsolidatedPermissionEntry( + string UserDisplayName, string UserLogin, string PermissionLevel, + string AccessType, string GrantedThrough, + bool IsExternalUser, bool IsHighPrivilege, + IReadOnlyList Locations) +{ + public int LocationCount => Locations.Count; +} +``` + +From SharepointToolbox/Core/Models/LocationInfo.cs: +```csharp +public record LocationInfo(string SiteUrl, string SiteTitle, string ObjectTitle, string ObjectUrl, string ObjectType); +``` + +From SharepointToolbox/Services/Export/UserAccessCsvExportService.cs: +```csharp +public class UserAccessCsvExportService +{ + public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList entries); + public async Task WriteAsync(IReadOnlyList entries, string outputDirectory, CancellationToken ct); + public async Task WriteSingleFileAsync(IReadOnlyList entries, string filePath, CancellationToken ct); +} +``` + +From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs (export call sites): +```csharp +// Line 495 — ExportCsvAsync: +await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None); +// Line 526 — ExportHtmlAsync: +await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); +``` + + + + + + + Task 1: Add MergePermissions property to both ViewModels and localization keys + + SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs, + SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs, + SharepointToolbox/Localization/Strings.resx, + SharepointToolbox/Localization/Strings.fr.resx + + + 1. In `UserAccessAuditViewModel.cs`, add a new `[ObservableProperty]` field after the existing observable properties block (around line 101): + ```csharp + [ObservableProperty] + private bool _mergePermissions; + ``` + No partial handler needed — the property defaults to `false` and has no side effects on change. + + 2. In `PermissionsViewModel.cs`, add the same `[ObservableProperty]` field in the observable properties section: + ```csharp + [ObservableProperty] + private bool _mergePermissions; + ``` + This is a no-op placeholder — PermissionsViewModel does NOT use this value in any export logic. + + 3. In `Strings.resx`, add two new entries following the existing naming convention (look at existing keys like `audit.grp.scanOptions`, `chk.includeInherited` etc. for the exact naming pattern): + - Key: `audit.grp.export` — Value: `Export Options` + - Key: `chk.merge.permissions` — Value: `Merge duplicate permissions` + + 4. In `Strings.fr.resx`, add the same two keys: + - Key: `audit.grp.export` — Value: `Options d'exportation` + - Key: `chk.merge.permissions` — Value: `Fusionner les permissions en double` + + IMPORTANT: Both .resx files MUST have the keys added. Missing French keys cause empty strings in French locale. + + + cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5 + + Both ViewModels have MergePermissions property that defaults to false. Both .resx files have the two new localization keys. Solution builds without errors or warnings. + + + + Task 2: Add Export Options GroupBox to both XAML views + + SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml, + SharepointToolbox/Views/Tabs/PermissionsView.xaml + + + 1. In `UserAccessAuditView.xaml`, locate the "Scan Options" GroupBox (around lines 199-210 — look for `GroupBox Header="{Binding [audit.grp.scanOptions]..."` or similar). Add a new GroupBox immediately AFTER the Scan Options GroupBox, within the same DockPanel, using the identical pattern: + ```xml + + + + + + ``` + Match the exact `Source={x:Static loc:TranslationSource.Instance}` pattern used by the existing Scan Options GroupBox for localized headers and checkbox labels. + + 2. In `PermissionsView.xaml`, locate the "Display Options" GroupBox in the left panel. Add the same Export Options GroupBox after it, using the same XAML pattern as above. The binding `{Binding MergePermissions}` will bind to `PermissionsViewModel.MergePermissions` (the no-op placeholder). + + IMPORTANT: Do NOT modify any existing XAML elements. Only ADD the new GroupBox. The GroupBox must be always visible (not conditionally hidden). + + + cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5 + + Both XAML views show an "Export Options" GroupBox with a "Merge duplicate permissions" checkbox bound to MergePermissions. Existing UI elements are unchanged. + + + + Task 3: Implement consolidated CSV export path and wire ViewModel call site + + SharepointToolbox/Services/Export/UserAccessCsvExportService.cs, + SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs, + SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs + + + - RPT-03-f: `WriteSingleFileAsync(entries, path, ct, mergePermissions: false)` produces byte-identical output to current `WriteSingleFileAsync(entries, path, ct)` — capture current output first, then verify no change + - RPT-03-g: `WriteSingleFileAsync(entries, path, ct, mergePermissions: true)` writes consolidated CSV with header `"User","User Login","Permission Level","Access Type","Granted Through","Locations","Location Count"` and semicolon-separated site titles in Locations column + - Edge case: single-location consolidated entry has LocationCount=1 and Locations=single site title (no semicolons) + + + 1. In `UserAccessCsvExportService.cs`, add `bool mergePermissions = false` parameter to `WriteSingleFileAsync`: + ```csharp + public async Task WriteSingleFileAsync( + IReadOnlyList entries, + string filePath, + CancellationToken ct, + bool mergePermissions = false) + ``` + + 2. At the top of `WriteSingleFileAsync`, add an early-return consolidated branch: + ```csharp + if (mergePermissions) + { + var consolidated = PermissionConsolidator.Consolidate(entries); + // Build consolidated CSV with distinct header and rows + var sb = new StringBuilder(); + // Summary section (same pattern as existing) + sb.AppendLine("\"User Access Audit Report (Consolidated)\""); + sb.AppendLine($"\"Users Audited\",\"{consolidated.Select(e => e.UserLogin).Distinct().Count()}\""); + sb.AppendLine($"\"Total Entries\",\"{consolidated.Count}\""); + sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); + sb.AppendLine(); + sb.AppendLine("\"User\",\"User Login\",\"Permission Level\",\"Access Type\",\"Granted Through\",\"Locations\",\"Location Count\""); + foreach (var entry in consolidated) + { + var locations = string.Join("; ", entry.Locations.Select(l => l.SiteTitle)); + sb.AppendLine(string.Join(",", new[] + { + $"\"{entry.UserDisplayName}\"", + $"\"{entry.UserLogin}\"", + $"\"{entry.PermissionLevel}\"", + $"\"{entry.AccessType}\"", + $"\"{entry.GrantedThrough}\"", + $"\"{locations}\"", + $"\"{entry.LocationCount}\"" + })); + } + await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(false), ct); + return; + } + ``` + Leave the existing code path below this branch COMPLETELY UNTOUCHED. + + 3. In `UserAccessAuditViewModel.cs`, update the `ExportCsvAsync` method call site (line ~495): + Change: `await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None);` + To: `await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions);` + + 4. Add test methods in `UserAccessCsvExportServiceTests.cs` for RPT-03-f (non-consolidated identical) and RPT-03-g (consolidated CSV format). Use test data with 2-3 UserAccessEntry rows where 2 share the same consolidation key. + + IMPORTANT: Do NOT modify `BuildCsv` — consolidation applies only at `WriteSingleFileAsync` level per RESEARCH.md Pitfall 4. Do NOT touch the existing code path below the `if (mergePermissions)` branch. + + + cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests" --no-restore -v q 2>&1 | tail -10 + + CSV export with mergePermissions=false is identical to pre-toggle output. CSV export with mergePermissions=true writes consolidated rows with Locations and LocationCount columns. ViewModel passes MergePermissions to the service. + + + + + +1. `dotnet build` succeeds with 0 errors, 0 warnings +2. `dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests"` — all tests pass including new consolidation tests +3. `dotnet test` — full suite green, no regressions + + + +- MergePermissions property exists on both ViewModels, defaults to false +- Export Options GroupBox visible in both XAML tabs with localized labels +- CSV consolidated path produces correct output with merged rows +- Non-consolidated CSV path is byte-identical to pre-Phase-16 output +- All existing tests pass without modification + + + +After completion, create `.planning/phases/16-report-consolidation-toggle/16-01-SUMMARY.md` + diff --git a/.planning/phases/16-report-consolidation-toggle/16-02-PLAN.md b/.planning/phases/16-report-consolidation-toggle/16-02-PLAN.md new file mode 100644 index 0000000..0bc827f --- /dev/null +++ b/.planning/phases/16-report-consolidation-toggle/16-02-PLAN.md @@ -0,0 +1,245 @@ +--- +phase: 16-report-consolidation-toggle +plan: "02" +type: execute +wave: 2 +depends_on: ["16-01"] +files_modified: + - SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs + - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs + - SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs +autonomous: true +requirements: [RPT-03] + +must_haves: + truths: + - "HTML export with mergePermissions=false produces byte-identical output to pre-Phase-16 behavior" + - "HTML export with mergePermissions=true renders consolidated by-user rows with Sites column" + - "Consolidated rows with 1 location show site title inline (no badge)" + - "Consolidated rows with 2+ locations show clickable [N sites] badge that expands sub-list" + - "By-site view toggle is omitted from HTML when consolidation is ON" + - "ViewModel passes MergePermissions to HTML export service" + artifacts: + - path: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs" + provides: "Consolidated HTML rendering with expandable location sub-lists" + contains: "mergePermissions" + - path: "SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs" + provides: "Tests for consolidated and non-consolidated HTML paths" + contains: "mergePermissions" + key_links: + - from: "UserAccessAuditViewModel.ExportHtmlAsync" + to: "UserAccessHtmlExportService.WriteAsync" + via: "MergePermissions parameter passthrough" + pattern: "WriteAsync.*MergePermissions" + - from: "UserAccessHtmlExportService.BuildHtml" + to: "PermissionConsolidator.Consolidate" + via: "Early-return branch when mergePermissions=true" + pattern: "PermissionConsolidator\\.Consolidate" + - from: "Consolidated HTML" + to: "toggleGroup JS" + via: "data-group='loc{idx}' on location sub-rows" + pattern: "data-group.*loc" +--- + + +Implement the consolidated HTML rendering path in UserAccessHtmlExportService — expandable location sub-lists using existing toggleGroup() JS, by-site view suppression, and wire the ViewModel HTML export call site. + +Purpose: Completes the user-visible consolidation behavior for HTML reports — the primary export format. +Output: Working consolidated HTML export with expandable site lists, full test coverage. + + + +@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/16-report-consolidation-toggle/16-CONTEXT.md +@.planning/phases/16-report-consolidation-toggle/16-RESEARCH.md +@.planning/phases/16-report-consolidation-toggle/16-01-SUMMARY.md + + + + +From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs: +```csharp +[ObservableProperty] +private bool _mergePermissions; + +// ExportHtmlAsync call site (line ~526): +await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); +// Must become: +await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions, branding); +``` + +From SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs: +```csharp +public class UserAccessHtmlExportService +{ + public string BuildHtml(IReadOnlyList entries, ReportBranding? branding = null); + public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct, ReportBranding? branding = null); +} +``` + +From SharepointToolbox/Core/Helpers/PermissionConsolidator.cs: +```csharp +public static IReadOnlyList Consolidate(IReadOnlyList entries); +``` + +From SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs: +```csharp +public record ConsolidatedPermissionEntry( + string UserDisplayName, string UserLogin, string PermissionLevel, + string AccessType, string GrantedThrough, + bool IsExternalUser, bool IsHighPrivilege, + IReadOnlyList Locations) +{ + public int LocationCount => Locations.Count; +} +``` + +Existing toggleGroup JS (reuse as-is, already in BuildHtml inline JS): +```javascript +function toggleGroup(id) { + var rows = document.querySelectorAll('tr[data-group="' + id + '"]'); + var isHidden = rows.length > 0 && rows[0].style.display === 'none'; + rows.forEach(function(r) { r.style.display = isHidden ? '' : 'none'; }); +} +``` + +Consolidated HTML column layout (from RESEARCH.md Pattern 4): +| Column | Source Field | +|--------|-------------| +| User | UserDisplayName (+ Guest badge if IsExternalUser) | +| Permission Level | PermissionLevel (+ high-priv icon if IsHighPrivilege) | +| Access Type | badge from AccessType | +| Granted Through | GrantedThrough | +| Sites | inline title OR [N sites] badge + expandable sub-list | + + + + + + + Task 1: Implement consolidated HTML rendering path in BuildHtml and wire WriteAsync + + SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs, + SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs, + SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs + + + - RPT-03-b: `BuildHtml(entries, mergePermissions: false)` output is byte-identical to current `BuildHtml(entries)` output + - RPT-03-c: `BuildHtml(entries, mergePermissions: true)` includes consolidated rows and a "Sites" column header + - RPT-03-d: When a consolidated entry has LocationCount >= 2, the HTML contains an `[N sites]` badge with `onclick="toggleGroup('loc...')"` and hidden sub-rows with `data-group="loc..."` + - RPT-03-e: When mergePermissions=true, the HTML does NOT contain the "By Site" button or `view-site` div + - Edge: Single-location consolidated entry renders site title inline (no badge, no expandable rows) + + + 1. Change `BuildHtml` signature to add `bool mergePermissions = false` as the second parameter (before `branding`): + ```csharp + public string BuildHtml(IReadOnlyList entries, bool mergePermissions = false, ReportBranding? branding = null) + ``` + + 2. At the very beginning of `BuildHtml`, after the stats computation block, add an early-return branch: + ```csharp + if (mergePermissions) + { + var consolidated = PermissionConsolidator.Consolidate(entries); + return BuildConsolidatedHtml(consolidated, entries, branding); + } + ``` + Leave the ENTIRE existing code path below this branch COMPLETELY UNTOUCHED. Not a single character change. + + 3. Create a private `BuildConsolidatedHtml` method that builds the consolidated HTML report: + - Reuse the same HTML shell (DOCTYPE, head, CSS, header, stats section, user summary cards) from the existing `BuildHtml` — extract the stats from `entries` (the original flat list) for accurate counts. + - Include the existing `toggleGroup()` and `toggleView()` JS functions (copy from existing inline JS). + - **OMIT the "By Site" button** from the view toggle bar — only render the "By User" button (or omit the view toggle entirely since there's only one view). + - **OMIT the `view-site` div** and its by-site table entirely. + - Render a single by-user table with columns: User, Permission Level, Access Type, Granted Through, Sites. + - Group consolidated entries by UserLogin (similar to existing user grouping pattern with `ugrp{n}` group headers). + - For each `ConsolidatedPermissionEntry` row: + - **Sites column — 1 location:** Render `entry.Locations[0].SiteTitle` as plain text. + - **Sites column — 2+ locations:** Render `{entry.LocationCount} sites`. Immediately after the main ``, emit hidden sub-rows: + ```html + + + {loc.SiteTitle} + + + ``` + - Use a SEPARATE counter `int locIdx = 0` for location group IDs, distinct from user group counter `int grpIdx = 0` (RESEARCH.md Pitfall 2). + - Apply the same CSS classes: `.guest-badge` for external users, `.high-priv` for high-privilege rows, `.badge` for access type badges. + - Include the same search/filter JS if present in the existing template. + + 4. Change `WriteAsync` signature to add `bool mergePermissions = false` after `ct` and before `branding`: + ```csharp + public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct, bool mergePermissions = false, ReportBranding? branding = null) + ``` + Pass `mergePermissions` through to `BuildHtml`: + ```csharp + var html = BuildHtml(entries, mergePermissions, branding); + ``` + + 5. In `UserAccessAuditViewModel.cs`, update the `ExportHtmlAsync` call site (line ~526): + Change: `await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);` + To: `await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions, branding);` + + 6. Add test methods in `UserAccessHtmlExportServiceTests.cs`: + - **RPT-03-b test:** Capture `BuildHtml(testEntries)` output (old signature), then verify `BuildHtml(testEntries, mergePermissions: false)` produces identical string. + - **RPT-03-c test:** `BuildHtml(testEntries, mergePermissions: true)` contains "Sites" column header and consolidated row content. + - **RPT-03-d test:** Create test data with 2+ entries sharing the same consolidation key. Verify output contains `onclick="toggleGroup('loc` and `data-group="loc` patterns. + - **RPT-03-e test:** `BuildHtml(testEntries, mergePermissions: true)` does NOT contain `btn-site` or `view-site`. + - Use 3-4 UserAccessEntry test rows where 2 share the same key (same UserLogin+PermissionLevel+AccessType+GrantedThrough but different sites). + + CRITICAL ANTI-PATTERNS: + - Do NOT modify any line of the existing non-consolidated code path in BuildHtml. The early-return branch guarantees isolation. + - Do NOT reuse the `ugrp` counter for location groups — use `loc{locIdx}` with its own counter. + - Do NOT forget to pass mergePermissions through WriteAsync to BuildHtml (Pitfall 3). + + + cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests" --no-restore -v q 2>&1 | tail -10 + + HTML export with mergePermissions=false is byte-identical to pre-toggle output. HTML export with mergePermissions=true renders consolidated rows with Sites column, expandable location sub-lists for 2+ locations, and omits the by-site view. All tests pass. + + + + Task 2: Full solution build and test suite verification + + + 1. Run `dotnet build` on the entire solution to verify zero errors and zero warnings. + 2. Run `dotnet test` to verify the full test suite passes — all existing tests plus new Phase 16 tests. + 3. Verify test count has increased by at least 4 (RPT-03-b through RPT-03-g from Plans 01 and 02). + 4. If any test fails, diagnose and fix. Common issues: + - Existing tests calling `BuildHtml(entries)` still work because `mergePermissions` defaults to `false`. + - Existing tests calling `WriteAsync(entries, path, ct, branding)` — verify the parameter order change doesn't break existing callers. Since `mergePermissions` is now between `ct` and `branding`, check that no existing call site passes `branding` positionally without the new parameter. If so, fix the call site. + - Existing tests calling `WriteSingleFileAsync(entries, path, ct)` still work because `mergePermissions` defaults to `false`. + + + cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test -v q 2>&1 | tail -15 + + Full solution builds with 0 errors, 0 warnings. All tests pass (existing + new). No regressions. + + + + + +1. `dotnet build` — 0 errors, 0 warnings +2. `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` — all HTML export tests pass +3. `dotnet test` — full suite green, no regressions +4. Manual spot-check: `BuildHtml(entries, false)` output matches `BuildHtml(entries)` character-for-character + + + +- HTML consolidated path renders expandable [N sites] badges with toggleGroup integration +- HTML non-consolidated path is byte-identical to pre-Phase-16 output +- By-site view is suppressed when consolidation is ON +- ViewModel wiring passes MergePermissions to WriteAsync +- Full test suite passes with no regressions + + + +After completion, create `.planning/phases/16-report-consolidation-toggle/16-02-SUMMARY.md` +