docs(11): create phase plan for HTML export branding and ViewModel integration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
308
.planning/phases/11-html-export-branding/11-02-PLAN.md
Normal file
308
.planning/phases/11-html-export-branding/11-02-PLAN.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
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 BuildSimplifiedHtml(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)`
|
||||
- `BuildSimplifiedHtml(IReadOnlyList<SimplifiedPermissionEntry> entries, ReportBranding? branding = null)`
|
||||
- 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>
|
||||
Reference in New Issue
Block a user