309 lines
15 KiB
Markdown
309 lines
15 KiB
Markdown
---
|
|
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>
|