--- phase: 16-report-consolidation-toggle plan: "02" subsystem: export tags: [html-export, consolidation, expandable-rows, toggleGroup, tdd] dependency-graph: requires: [16-01, 15-01, 15-02] provides: [Consolidated HTML export path, expandable location sub-lists, by-site view suppression] affects: [UserAccessHtmlExportService, UserAccessAuditViewModel] tech-stack: added: [] patterns: [early-return branch, optional parameter default, TDD red-green, private method extraction] key-files: created: [] modified: - SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs - SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs decisions: - "BuildConsolidatedHtml is a private method called via early-return in BuildHtml — existing non-consolidated code path is completely untouched" - "Separate locIdx counter for location group IDs (loc0, loc1...) is distinct from grpIdx for user group IDs (ugrp0, ugrp1...) — avoids ID collision Pitfall 2" - "Existing branding test call site updated to use named parameter branding: because mergePermissions was inserted before branding in the signature" metrics: duration: ~10min completed: 2026-04-09 tasks: 2 files-modified: 3 --- # Phase 16 Plan 02: Consolidated HTML Rendering Path Summary Consolidated HTML export with expandable [N sites] badges using toggleGroup() JS, by-site view suppression when mergePermissions=true, and ViewModel wiring for the HTML export call site. ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 (RED) | Add failing tests for RPT-03-b through RPT-03-e | 3d95d2a | UserAccessHtmlExportServiceTests.cs | | 1 (GREEN) | Implement consolidated HTML rendering path and wire ViewModel | 0ebe707 | UserAccessHtmlExportService.cs, UserAccessAuditViewModel.cs, UserAccessHtmlExportServiceTests.cs | | 2 | Full solution build and test suite verification | — | (no source changes — verification only) | ## What Was Built ### BuildHtml Signature Change `BuildHtml` now accepts `bool mergePermissions = false` as the second parameter (before `branding`): ```csharp public string BuildHtml(IReadOnlyList entries, bool mergePermissions = false, ReportBranding? branding = null) ``` The early-return branch: ```csharp if (mergePermissions) { var consolidated = PermissionConsolidator.Consolidate(entries); return BuildConsolidatedHtml(consolidated, entries, branding); } ``` The entire existing code path below this branch is completely untouched. ### WriteAsync Signature Change `WriteAsync` now accepts `bool mergePermissions = false` after `ct` and passes it through to `BuildHtml`: ```csharp public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct, bool mergePermissions = false, ReportBranding? branding = null) ``` ### BuildConsolidatedHtml Private Method Produces a consolidated HTML report: - Same HTML shell (DOCTYPE, head, CSS, stats cards, user summary cards) - Single by-user table with columns: User, Permission Level, Access Type, Granted Through, Sites - Group headers per UserLogin with `ugrp{n}` IDs - Sites column behavior: - 1 location: plain text site title (no badge, no sub-rows) - 2+ locations: `N sites` with hidden sub-rows (`data-group="loc{n}" style="display:none"`) containing linked site titles - By-site view (view-site div) is completely omitted - btn-site is completely omitted — only By User button rendered - Separate `locIdx` counter for location groups, distinct from `grpIdx` for user groups ### ViewModel Wiring `UserAccessAuditViewModel.ExportHtmlAsync` now passes `MergePermissions` to `WriteAsync`: ```csharp await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions, branding); ``` ### Tests (TDD) 4 new test methods added: - `BuildHtml_mergePermissionsFalse_identical_to_default` — RPT-03-b: byte-identical output to default call - `BuildHtml_mergePermissionsTrue_contains_sites_column` — RPT-03-c: "Sites" column header present - `BuildHtml_mergePermissionsTrue_multiLocation_has_badge_and_subrows` — RPT-03-d: onclick toggleGroup + data-group=loc pattern - `BuildHtml_mergePermissionsTrue_omits_bysite_view` — RPT-03-e: no btn-site, no view-site All 12 UserAccessHtmlExportServiceTests pass. Full suite: 302 passed, 26 skipped. ## Decisions Made 1. **Early-return + private method** — Same pattern as Plan 01's consolidated CSV path. `BuildConsolidatedHtml` is extracted as a private method to keep `BuildHtml` clean. The branch guarantees the existing code path is never reached when `mergePermissions=true`. 2. **Separate locIdx counter** — RESEARCH.md Pitfall 2 explicitly warned about ID collision between user group headers and location sub-rows. Used distinct `int grpIdx` (for `ugrp{n}`) and `int locIdx` (for `loc{n}`) to prevent any overlap. 3. **Named parameter fix for branding test** — The existing `BuildHtml_WithBranding_ContainsLogoImg` test called `BuildHtml(entries, MakeBranding(...))` positionally. Inserting `mergePermissions` before `branding` broke that call — fixed by adding `branding:` named argument. This is a Rule 1 auto-fix (broken test). ## Verification Results - `dotnet build` — 0 errors, 0 warnings - `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` — 12/12 passed - `dotnet test` full suite — 302 passed, 26 skipped, 0 failed (no regressions) - Test count increased by 4 from Plan 02 (RPT-03-b through RPT-03-e) + 2 previously flaky tests now stable ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Fixed branding test call site broken by parameter insertion** - **Found during:** Task 1 (GREEN phase — first test run) - **Issue:** Existing `BuildHtml_WithBranding_ContainsLogoImg` test passed `MakeBranding(...)` as positional argument 2, which collided with the newly inserted `bool mergePermissions` parameter at position 2 - **Fix:** Added named `branding:` argument: `svc.BuildHtml(new[] { DefaultEntry }, branding: MakeBranding(msp: true))` - **Files modified:** SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs - **Commit:** 0ebe707 ## Self-Check: PASSED Files exist: - SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs — contains `mergePermissions`, `BuildConsolidatedHtml`, `PermissionConsolidator.Consolidate` - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs — contains `MergePermissions` in WriteAsync call - SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs — contains 4 new consolidation test methods Commits exist: 3d95d2a (RED), 0ebe707 (GREEN)