--- phase: 11-html-export-branding plan: 01 type: execute wave: 1 depends_on: [] files_modified: - SharepointToolbox/Core/Models/ReportBranding.cs - SharepointToolbox/Services/Export/BrandingHtmlHelper.cs - SharepointToolbox.Tests/Services/Export/BrandingHtmlHelperTests.cs autonomous: true requirements: - BRAND-05 must_haves: truths: - "BrandingHtmlHelper.BuildBrandingHeader returns a div with two img tags when both MSP and client logos are provided" - "BrandingHtmlHelper.BuildBrandingHeader returns a div with one img tag when only MSP or only client logo is provided" - "BrandingHtmlHelper.BuildBrandingHeader returns empty string when branding is null or both logos are null" - "ReportBranding record bundles MspLogo and ClientLogo as nullable LogoData properties" artifacts: - path: "SharepointToolbox/Core/Models/ReportBranding.cs" provides: "Immutable DTO bundling MSP and client logos for export pipeline" contains: "record ReportBranding" - path: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs" provides: "Static helper generating branding header HTML fragment" contains: "BuildBrandingHeader" - path: "SharepointToolbox.Tests/Services/Export/BrandingHtmlHelperTests.cs" provides: "Unit tests covering all 4 branding states" min_lines: 50 key_links: - from: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs" to: "SharepointToolbox/Core/Models/ReportBranding.cs" via: "parameter type" pattern: "ReportBranding\\?" - from: "SharepointToolbox/Services/Export/BrandingHtmlHelper.cs" to: "SharepointToolbox/Core/Models/LogoData.cs" via: "property access" pattern: "MimeType.*Base64" --- Create the ReportBranding model and BrandingHtmlHelper static class that all HTML exporters will call to render the branding header. Purpose: Centralizes the branding header HTML generation so all 5 exporters share identical markup. This is the foundation artifact that Plan 02 depends on. Output: ReportBranding record, BrandingHtmlHelper static class, and comprehensive unit tests covering all logo combination states. @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/11-html-export-branding/11-CONTEXT.md @.planning/phases/11-html-export-branding/11-RESEARCH.md From SharepointToolbox/Core/Models/LogoData.cs: ```csharp namespace SharepointToolbox.Core.Models; public record LogoData { public string Base64 { get; init; } = string.Empty; public string MimeType { get; init; } = string.Empty; } ``` From SharepointToolbox/Core/Models/BrandingSettings.cs: ```csharp namespace SharepointToolbox.Core.Models; public class BrandingSettings { public LogoData? MspLogo { get; set; } } ``` From SharepointToolbox/Core/Models/TenantProfile.cs (relevant property): ```csharp public LogoData? ClientLogo { get; set; } ``` Task 1: Create ReportBranding record and BrandingHtmlHelper with tests SharepointToolbox/Core/Models/ReportBranding.cs, SharepointToolbox/Services/Export/BrandingHtmlHelper.cs, SharepointToolbox.Tests/Services/Export/BrandingHtmlHelperTests.cs - Test 1: BuildBrandingHeader with null ReportBranding returns empty string - Test 2: BuildBrandingHeader with both logos null returns empty string - Test 3: BuildBrandingHeader with only MspLogo returns HTML with one img tag containing MSP base64 data-URI, no second img - Test 4: BuildBrandingHeader with only ClientLogo returns HTML with one img tag containing client base64 data-URI, no flex spacer div - Test 5: BuildBrandingHeader with both logos returns HTML with two img tags and a flex spacer div between them - Test 6: All generated img tags use inline data-URI format: src="data:{MimeType};base64,{Base64}" - Test 7: All generated img tags have max-height:60px and max-width:200px styles - Test 8: The outer div uses display:flex;gap:16px;align-items:center styling 1. Create `SharepointToolbox/Core/Models/ReportBranding.cs`: ```csharp namespace SharepointToolbox.Core.Models; /// /// Bundles MSP and client logos for passing to export services. /// Export services receive this as a simple DTO — they don't know /// about IBrandingService or ProfileService. /// public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo); ``` This is a positional record (OK because it is never deserialized from JSON — it is always constructed in code). 2. Create `SharepointToolbox/Services/Export/BrandingHtmlHelper.cs`: ```csharp using System.Text; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services.Export; /// /// Generates the branding header HTML fragment for HTML reports. /// Called by each HTML export service between <body> and <h1>. /// Returns empty string when no logos are configured (no broken images). /// internal static class BrandingHtmlHelper { public static string BuildBrandingHeader(ReportBranding? branding) { if (branding is null) return string.Empty; var msp = branding.MspLogo; var client = branding.ClientLogo; if (msp is null && client is null) return string.Empty; var sb = new StringBuilder(); sb.AppendLine("
"); if (msp is not null) sb.AppendLine($" \"\""); if (msp is not null && client is not null) sb.AppendLine("
"); if (client is not null) sb.AppendLine($" \"\""); sb.AppendLine("
"); return sb.ToString(); } } ``` Key decisions per CONTEXT.md locked decisions: - `display:flex;gap:16px` layout (MSP left, client right) - `` inline data-URI format - `max-height:60px` keeps logos reasonable - Returns empty string (not null) when no branding — callers need no null checks - `alt=""` (decorative image — not essential content) - Class is `internal` — only used within Services.Export namespace. Tests access via `InternalsVisibleTo`. 3. Create `SharepointToolbox.Tests/Services/Export/BrandingHtmlHelperTests.cs`: Write tests FIRST (RED phase), then verify the implementation makes them GREEN. Use `[Trait("Category", "Unit")]` per project convention. Create helper method `MakeLogo(string mime = "image/png", string base64 = "dGVzdA==")` to build test LogoData instances. Tests must assert exact HTML structure: data-URI format, style attributes, flex spacer presence/absence. 4. Verify the test project has `InternalsVisibleTo` for the helper class. Check `SharepointToolbox.csproj` or `AssemblyInfo.cs` for `[assembly: InternalsVisibleTo("SharepointToolbox.Tests")]`. If missing, add `` inside an `` in `SharepointToolbox.csproj`.
dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~BrandingHtmlHelperTests" --no-build -q ReportBranding record exists in Core/Models. BrandingHtmlHelper generates correct HTML for all 4 states (null branding, both null, single logo, both logos). All tests pass. Build succeeds with no warnings.
```bash dotnet build --no-restore -warnaserror dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~BrandingHtmlHelper" --no-build -q ``` Both commands must pass with zero failures. - ReportBranding record exists at Core/Models/ReportBranding.cs as a positional record with two nullable LogoData params - BrandingHtmlHelper.BuildBrandingHeader handles all 4 states correctly (null branding, both null, single, both) - Generated HTML uses data-URI format, flex layout, 60px max-height per locked decisions - No broken image tags when logos are missing - Tests cover all states with assertions on HTML structure - Build passes with zero warnings After completion, create `.planning/phases/11-html-export-branding/11-01-SUMMARY.md`