diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 9808ac3..a65c39a 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -18,10 +18,10 @@ Requirements for v2.2 Report Branding & User Directory. Each maps to roadmap pha ### User Directory -- [ ] **UDIR-01**: User can toggle between search mode and directory browse mode in user access audit tab -- [ ] **UDIR-02**: User can browse full tenant user directory with pagination (handles 999+ users) +- [x] **UDIR-01**: User can toggle between search mode and directory browse mode in user access audit tab +- [x] **UDIR-02**: User can browse full tenant user directory with pagination (handles 999+ users) - [x] **UDIR-03**: User can filter directory by user type (member vs guest) -- [ ] **UDIR-04**: User can see department and job title columns in directory list +- [x] **UDIR-04**: User can see department and job title columns in directory list - [ ] **UDIR-05**: User can select one or more users from directory to run the access audit ## Future Requirements @@ -57,10 +57,10 @@ Which phases cover which requirements. Updated during roadmap creation. | BRAND-05 | Phase 11 | Complete | | BRAND-04 | Phase 11 | Complete | | BRAND-02 | Phase 12 | Complete | -| UDIR-01 | Phase 13 | Pending | -| UDIR-02 | Phase 13 | Pending | +| UDIR-01 | Phase 13 | Complete | +| UDIR-02 | Phase 13 | Complete | | UDIR-03 | Phase 13 | Complete | -| UDIR-04 | Phase 13 | Pending | +| UDIR-04 | Phase 13 | Complete | | UDIR-05 | Phase 14 | Pending | **Coverage:** diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 092d7ba..afbf60d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -34,7 +34,7 @@ - [x] **Phase 10: Branding Data Foundation** — Models, repository, and services for logo storage and user directory enumeration (completed 2026-04-08) - [x] **Phase 11: HTML Export Branding + ViewModel Integration** — Inject logos into all 5 HTML report types; wire branding into export-triggering ViewModels and logo management commands (completed 2026-04-08) - [x] **Phase 12: Branding UI Views** — Settings and profile dialog logo sections with live preview; auto-pull client logo from Entra branding API (completed 2026-04-08) -- [ ] **Phase 13: User Directory ViewModel** — Browse mode state, paginated directory load, member/guest filter, and department/job title columns +- [x] **Phase 13: User Directory ViewModel** — Browse mode state, paginated directory load, member/guest filter, and department/job title columns (completed 2026-04-08) - [ ] **Phase 14: User Directory View** — Toggle panel in UserAccessAuditView, user selection to trigger existing audit pipeline ## Phase Details @@ -97,8 +97,8 @@ Plans: 4. Each user row in the observable collection exposes DisplayName, UPN, Department, and JobTitle; Department and JobTitle columns are visible and sortable in the ViewModel's `ICollectionView` **Plans**: 2 plans Plans: -- [ ] 13-01-PLAN.md — Extend GraphDirectoryUser with UserType + service includeGuests parameter -- [ ] 13-02-PLAN.md — UserAccessAuditViewModel directory browse mode (toggle, load, filter, sort, tests) +- [x] 13-01-PLAN.md — Extend GraphDirectoryUser with UserType + service includeGuests parameter +- [x] 13-02-PLAN.md — UserAccessAuditViewModel directory browse mode (toggle, load, filter, sort, tests) ### Phase 14: User Directory View **Goal**: Administrators can toggle into directory browse mode from the user access audit tab, see the paginated user list with filters, and launch an access audit for a selected user. @@ -120,5 +120,5 @@ Plans: | 10. Branding Data Foundation | v2.2 | 3/3 | Complete | 2026-04-08 | | 11. HTML Export Branding + ViewModel Integration | 4/4 | Complete | 2026-04-08 | — | | 12. Branding UI Views | 3/3 | Complete | 2026-04-08 | — | -| 13. User Directory ViewModel | 1/2 | In Progress| | — | +| 13. User Directory ViewModel | 2/2 | Complete | 2026-04-08 | — | | 14. User Directory View | v2.2 | 0/? | Not started | — | diff --git a/.planning/STATE.md b/.planning/STATE.md index 28f445d..06cbd24 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v2.2 milestone_name: Report Branding & User Directory status: completed -stopped_at: Completed 13-01-PLAN.md -last_updated: "2026-04-08T14:02:35.700Z" +stopped_at: Completed 13-02-PLAN.md +last_updated: "2026-04-08T14:08:49.579Z" last_activity: 2026-04-08 — Phase 11 planning completed progress: total_phases: 5 - completed_phases: 3 + completed_phases: 4 total_plans: 12 - completed_plans: 11 + completed_plans: 12 --- # Project State @@ -69,6 +69,8 @@ Decisions are logged in PROJECT.md Key Decisions table. - [Phase 12]: Used Grid overlay with DataTrigger for logo/placeholder visibility toggle in SettingsView - [Phase 12]: Label+StackPanel layout for logo section in ProfileManagementDialog, consistent with SettingsView pattern - [Phase 13]: UserType added as last positional param for backward compat; includeGuests defaults false; userType always in Select +- [Phase 13]: Directory always fetches with includeGuests=true from Graph; member/guest filtering is in-memory via ICollectionView +- [Phase 13]: Separate _directoryCts for directory load cancellation (independent from base class _cts) ### Pending Todos @@ -83,7 +85,7 @@ None. ## Session Continuity -Last session: 2026-04-08T14:02:35.697Z -Stopped at: Completed 13-01-PLAN.md +Last session: 2026-04-08T14:08:49.577Z +Stopped at: Completed 13-02-PLAN.md Resume file: None Next step: `/gsd:execute-phase 11` diff --git a/.planning/phases/11-html-export-branding/11-02-PLAN.md b/.planning/phases/11-html-export-branding/11-02-PLAN.md index 9fb2c22..e6157b9 100644 --- a/.planning/phases/11-html-export-branding/11-02-PLAN.md +++ b/.planning/phases/11-html-export-branding/11-02-PLAN.md @@ -107,7 +107,7 @@ internal static class BrandingHtmlHelper HtmlExportService.cs: ```csharp public string BuildHtml(IReadOnlyList entries) -public string BuildSimplifiedHtml(IReadOnlyList entries) +public string BuildHtml(IReadOnlyList entries) public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct) public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct) ``` @@ -160,7 +160,7 @@ public async Task WriteAsync(IReadOnlyList entries, string file For `HtmlExportService.cs`: - `BuildHtml(IReadOnlyList entries, ReportBranding? branding = null)` - - `BuildSimplifiedHtml(IReadOnlyList entries, ReportBranding? branding = null)` + - `BuildHtml(IReadOnlyList entries, ReportBranding? branding = null)` (second overload of BuildHtml — NOT a separate method) - In both methods, find the line `sb.AppendLine("");` followed by `sb.AppendLine("

...")` - Insert `sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));` AFTER the `` line and BEFORE the `

` line diff --git a/.planning/phases/11-html-export-branding/11-03-PLAN.md b/.planning/phases/11-html-export-branding/11-03-PLAN.md index 90d770c..35bf6f4 100644 --- a/.planning/phases/11-html-export-branding/11-03-PLAN.md +++ b/.planning/phases/11-html-export-branding/11-03-PLAN.md @@ -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 diff --git a/.planning/phases/11-html-export-branding/11-CONTEXT.md b/.planning/phases/11-html-export-branding/11-CONTEXT.md new file mode 100644 index 0000000..8a47515 --- /dev/null +++ b/.planning/phases/11-html-export-branding/11-CONTEXT.md @@ -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` — keep concrete classes with optional branding param | +| Report header layout | `display: flex; gap: 16px` — MSP logo left, client logo right | +| Logo HTML format | `` 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 `` 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)` | `

SharePoint Permissions Report

` at line 76 | +| HtmlExportService (simplified) | Same file | `BuildHtml(IReadOnlyList)` (2nd overload) | Similar pattern | +| SearchHtmlExportService | `Services/Export/SearchHtmlExportService.cs` | `BuildHtml(IReadOnlyList)` | `

File Search Results

` at line 46 | +| StorageHtmlExportService | `Services/Export/StorageHtmlExportService.cs` | `BuildHtml(IReadOnlyList)` | `

SharePoint Storage Metrics

` at line 51 | +| DuplicatesHtmlExportService | `Services/Export/DuplicatesHtmlExportService.cs` | `BuildHtml(IReadOnlyList)` | `

Duplicate Detection Report

` at line 55 | +| UserAccessHtmlExportService | `Services/Export/UserAccessHtmlExportService.cs` | `BuildHtml(IReadOnlyList)` | `

User Access Audit Report

` at line 91 | + +### WriteAsync Signatures (7 overloads across 5 services) + +```csharp +// HtmlExportService.cs +WriteAsync(IReadOnlyList, string filePath, CancellationToken) +WriteAsync(IReadOnlyList, string filePath, CancellationToken) + +// SearchHtmlExportService.cs +WriteAsync(IReadOnlyList, string filePath, CancellationToken) + +// StorageHtmlExportService.cs +WriteAsync(IReadOnlyList, string filePath, CancellationToken) +WriteAsync(IReadOnlyList, IReadOnlyList, string filePath, CancellationToken) + +// DuplicatesHtmlExportService.cs +WriteAsync(IReadOnlyList, string filePath, CancellationToken) + +// UserAccessHtmlExportService.cs +WriteAsync(IReadOnlyList, 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 `` and the existing `

` 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) diff --git a/.planning/phases/11-html-export-branding/11-VALIDATION.md b/.planning/phases/11-html-export-branding/11-VALIDATION.md new file mode 100644 index 0000000..792ea70 --- /dev/null +++ b/.planning/phases/11-html-export-branding/11-VALIDATION.md @@ -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 `` 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 diff --git a/.planning/phases/12-branding-ui-views/12-01-PLAN.md b/.planning/phases/12-branding-ui-views/12-01-PLAN.md new file mode 100644 index 0000000..b6728e3 --- /dev/null +++ b/.planning/phases/12-branding-ui-views/12-01-PLAN.md @@ -0,0 +1,351 @@ +--- +phase: 12-branding-ui-views +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs + - SharepointToolbox/App.xaml + - SharepointToolbox/Localization/Strings.resx + - SharepointToolbox/Localization/Strings.fr.resx + - SharepointToolbox/ViewModels/ProfileManagementViewModel.cs + - SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs + - SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs +autonomous: true +requirements: + - BRAND-02 + - BRAND-04 + +must_haves: + truths: + - "Base64ToImageSourceConverter converts a data URI string to a non-null BitmapImage" + - "Base64ToImageSourceConverter returns null for null, empty, or malformed input" + - "Converter is registered in App.xaml as a global resource with key Base64ToImageConverter" + - "ProfileManagementViewModel exposes ClientLogoPreview (string?) that updates when SelectedProfile changes, and after Browse/Clear/AutoPull commands" + - "Localization keys for logo UI exist in both EN and FR resource files" + artifacts: + - path: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs" + provides: "IValueConverter converting data URI strings to BitmapImage for WPF Image binding" + contains: "class Base64ToImageSourceConverter" + - path: "SharepointToolbox/App.xaml" + provides: "Global converter registration" + contains: "Base64ToImageConverter" + - path: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs" + provides: "ClientLogoPreview observable property for client logo display" + contains: "ClientLogoPreview" + - path: "SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs" + provides: "Unit tests for converter behavior" + min_lines: 30 + key_links: + - from: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs" + to: "SharepointToolbox/App.xaml" + via: "resource registration" + pattern: "Base64ToImageConverter" + - from: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs" + to: "SharepointToolbox/Core/Models/LogoData.cs" + via: "data URI formatting" + pattern: "ClientLogoPreview" +--- + + +Create the Base64ToImageSourceConverter, add localization keys for logo UI, register the converter globally, and add the ClientLogoPreview property to ProfileManagementViewModel. + +Purpose: Provides the infrastructure (converter, localization, ViewModel property) that Plans 02 and 03 need to build the XAML views. + +Output: Converter with tests, localization keys (EN+FR), App.xaml registration, ClientLogoPreview property with test coverage. + + + +@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/12-branding-ui-views/12-RESEARCH.md + + + + +From SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs: +```csharp +private string? _mspLogoPreview; +public string? MspLogoPreview +{ + get => _mspLogoPreview; + private set { _mspLogoPreview = value; OnPropertyChanged(); } +} + +// Set in LoadAsync: +var mspLogo = await _brandingService.GetMspLogoAsync(); +MspLogoPreview = mspLogo is not null ? $"data:{mspLogo.MimeType};base64,{mspLogo.Base64}" : null; + +// Set in BrowseMspLogoAsync: +MspLogoPreview = $"data:{logo.MimeType};base64,{logo.Base64}"; + +// Set in ClearMspLogoAsync: +MspLogoPreview = null; +``` + +From SharepointToolbox/ViewModels/ProfileManagementViewModel.cs (current state): +```csharp +// BrowseClientLogoAsync sets SelectedProfile.ClientLogo = logo (LogoData) +// ClearClientLogoAsync sets SelectedProfile.ClientLogo = null +// AutoPullClientLogoAsync sets SelectedProfile.ClientLogo = logo +// NO ClientLogoPreview string property exists — this plan adds it +``` + +From SharepointToolbox/Core/Models/LogoData.cs: +```csharp +public record LogoData +{ + public string Base64 { get; init; } = string.Empty; + public string MimeType { get; init; } = string.Empty; +} +``` + +From SharepointToolbox/Views/Converters/IndentConverter.cs (converter pattern): +```csharp +public class StringToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is string s && !string.IsNullOrEmpty(s) ? Visibility.Visible : Visibility.Collapsed; + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); +} +``` + +From SharepointToolbox/App.xaml (converter registration pattern): +```xml + +``` + + + + + + + Task 1: Create Base64ToImageSourceConverter with tests + + SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs, + SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs + + + - Test 1: Convert with null value returns null + - Test 2: Convert with empty string returns null + - Test 3: Convert with non-string value returns null + - Test 4: Convert with valid data URI "data:image/png;base64,{validBase64}" returns a non-null BitmapImage + - Test 5: Convert with malformed string (no "base64," prefix) returns null (does not throw) + - Test 6: ConvertBack throws NotImplementedException + + + 1. Create `SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs`: + ```csharp + using System.Globalization; + using System.IO; + using System.Windows.Data; + using System.Windows.Media.Imaging; + + namespace SharepointToolbox.Views.Converters; + + public class Base64ToImageSourceConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not string dataUri || string.IsNullOrEmpty(dataUri)) + return null; + + try + { + var marker = "base64,"; + var idx = dataUri.IndexOf(marker, StringComparison.Ordinal); + if (idx < 0) return null; + + var base64 = dataUri[(idx + marker.Length)..]; + var bytes = System.Convert.FromBase64String(base64); + + var image = new BitmapImage(); + using var ms = new MemoryStream(bytes); + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.StreamSource = ms; + image.EndInit(); + image.Freeze(); + return image; + } + catch + { + return null; + } + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + ``` + Key decisions: + - Parses data URI by finding "base64," marker — works with any mime type + - `BitmapCacheOption.OnLoad` ensures the stream can be disposed immediately + - `Freeze()` makes the image cross-thread safe (required for WPF binding) + - Catches all exceptions to avoid binding errors — returns null on failure + + 2. Create `SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs`: + Write tests FIRST (RED), then verify GREEN. + Use `[Trait("Category", "Unit")]` per project convention. + Note: Tests that create BitmapImage need `[STAThread]` or run on STA thread. Use xUnit's `[WpfFact]` from `Xunit.StaFact` if available, or mark tests with `[Fact]` and handle STA requirement. + For the valid data URI test, use a minimal valid 1x1 PNG base64: `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==` + + IMPORTANT: Check if `Xunit.StaFact` NuGet package is referenced in the test project. If not, the BitmapImage tests may need to be skipped or use a workaround (run converter logic that doesn't need STA for null/empty cases, skip the BitmapImage creation test if STA not available). + + + dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter" --no-build -q + + Converter class exists, handles all edge cases without throwing, tests pass. + + + + Task 2: Register converter in App.xaml + + SharepointToolbox/App.xaml + + + - App.xaml contains a Base64ToImageSourceConverter resource with key "Base64ToImageConverter" + + + 1. In `SharepointToolbox/App.xaml`, add inside ``: + ```xml + + ``` + Place it after the existing converter registrations. + + + dotnet build --no-restore -warnaserror + + Converter is globally available via StaticResource Base64ToImageConverter. + + + + Task 3: Add localization keys for logo UI (EN + FR) + + SharepointToolbox/Localization/Strings.resx, + SharepointToolbox/Localization/Strings.fr.resx + + + - Both resx files contain matching keys for logo UI labels + + + 1. Add to `Strings.resx` (EN): + - `settings.logo.title` = "MSP Logo" + - `settings.logo.browse` = "Import" + - `settings.logo.clear` = "Clear" + - `settings.logo.nopreview` = "No logo configured" + - `profile.logo.title` = "Client Logo" + - `profile.logo.browse` = "Import" + - `profile.logo.clear` = "Clear" + - `profile.logo.autopull` = "Pull from Entra" + - `profile.logo.nopreview` = "No logo configured" + + 2. Add to `Strings.fr.resx` (FR): + - `settings.logo.title` = "Logo MSP" + - `settings.logo.browse` = "Importer" + - `settings.logo.clear` = "Effacer" + - `settings.logo.nopreview` = "Aucun logo configuré" + - `profile.logo.title` = "Logo client" + - `profile.logo.browse` = "Importer" + - `profile.logo.clear` = "Effacer" + - `profile.logo.autopull` = "Importer depuis Entra" + - `profile.logo.nopreview` = "Aucun logo configuré" + + + dotnet build --no-restore -warnaserror + + All 9 localization keys exist in both EN and FR resource files. + + + + Task 4: Add ClientLogoPreview property to ProfileManagementViewModel + + SharepointToolbox/ViewModels/ProfileManagementViewModel.cs, + SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs + + + - ProfileManagementViewModel exposes ClientLogoPreview (string?) property + - ClientLogoPreview updates to data URI when SelectedProfile changes and has a ClientLogo + - ClientLogoPreview updates to null when SelectedProfile is null or has no ClientLogo + - BrowseClientLogoAsync updates ClientLogoPreview after successful import + - ClearClientLogoAsync sets ClientLogoPreview to null + - AutoPullClientLogoAsync updates ClientLogoPreview after successful pull + + + 1. Add to `ProfileManagementViewModel.cs`: + ```csharp + private string? _clientLogoPreview; + public string? ClientLogoPreview + { + get => _clientLogoPreview; + private set { _clientLogoPreview = value; OnPropertyChanged(); } + } + + private static string? FormatLogoPreview(LogoData? logo) + => logo is not null ? $"data:{logo.MimeType};base64,{logo.Base64}" : null; + ``` + + 2. Update `OnSelectedProfileChanged` to refresh preview: + ```csharp + partial void OnSelectedProfileChanged(TenantProfile? value) + { + ClientLogoPreview = FormatLogoPreview(value?.ClientLogo); + // ... existing NotifyCanExecuteChanged calls ... + } + ``` + + 3. Update `BrowseClientLogoAsync` — after `SelectedProfile.ClientLogo = logo;` add: + ```csharp + ClientLogoPreview = FormatLogoPreview(logo); + ``` + + 4. Update `ClearClientLogoAsync` — after `SelectedProfile.ClientLogo = null;` add: + ```csharp + ClientLogoPreview = null; + ``` + + 5. Update `AutoPullClientLogoAsync` — after `SelectedProfile.ClientLogo = logo;` add: + ```csharp + ClientLogoPreview = FormatLogoPreview(logo); + ``` + + 6. Update existing tests in `ProfileManagementViewModelLogoTests.cs`: + - Add test: ClientLogoPreview is null when no profile selected + - Add test: ClientLogoPreview updates when SelectedProfile with logo is selected + - Add test: ClearClientLogoAsync sets ClientLogoPreview to null + + + dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~ProfileManagementViewModel" --no-build -q + + ClientLogoPreview property exists and stays in sync with SelectedProfile.ClientLogo across all mutations. Tests pass. + + + + + +```bash +dotnet build --no-restore -warnaserror +dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter|FullyQualifiedName~ProfileManagementViewModel" --no-build -q +``` +Both commands must pass with zero failures. + + + +- Base64ToImageSourceConverter converts data URI strings to BitmapImage, returns null on bad input +- Converter registered in App.xaml as "Base64ToImageConverter" +- 9 localization keys present in both Strings.resx and Strings.fr.resx +- ProfileManagementViewModel.ClientLogoPreview stays in sync with SelectedProfile.ClientLogo +- All tests pass, build succeeds with zero warnings + + + +After completion, create `.planning/phases/12-branding-ui-views/12-01-SUMMARY.md` + diff --git a/.planning/phases/12-branding-ui-views/12-02-PLAN.md b/.planning/phases/12-branding-ui-views/12-02-PLAN.md new file mode 100644 index 0000000..fdab79b --- /dev/null +++ b/.planning/phases/12-branding-ui-views/12-02-PLAN.md @@ -0,0 +1,182 @@ +--- +phase: 12-branding-ui-views +plan: 02 +type: execute +wave: 2 +depends_on: [12-01] +files_modified: + - SharepointToolbox/Views/Tabs/SettingsView.xaml +autonomous: true +requirements: + - BRAND-02 + +must_haves: + truths: + - "SettingsView displays an MSP Logo section with a labeled GroupBox below the data folder section" + - "The logo section shows a live thumbnail preview bound to MspLogoPreview via Base64ToImageConverter" + - "When MspLogoPreview is null, the preview area shows a 'No logo configured' placeholder text" + - "Import and Clear buttons are bound to BrowseMspLogoCommand and ClearMspLogoCommand respectively" + - "StatusMessage displays below the logo section when set" + artifacts: + - path: "SharepointToolbox/Views/Tabs/SettingsView.xaml" + provides: "MSP logo section with live preview, import, and clear controls" + contains: "MspLogoPreview" + key_links: + - from: "SharepointToolbox/Views/Tabs/SettingsView.xaml" + to: "SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs" + via: "data binding" + pattern: "BrowseMspLogoCommand|ClearMspLogoCommand|MspLogoPreview" + - from: "SharepointToolbox/Views/Tabs/SettingsView.xaml" + to: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs" + via: "StaticResource" + pattern: "Base64ToImageConverter" +--- + + +Add the MSP logo section to SettingsView.xaml with live thumbnail preview, Import and Clear buttons. + +Purpose: Allows administrators to see the current MSP logo and manage it directly from the Settings tab. + +Output: Updated SettingsView.xaml with a logo section that binds to existing ViewModel commands and properties. + + + +@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/12-branding-ui-views/12-RESEARCH.md + + + + +From SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs: +```csharp +public string? MspLogoPreview { get; } // data URI string or null +public IAsyncRelayCommand BrowseMspLogoCommand { get; } +public IAsyncRelayCommand ClearMspLogoCommand { get; } +public string StatusMessage { get; set; } // inherited from FeatureViewModelBase +``` + + +From SharepointToolbox/Views/Tabs/SettingsView.xaml: +```xml + + + +