---
phase: 11-html-export-branding
plan: 02
type: execute
wave: 2
depends_on: ["11-01"]
files_modified:
  - 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
autonomous: true
requirements:
  - BRAND-05

must_haves:
  truths:
    - "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"
  artifacts:
    - path: "SharepointToolbox/Services/Export/HtmlExportService.cs"
      provides: "Permissions HTML export with optional branding"
      contains: "ReportBranding? branding = null"
    - path: "SharepointToolbox/Services/Export/SearchHtmlExportService.cs"
      provides: "Search HTML export with optional branding"
      contains: "ReportBranding? branding = null"
    - path: "SharepointToolbox/Services/Export/StorageHtmlExportService.cs"
      provides: "Storage HTML export with optional branding"
      contains: "ReportBranding? branding = null"
    - path: "SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs"
      provides: "Duplicates HTML export with optional branding"
      contains: "ReportBranding? branding = null"
    - path: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
      provides: "User access HTML export with optional branding"
      contains: "ReportBranding? branding = null"
  key_links:
    - from: "SharepointToolbox/Services/Export/HtmlExportService.cs"
      to: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs"
      via: "static method call"
      pattern: "BrandingHtmlHelper\\.BuildBrandingHeader"
    - from: "SharepointToolbox/Services/Export/SearchHtmlExportService.cs"
      to: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs"
      via: "static method call"
      pattern: "BrandingHtmlHelper\\.BuildBrandingHeader"
    - from: "SharepointToolbox/Services/Export/StorageHtmlExportService.cs"
      to: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs"
      via: "static method call"
      pattern: "BrandingHtmlHelper\\.BuildBrandingHeader"
    - from: "SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs"
      to: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs"
      via: "static method call"
      pattern: "BrandingHtmlHelper\\.BuildBrandingHeader"
    - from: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
      to: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs"
      via: "static method call"
      pattern: "BrandingHtmlHelper\\.BuildBrandingHeader"
---

<objective>
Add optional `ReportBranding? branding = null` parameter to all 5 HTML export services and inject the branding header HTML between `<body>` and `<h1>` 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.
</objective>

<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>

<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

<interfaces>
<!-- From Plan 11-01 (must be completed first) -->

From SharepointToolbox/Core/Models/ReportBranding.cs:
```csharp
namespace SharepointToolbox.Core.Models;
public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo);
```

From SharepointToolbox/Services/Export/BrandingHtmlHelper.cs:
```csharp
namespace SharepointToolbox.Services.Export;
internal static class BrandingHtmlHelper
{
    public static string BuildBrandingHeader(ReportBranding? branding);
}
```

<!-- Current WriteAsync signatures that need branding param added -->

HtmlExportService.cs:
```csharp
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:
```csharp
public string BuildHtml(IReadOnlyList<SearchResult> results)
public async Task WriteAsync(IReadOnlyList<SearchResult> results, string filePath, CancellationToken ct)
```

StorageHtmlExportService.cs:
```csharp
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:
```csharp
public string BuildHtml(IReadOnlyList<DuplicateGroup> groups)
public async Task WriteAsync(IReadOnlyList<DuplicateGroup> groups, string filePath, CancellationToken ct)
```

UserAccessHtmlExportService.cs:
```csharp
public string BuildHtml(IReadOnlyList<UserAccessEntry> entries)
public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct)
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add branding parameter to all 5 HTML export services</name>
  <files>
    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
  </files>
  <action>
    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.
  </action>
  <verify>
    <automated>dotnet build --no-restore -warnaserror</automated>
  </verify>
  <done>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.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Extend export tests to verify branding injection</name>
  <files>
    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
  </files>
  <behavior>
    - 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)
  </behavior>
  <action>
    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.
  </action>
  <verify>
    <automated>dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Export" --no-build -q</automated>
  </verify>
  <done>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).</done>
</task>

</tasks>

<verification>
```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.
</verification>

<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>

<output>
After completion, create `.planning/phases/11-html-export-branding/11-02-SUMMARY.md`
</output>
