Files
Sharepoint-Toolbox/.planning/phases/11-html-export-branding/11-02-PLAN.md
Dev df6f4949a8 docs(13-02): complete User Directory ViewModel plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:44:56 +02:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
11-html-export-branding 02 execute 2
11-01
SharepointToolbox/Services/Export/HtmlExportService.cs
SharepointToolbox/Services/Export/SearchHtmlExportService.cs
SharepointToolbox/Services/Export/StorageHtmlExportService.cs
SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs
SharepointToolbox.Tests/Services/Export/HtmlExportServiceTests.cs
SharepointToolbox.Tests/Services/Export/SearchExportServiceTests.cs
SharepointToolbox.Tests/Services/Export/StorageHtmlExportServiceTests.cs
SharepointToolbox.Tests/Services/Export/DuplicatesHtmlExportServiceTests.cs
SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs
true
BRAND-05
truths artifacts key_links
Each of the 5 HTML exporters accepts an optional ReportBranding? branding = null parameter on BuildHtml and WriteAsync
When branding is provided with logos, the exported HTML contains the branding header div between body and h1
When branding is null or has no logos, the exported HTML is identical to pre-branding output
Existing callers without branding parameter still compile and produce identical output
path provides contains
SharepointToolbox/Services/Export/HtmlExportService.cs Permissions HTML export with optional branding ReportBranding? branding = null
path provides contains
SharepointToolbox/Services/Export/SearchHtmlExportService.cs Search HTML export with optional branding ReportBranding? branding = null
path provides contains
SharepointToolbox/Services/Export/StorageHtmlExportService.cs Storage HTML export with optional branding ReportBranding? branding = null
path provides contains
SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs Duplicates HTML export with optional branding ReportBranding? branding = null
path provides contains
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs User access HTML export with optional branding ReportBranding? branding = null
from to via pattern
SharepointToolbox/Services/Export/HtmlExportService.cs SharepointToolbox/Services/Export/BrandingHtmlHelper.cs static method call BrandingHtmlHelper.BuildBrandingHeader
from to via pattern
SharepointToolbox/Services/Export/SearchHtmlExportService.cs SharepointToolbox/Services/Export/BrandingHtmlHelper.cs static method call BrandingHtmlHelper.BuildBrandingHeader
from to via pattern
SharepointToolbox/Services/Export/StorageHtmlExportService.cs SharepointToolbox/Services/Export/BrandingHtmlHelper.cs static method call BrandingHtmlHelper.BuildBrandingHeader
from to via pattern
SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs SharepointToolbox/Services/Export/BrandingHtmlHelper.cs static method call BrandingHtmlHelper.BuildBrandingHeader
from to via pattern
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs SharepointToolbox/Services/Export/BrandingHtmlHelper.cs static method call BrandingHtmlHelper.BuildBrandingHeader
Add optional `ReportBranding? branding = null` parameter to all 5 HTML export services and inject the branding header HTML between `` and `

` in each.

Purpose: BRAND-05 requires all five HTML report types to display logos. This plan modifies each exporter to call BrandingHtmlHelper.BuildBrandingHeader(branding) at the correct injection point. Default null parameter ensures zero regression for existing callers.

Output: All 5 HTML export services accept branding, inject header when provided, and extended tests verify branding appears in output.

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/11-html-export-branding/11-CONTEXT.md @.planning/phases/11-html-export-branding/11-RESEARCH.md @.planning/phases/11-html-export-branding/11-01-SUMMARY.md

From SharepointToolbox/Core/Models/ReportBranding.cs:

namespace SharepointToolbox.Core.Models;
public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo);

From SharepointToolbox/Services/Export/BrandingHtmlHelper.cs:

namespace SharepointToolbox.Services.Export;
internal static class BrandingHtmlHelper
{
    public static string BuildBrandingHeader(ReportBranding? branding);
}

HtmlExportService.cs:

public string BuildHtml(IReadOnlyList<PermissionEntry> entries)
public string BuildHtml(IReadOnlyList<SimplifiedPermissionEntry> entries)
public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
public async Task WriteAsync(IReadOnlyList<SimplifiedPermissionEntry> entries, string filePath, CancellationToken ct)

SearchHtmlExportService.cs:

public string BuildHtml(IReadOnlyList<SearchResult> results)
public async Task WriteAsync(IReadOnlyList<SearchResult> results, string filePath, CancellationToken ct)

StorageHtmlExportService.cs:

public string BuildHtml(IReadOnlyList<StorageNode> nodes)
public string BuildHtml(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics)
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, string filePath, CancellationToken ct)
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, string filePath, CancellationToken ct)

DuplicatesHtmlExportService.cs:

public string BuildHtml(IReadOnlyList<DuplicateGroup> groups)
public async Task WriteAsync(IReadOnlyList<DuplicateGroup> groups, string filePath, CancellationToken ct)

UserAccessHtmlExportService.cs:

public string BuildHtml(IReadOnlyList<UserAccessEntry> entries)
public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct)
Task 1: Add branding parameter to all 5 HTML export services SharepointToolbox/Services/Export/HtmlExportService.cs, SharepointToolbox/Services/Export/SearchHtmlExportService.cs, SharepointToolbox/Services/Export/StorageHtmlExportService.cs, SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs, SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs For each of the 5 HTML export service files, apply the same two-step modification:
**Step 1: Add `using SharepointToolbox.Core.Models;`** at the top if not already present (needed for `ReportBranding`).

**Step 2: Modify BuildHtml signatures.** Add `ReportBranding? branding = null` as the LAST parameter:

For `HtmlExportService.cs`:
- `BuildHtml(IReadOnlyList<PermissionEntry> entries, ReportBranding? branding = null)`
- `BuildHtml(IReadOnlyList<SimplifiedPermissionEntry> entries, ReportBranding? branding = null)` (second overload of BuildHtml — NOT a separate method)
- In both methods, find the line `sb.AppendLine("<body>");` followed by `sb.AppendLine("<h1>...")`
- Insert `sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));` AFTER the `<body>` line and BEFORE the `<h1>` line

For `SearchHtmlExportService.cs`:
- `BuildHtml(IReadOnlyList<SearchResult> results, ReportBranding? branding = null)`
- This file uses raw string literal (`"""`). Find `<body>` followed by `<h1>File Search Results</h1>`.
- Split the raw string: close the raw string after `<body>`, append the branding header call, then start a new raw string or `sb.AppendLine` for the `<h1>`. The simplest approach: break the raw string literal at the `<body>` / `<h1>` boundary and insert `sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));` between the two pieces.

For `StorageHtmlExportService.cs`:
- Two `BuildHtml` overloads — add `ReportBranding? branding = null` to both
- Both use raw string literals. Same injection approach: break at `<body>` / `<h1>` boundary.
- The 2-param `BuildHtml` also needs the branding param: `BuildHtml(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, ReportBranding? branding = null)`

For `DuplicatesHtmlExportService.cs`:
- `BuildHtml(IReadOnlyList<DuplicateGroup> groups, ReportBranding? branding = null)`
- Raw string literal. Same injection approach.

For `UserAccessHtmlExportService.cs`:
- `BuildHtml(IReadOnlyList<UserAccessEntry> entries, ReportBranding? branding = null)`
- Uses `sb.AppendLine("<body>");` and `sb.AppendLine("<h1>User Access Audit Report</h1>");`
- Insert `sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));` between them.

**Step 3: Modify WriteAsync signatures.** Add `ReportBranding? branding = null` as the LAST parameter on each WriteAsync overload. Inside each WriteAsync, pass `branding` through to the corresponding `BuildHtml` call.

Per RESEARCH Pitfall 2: Place `branding` AFTER `CancellationToken ct` in WriteAsync signatures so existing positional callers are unaffected:
```csharp
public async Task WriteAsync(..., CancellationToken ct, ReportBranding? branding = null)
```

**CRITICAL:** Do NOT change the `_togIdx` reset logic in `StorageHtmlExportService.BuildHtml` (see RESEARCH Pitfall 5).

**CRITICAL:** Every existing caller without the branding parameter must compile unchanged. The `= null` default handles this.
dotnet build --no-restore -warnaserror All 5 HTML export services accept optional ReportBranding parameter on BuildHtml and WriteAsync. BrandingHtmlHelper.BuildBrandingHeader is called between body and h1 in each. Build passes with zero warnings. No existing callers broken. Task 2: Extend export tests to verify branding injection SharepointToolbox.Tests/Services/Export/HtmlExportServiceTests.cs, SharepointToolbox.Tests/Services/Export/SearchExportServiceTests.cs, SharepointToolbox.Tests/Services/Export/StorageHtmlExportServiceTests.cs, SharepointToolbox.Tests/Services/Export/DuplicatesHtmlExportServiceTests.cs, SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs - Test 1 (HtmlExportServiceTests): BuildHtml with ReportBranding(mspLogo, null) produces HTML containing img tag with MSP logo data-URI - Test 2 (HtmlExportServiceTests): BuildHtml with null branding produces HTML that does NOT contain "branding-header" or data-URI img tags - Test 3 (HtmlExportServiceTests): BuildHtml with both logos produces HTML containing two img tags - Test 4 (SearchExportServiceTests): BuildHtml with branding contains img tag between body and h1 - Test 5 (StorageHtmlExportServiceTests): BuildHtml with branding contains img tag - Test 6 (DuplicatesHtmlExportServiceTests): BuildHtml with branding contains img tag - Test 7 (UserAccessHtmlExportServiceTests): BuildHtml with branding contains img tag - Test 8 (regression): Each existing test still passes unchanged (no branding = same output) Add new test methods to each existing test file. Each test file already has helper methods for creating test data (e.g., `MakeEntry` in HtmlExportServiceTests). Use the same pattern.
Create a shared helper in each test class:
```csharp
private static ReportBranding MakeBranding(bool msp = true, bool client = false)
{
    var mspLogo = msp ? new LogoData { Base64 = "bXNw", MimeType = "image/png" } : null;
    var clientLogo = client ? new LogoData { Base64 = "Y2xpZW50", MimeType = "image/jpeg" } : null;
    return new ReportBranding(mspLogo, clientLogo);
}
```

For HtmlExportServiceTests (the most thorough — 3 new tests):
```csharp
[Fact]
public void BuildHtml_WithMspBranding_ContainsMspLogoImg()
{
    var entry = MakeEntry("Test", "test@contoso.com");
    var svc = new HtmlExportService();
    var html = svc.BuildHtml(new[] { entry }, MakeBranding(msp: true, client: false));
    Assert.Contains("data:image/png;base64,bXNw", html);
}

[Fact]
public void BuildHtml_WithNullBranding_ContainsNoLogoImg()
{
    var entry = MakeEntry("Test", "test@contoso.com");
    var svc = new HtmlExportService();
    var html = svc.BuildHtml(new[] { entry });
    Assert.DoesNotContain("data:image/png;base64,", html);
}

[Fact]
public void BuildHtml_WithBothLogos_ContainsTwoImgs()
{
    var entry = MakeEntry("Test", "test@contoso.com");
    var svc = new HtmlExportService();
    var html = svc.BuildHtml(new[] { entry }, MakeBranding(msp: true, client: true));
    Assert.Contains("data:image/png;base64,bXNw", html);
    Assert.Contains("data:image/jpeg;base64,Y2xpZW50", html);
}
```

For each of the other 4 test files, add one test confirming branding injection works:
```csharp
[Fact]
public void BuildHtml_WithBranding_ContainsLogoImg()
{
    // Use existing test data creation pattern from the file
    var svc = new XxxHtmlExportService();
    var html = svc.BuildHtml(testData, MakeBranding(msp: true));
    Assert.Contains("data:image/png;base64,bXNw", html);
}
```

Add `using SharepointToolbox.Core.Models;` to each test file if not present.
dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Export" --no-build -q All 5 export test files have branding tests. Tests confirm: branding img tags appear when branding is provided, no img tags appear when branding is null. All existing export tests continue to pass (regression verified). ```bash dotnet build --no-restore -warnaserror dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Export" --no-build -q dotnet test SharepointToolbox.Tests --no-build -q ``` All three commands must pass with zero failures. The last command verifies no regressions across the full test suite.

<success_criteria>

  • All 5 HTML export services have optional ReportBranding? branding = null on BuildHtml and WriteAsync
  • Branding header is injected between body and h1 via BrandingHtmlHelper.BuildBrandingHeader call
  • Default null parameter preserves backward compatibility (existing callers compile unchanged)
  • Tests verify branding img tags appear when branding is provided
  • Tests verify no img tags appear when branding is null (identical to pre-branding output)
  • Full test suite passes with no regressions </success_criteria>
After completion, create `.planning/phases/11-html-export-branding/11-02-SUMMARY.md`