docs(13-02): complete User Directory ViewModel plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,7 +107,7 @@ internal static class BrandingHtmlHelper
|
||||
HtmlExportService.cs:
|
||||
```csharp
|
||||
public string BuildHtml(IReadOnlyList<PermissionEntry> entries)
|
||||
public string BuildSimplifiedHtml(IReadOnlyList<SimplifiedPermissionEntry> 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)
|
||||
```
|
||||
@@ -160,7 +160,7 @@ public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string file
|
||||
|
||||
For `HtmlExportService.cs`:
|
||||
- `BuildHtml(IReadOnlyList<PermissionEntry> entries, ReportBranding? branding = null)`
|
||||
- `BuildSimplifiedHtml(IReadOnlyList<SimplifiedPermissionEntry> 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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ files_modified:
|
||||
- SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs
|
||||
- SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs
|
||||
- SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
||||
- SharepointToolbox/App.xaml.cs
|
||||
# Note: App.xaml.cs does NOT need changes — DI container auto-resolves IBrandingService for ViewModel constructors
|
||||
autonomous: true
|
||||
requirements:
|
||||
- BRAND-05
|
||||
@@ -103,7 +103,8 @@ Each ViewModel has:
|
||||
|
||||
PermissionsViewModel has two constructors: full (DI) and test (internal, omits export services).
|
||||
UserAccessAuditViewModel also has two constructors.
|
||||
The other 3 ViewModels (Search, Storage, Duplicates) have a single constructor each.
|
||||
StorageViewModel also has two constructors (test constructor at line 151).
|
||||
The other 2 ViewModels (Search, Duplicates) have a single constructor each.
|
||||
|
||||
DI registrations in App.xaml.cs:
|
||||
```csharp
|
||||
@@ -136,7 +137,7 @@ But each ViewModel registration must now resolve IBrandingService in addition to
|
||||
|
||||
3. Modify the DI constructor to accept `IBrandingService brandingService` parameter and assign `_brandingService = brandingService;`.
|
||||
|
||||
4. For ViewModels with a test constructor (PermissionsViewModel, UserAccessAuditViewModel): add `IBrandingService? brandingService = null` as the last parameter, assign `_brandingService = brandingService!;`. Using `null!` is acceptable because test constructors are only used in tests where branding is not exercised. Alternatively, create a no-op implementation — but `null!` matches existing pattern where `_htmlExportService = null` is already used in test constructors.
|
||||
4. For ViewModels with a test constructor (PermissionsViewModel, UserAccessAuditViewModel, StorageViewModel): add `IBrandingService? brandingService = null` as the last parameter, assign `_brandingService = brandingService!;`. Using `null!` is acceptable because test constructors are only used in tests where branding is not exercised. Alternatively, create a no-op implementation — but `null!` matches existing pattern where `_htmlExportService = null` is already used in test constructors. **Verify that existing test files for all 3 ViewModels still compile after the constructor changes.**
|
||||
|
||||
5. Modify `ExportHtmlAsync()` — add branding assembly BEFORE the WriteAsync call:
|
||||
```csharp
|
||||
|
||||
123
.planning/phases/11-html-export-branding/11-CONTEXT.md
Normal file
123
.planning/phases/11-html-export-branding/11-CONTEXT.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
phase: 11
|
||||
title: HTML Export Branding + ViewModel Integration
|
||||
status: ready-for-planning
|
||||
created: 2026-04-08
|
||||
---
|
||||
|
||||
# Phase 11 Context: HTML Export Branding + ViewModel Integration
|
||||
|
||||
## Decided Areas (from Phase 10 context + STATE.md)
|
||||
|
||||
These are locked — do not re-litigate during planning or execution.
|
||||
|
||||
| Decision | Value |
|
||||
|---|---|
|
||||
| Logo storage format | Base64 strings in JSON (not file paths) |
|
||||
| MSP logo location | `BrandingSettings.MspLogo` → `branding.json` via `BrandingRepository` |
|
||||
| Client logo location | `TenantProfile.ClientLogo` (per-tenant, in profile JSON) |
|
||||
| Logo model | `LogoData { string Base64, string MimeType }` — shared by both MSP and client logos |
|
||||
| SVG support | Rejected (XSS risk) — PNG/JPG only |
|
||||
| Export service signature change | Optional `ReportBranding? branding = null` parameter on existing `BuildHtml` methods |
|
||||
| No new interfaces | No `IHtmlExportService<T>` — keep concrete classes with optional branding param |
|
||||
| Report header layout | `display: flex; gap: 16px` — MSP logo left, client logo right |
|
||||
| Logo HTML format | `<img src="data:{MimeType};base64,{Base64}">` inline data-URI |
|
||||
| No new NuGet packages | All capabilities provided by existing stack |
|
||||
|
||||
## Phase Goal
|
||||
|
||||
All five HTML reports display MSP and client logos in a consistent header, and administrators can manage logos from Settings and the profile dialog without touching the View layer.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Running any of the five HTML exports (Permissions, Storage, Search, Duplicates, User Access) produces an HTML file whose header contains the MSP logo `<img>` tag when an MSP logo is configured
|
||||
2. When a client logo is configured for the active tenant, the same HTML export header contains both the MSP logo and the client logo side by side
|
||||
3. When no logo is configured, the HTML export header contains no broken image placeholder and the report renders identically to the pre-branding output
|
||||
4. SettingsViewModel exposes browse/clear commands for MSP logo; ProfileManagementViewModel exposes browse/clear commands for client logo — both commands are exercisable without opening any View
|
||||
5. Auto-pulling the client logo from the tenant's Entra branding API stores the logo in the tenant profile and falls back silently when no Entra branding is configured
|
||||
|
||||
## Depends On
|
||||
|
||||
Phase 10 (completed) — provides `LogoData`, `BrandingSettings`, `BrandingRepository`, `IBrandingService`, `TenantProfile.ClientLogo`
|
||||
|
||||
## Requirements Mapped
|
||||
|
||||
- **BRAND-05**: Logos appear in HTML report headers
|
||||
- **BRAND-04**: Auto-pull client logo from Entra branding API
|
||||
|
||||
## Code Context
|
||||
|
||||
### Phase 10 Infrastructure (already built)
|
||||
|
||||
| Asset | Path | Role |
|
||||
|---|---|---|
|
||||
| LogoData record | `Core/Models/LogoData.cs` | `{ string Base64, string MimeType }` |
|
||||
| BrandingSettings model | `Core/Models/BrandingSettings.cs` | `{ LogoData? MspLogo }` |
|
||||
| TenantProfile model | `Core/Models/TenantProfile.cs` | `{ LogoData? ClientLogo }` (per-tenant) |
|
||||
| IBrandingService | `Services/IBrandingService.cs` | `ImportLogoAsync`, `SaveMspLogoAsync`, `ClearMspLogoAsync`, `GetMspLogoAsync` |
|
||||
| BrandingService | `Services/BrandingService.cs` | Validates PNG/JPG via magic bytes, auto-compresses >512KB |
|
||||
| BrandingRepository | `Infrastructure/Persistence/BrandingRepository.cs` | JSON persistence with SemaphoreSlim + atomic write |
|
||||
|
||||
### HTML Export Services (5 targets for branding injection)
|
||||
|
||||
| Service | Path | `BuildHtml` Signature | Header Location |
|
||||
|---|---|---|---|
|
||||
| HtmlExportService | `Services/Export/HtmlExportService.cs` | `BuildHtml(IReadOnlyList<PermissionEntry>)` | `<h1>SharePoint Permissions Report</h1>` at line 76 |
|
||||
| HtmlExportService (simplified) | Same file | `BuildHtml(IReadOnlyList<SimplifiedPermissionEntry>)` (2nd overload) | Similar pattern |
|
||||
| SearchHtmlExportService | `Services/Export/SearchHtmlExportService.cs` | `BuildHtml(IReadOnlyList<SearchResult>)` | `<h1>File Search Results</h1>` at line 46 |
|
||||
| StorageHtmlExportService | `Services/Export/StorageHtmlExportService.cs` | `BuildHtml(IReadOnlyList<StorageNode>)` | `<h1>SharePoint Storage Metrics</h1>` at line 51 |
|
||||
| DuplicatesHtmlExportService | `Services/Export/DuplicatesHtmlExportService.cs` | `BuildHtml(IReadOnlyList<DuplicateGroup>)` | `<h1>Duplicate Detection Report</h1>` at line 55 |
|
||||
| UserAccessHtmlExportService | `Services/Export/UserAccessHtmlExportService.cs` | `BuildHtml(IReadOnlyList<UserAccessEntry>)` | `<h1>User Access Audit Report</h1>` at line 91 |
|
||||
|
||||
### WriteAsync Signatures (7 overloads across 5 services)
|
||||
|
||||
```csharp
|
||||
// HtmlExportService.cs
|
||||
WriteAsync(IReadOnlyList<PermissionEntry>, string filePath, CancellationToken)
|
||||
WriteAsync(IReadOnlyList<SimplifiedPermissionEntry>, string filePath, CancellationToken)
|
||||
|
||||
// SearchHtmlExportService.cs
|
||||
WriteAsync(IReadOnlyList<SearchResult>, string filePath, CancellationToken)
|
||||
|
||||
// StorageHtmlExportService.cs
|
||||
WriteAsync(IReadOnlyList<StorageNode>, string filePath, CancellationToken)
|
||||
WriteAsync(IReadOnlyList<StorageNode>, IReadOnlyList<FileTypeMetric>, string filePath, CancellationToken)
|
||||
|
||||
// DuplicatesHtmlExportService.cs
|
||||
WriteAsync(IReadOnlyList<DuplicateGroup>, string filePath, CancellationToken)
|
||||
|
||||
// UserAccessHtmlExportService.cs
|
||||
WriteAsync(IReadOnlyList<UserAccessEntry>, string filePath, CancellationToken)
|
||||
```
|
||||
|
||||
### ViewModels That Trigger Exports (5 targets)
|
||||
|
||||
| ViewModel | Path | Export Call Pattern |
|
||||
|---|---|---|
|
||||
| PermissionsViewModel | `ViewModels/Tabs/PermissionsViewModel.cs` | `_htmlExportService.WriteAsync(Results/SimplifiedResults, ...)` |
|
||||
| SearchViewModel | `ViewModels/Tabs/SearchViewModel.cs` | `_htmlExportService.WriteAsync(Results, ...)` |
|
||||
| StorageViewModel | `ViewModels/Tabs/StorageViewModel.cs` | `_htmlExportService.WriteAsync(Results, FileTypeMetrics, ...)` |
|
||||
| DuplicatesViewModel | `ViewModels/Tabs/DuplicatesViewModel.cs` | `_htmlExportService.WriteAsync(_lastGroups, ...)` |
|
||||
| UserAccessAuditViewModel | `ViewModels/Tabs/UserAccessAuditViewModel.cs` | `_htmlExportService.WriteAsync(Results, ...)` |
|
||||
|
||||
### Logo Management ViewModels (2 targets)
|
||||
|
||||
| ViewModel | Path | Current State |
|
||||
|---|---|---|
|
||||
| SettingsViewModel | `ViewModels/Tabs/SettingsViewModel.cs` | Has language + data folder; needs MSP logo browse/clear commands |
|
||||
| ProfileManagementViewModel | `ViewModels/ProfileManagementViewModel.cs` | Has CRUD profiles; needs client logo browse/clear/auto-pull commands |
|
||||
|
||||
### DI Registration
|
||||
|
||||
`App.xaml.cs` — All export services registered as `Transient`, branding services registered as `Singleton`.
|
||||
|
||||
### HTML Generation Pattern
|
||||
|
||||
All 5 HTML exporters use StringBuilder with inline HTML/CSS/JS. No template files. Each builds a self-contained single-file report. The branding header must be injected between `<body>` and the existing `<h1>` tag in each exporter.
|
||||
|
||||
## Deferred Ideas (out of scope for Phase 11)
|
||||
|
||||
- Logo preview in Settings UI (Phase 12)
|
||||
- Live thumbnail preview after import (Phase 12)
|
||||
- "Pull from Entra" button in profile dialog UI (Phase 12)
|
||||
- User directory browse mode (Phase 13-14)
|
||||
99
.planning/phases/11-html-export-branding/11-VALIDATION.md
Normal file
99
.planning/phases/11-html-export-branding/11-VALIDATION.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
phase: 11
|
||||
slug: html-export-branding
|
||||
status: draft
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: false
|
||||
created: 2026-04-08
|
||||
---
|
||||
|
||||
# Phase 11 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | xUnit 2.9.3 + Moq 4.20.72 |
|
||||
| **Config file** | `SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` |
|
||||
| **Quick run command** | `dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Export" --no-build -q` |
|
||||
| **Full suite command** | `dotnet test SharepointToolbox.Tests --no-build` |
|
||||
| **Estimated runtime** | ~15 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** `dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Export" --no-build -q`
|
||||
- **After every plan wave:** `dotnet test SharepointToolbox.Tests --no-build`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 15 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
|
||||
| 11-01-01 | 01 | 1 | BRAND-05 | unit | `dotnet test --filter "FullyQualifiedName~BrandingHtmlHelper" --no-build` | No (W0) | pending |
|
||||
| 11-02-01 | 02 | 2 | BRAND-05 | unit | `dotnet test --filter "FullyQualifiedName~Export" --no-build -q` | Yes (extend) | pending |
|
||||
| 11-02-02 | 02 | 2 | BRAND-05 | unit | same as above | Yes (extend) | pending |
|
||||
| 11-03-01 | 03 | 3 | BRAND-05 | integration | `dotnet build --no-restore -warnaserror && dotnet test --no-build -q` | Yes (compile check) | pending |
|
||||
| 11-04-01 | 04 | 1 | BRAND-04 | unit | `dotnet test --filter "FullyQualifiedName~ProfileService" --no-build` | Yes (extend) | pending |
|
||||
| 11-04-02 | 04 | 1 | BRAND-04 | unit | `dotnet test --filter "FullyQualifiedName~SettingsViewModel or FullyQualifiedName~ProfileManagement" --no-build` | No (W0) | pending |
|
||||
|
||||
*Status: pending / green / red / flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `SharepointToolbox.Tests/Services/Export/BrandingHtmlHelperTests.cs` — covers BRAND-05a/b/c (both logos, single logo, no logos)
|
||||
- [ ] `SharepointToolbox.Tests/ViewModels/SettingsViewModelLogoTests.cs` — covers MSP logo browse/clear commands
|
||||
- [ ] `SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs` — covers client logo + auto-pull (BRAND-04)
|
||||
- [ ] Extend existing `HtmlExportServiceTests.cs` — covers BRAND-05d/e (branding present/absent)
|
||||
- [ ] Extend existing `SearchExportServiceTests.cs`, `StorageHtmlExportServiceTests.cs`, `DuplicatesHtmlExportServiceTests.cs`, `UserAccessHtmlExportServiceTests.cs` — covers BRAND-05f
|
||||
|
||||
*Existing infrastructure covers test framework setup.*
|
||||
|
||||
---
|
||||
|
||||
## Requirements -> Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| BRAND-05a | BrandingHtmlHelper produces correct HTML for both logos | unit | `dotnet test --filter "FullyQualifiedName~BrandingHtmlHelper" --no-build -q` | No - Wave 0 |
|
||||
| BRAND-05b | BrandingHtmlHelper produces empty string for no logos | unit | same as above | No - Wave 0 |
|
||||
| BRAND-05c | BrandingHtmlHelper handles single logo (MSP only / client only) | unit | same as above | No - Wave 0 |
|
||||
| BRAND-05d | HtmlExportService.BuildHtml with branding includes header | unit | `dotnet test --filter "FullyQualifiedName~HtmlExportServiceTests" --no-build -q` | Yes (extend) |
|
||||
| BRAND-05e | HtmlExportService.BuildHtml without branding unchanged | unit | same as above | Yes (extend) |
|
||||
| BRAND-05f | Each of 5 exporters injects branding header between body and h1 | unit | `dotnet test --filter "FullyQualifiedName~Export" --no-build -q` | Partially (extend existing) |
|
||||
| BRAND-04a | Auto-pull handles 404 (no branding) gracefully | unit | `dotnet test --filter "FullyQualifiedName~AutoPull" --no-build -q` | No - Wave 0 |
|
||||
| BRAND-04b | Auto-pull handles empty stream gracefully | unit | same as above | No - Wave 0 |
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| MSP logo appears in exported HTML report | BRAND-05 | Requires visual inspection of rendered HTML | 1. Import MSP logo 2. Run permissions export 3. Open HTML in browser 4. Verify logo in header |
|
||||
| Both logos side by side in report header | BRAND-05 | Requires visual layout check | 1. Import MSP and client logo 2. Run any export 3. Verify both logos rendered side by side |
|
||||
| No broken images when no logo configured | BRAND-05 | Requires visual regression check | 1. Clear all logos 2. Run export 3. Compare output to pre-branding export |
|
||||
| Auto-pull from tenant without Entra branding | BRAND-04 | Requires live tenant without branding | 1. Select tenant without Entra branding 2. Click auto-pull 3. Verify silent fallback (no crash, no broken state) |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [x] Wave 0 covers all MISSING references
|
||||
- [x] No watch-mode flags
|
||||
- [x] Feedback latency < 15s
|
||||
- [x] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** approved
|
||||
Reference in New Issue
Block a user