diff --git a/.planning/research/SUMMARY.md b/.planning/research/SUMMARY.md index c8112e3..f8043d6 100644 --- a/.planning/research/SUMMARY.md +++ b/.planning/research/SUMMARY.md @@ -2,6 +2,349 @@ **Project:** SharePoint Toolbox — C#/WPF SharePoint Online Administration Desktop Tool **Domain:** SharePoint Online administration, auditing, and provisioning (MSP / IT admin) +**Researched:** 2026-04-02 (v1.0 original) | 2026-04-08 (v2.2 addendum) +**Confidence:** HIGH + +--- + +> **Note:** This file contains two sections. The original v1.0 research summary is preserved below +> the v2.2 section. The roadmapper should consume **v2.2 first** for the current milestone. + +--- + +# v2.2 Research Summary — Report Branding & User Directory + +**Milestone:** v2.2 — HTML Report Branding (MSP/client logos) + User Directory Browse Mode +**Synthesized:** 2026-04-08 +**Sources:** STACK.md (v2.2 addendum), FEATURES.md (v2.2), ARCHITECTURE.md (v2.2), PITFALLS.md (v2.2 addendum) + +--- + +## Executive Summary + +v2.2 adds two independent, self-contained features to a mature WPF MVVM codebase: logo branding +across all five HTML export services, and a full-directory browse mode as an alternative to the +existing people-picker in the User Access Audit tab. Both features are well within the capabilities +of the existing stack — no new NuGet packages are required. The implementation path is low risk +because neither feature touches the audit execution engine; they are purely additive layers on top +of proven infrastructure. + +The branding feature follows a single clear pattern: store logos as base64 strings in existing JSON +settings and profile files, pass them at export time via a new optional `ReportBranding` record, and +inject `` tags into a shared HTML header block. The architecture keeps the five export +services independent (each receives an optional parameter) while avoiding code duplication through a +shared header builder. The user directory browse feature adds a new `IGraphUserDirectoryService` +alongside the existing search service, wires it to new ViewModel state in +`UserAccessAuditViewModel`, and presents it as a toggle-panel in the View. The existing audit +pipeline is completely untouched. + +The primary risks are not technical complexity but execution discipline: logo size must be enforced +at import time (512 KB limit) to prevent HTML report bloat, Graph pagination must use `PageIterator` +to handle tenants with more than 999 users, and logo data must be stored as base64 strings (not file +paths) to ensure portability across machines. All three of these are straightforward to implement +once the storage strategy is decided and locked in at the beginning of each feature's implementation +phase. + +--- + +## Key Findings + +### Stack Additions — None Required + +The entire v2.2 scope is served by the existing stack: + +| Capability | Provided By | Notes | +|---|---|---| +| Logo encoding (file → base64) | BCL `Convert.ToBase64String` + `File.ReadAllBytesAsync` | Zero new packages | +| Logo preview in WPF settings UI | `BitmapImage` (WPF PresentationCore, already a transitive dep) | Standard WPF pattern | +| Logo file picker | `OpenFileDialog` (WPF Microsoft.Win32, already used in codebase) | Filter to PNG/JPG/GIF/BMP | +| User directory listing with pagination | `Microsoft.Graph` 5.74.0 `PageIterator` | Already installed | +| Local directory filtering | `ICollectionView.Filter` (WPF System.Windows.Data) | Already used in PermissionsViewModel | +| Logo + profile JSON persistence | `System.Text.Json` + existing Repository pattern | Backward-compatible nullable fields | + +Do NOT add: HTML template engines (Razor/Scriban), image processing libraries (ImageSharp, +Magick.NET), or PDF export libraries. All explicitly out of scope. + +--- + +### Feature Table Stakes vs. Differentiators + +**Feature 1: HTML Report Branding** + +Table stakes (must ship): +- MSP global logo in every HTML report header +- Client (per-tenant) logo in report header +- Logo renders without external URL (data-URI embedding for self-contained HTML portability) +- Graceful absence — no broken image icon when logo is not configured +- Consistent placement across all five HTML export types + +Differentiators (build after table stakes): +- Auto-pull client logo from Microsoft Entra tenant branding (`GET /organization/{id}/branding/localizations/default/bannerLogo`) — zero-config path using the existing `User.Read` delegated scope +- Report timestamp and tenant display name in header + +Anti-features — do not build: +- Per-tenant CSS color themes (design system complexity, disproportionate to MSP value) +- PDF export with embedded logo (requires third-party binary dependency) +- SVG logo support (XSS risk in data-URIs; PNG/JPG/GIF/BMP only) +- Hotlinked logo URL field (breaks offline/archived reports) + +**Feature 2: User Directory Browse Mode** + +Table stakes (must ship): +- Full directory listing (all enabled member users) with pagination +- In-memory text filter on DisplayName/UPN/Mail without server round-trips +- Sortable columns (Name, UPN) +- Select user from list to trigger existing audit pipeline +- Loading indicator with user count feedback ("Loaded X users...") +- Toggle between Browse mode and Search (people-picker) mode + +Differentiators (add after core browse is stable): +- Filter by account type (member vs. guest toggle) +- Department / Job Title columns +- Session-scoped directory cache (invalidated on tenant switch) + +Anti-features — do not build: +- Eager load on tab open (large tenants block UI and risk throttling) +- Delta query / incremental sync (wrong pattern for single-session audit) +- Multi-user bulk simultaneous audit (different results model, out of scope) +- Export user directory to CSV (identity reporting, not access audit) + +**Recommended MVP build order:** +1. MSP logo in all HTML reports — highest visible impact, lowest complexity +2. Client logo in HTML reports (import from file) — completes co-branding +3. User directory browse core (load, select, filter, pipe into audit) +4. Auto-pull client logo from Entra branding — add after file import path is proven +5. Directory guest filter + department/jobTitle columns — low-effort polish + +--- + +### Architecture Integration Points and Build Order + +**New files to create (7):** + +| Component | Layer | Purpose | +|---|---|---| +| `Core/Models/BrandingSettings.cs` | Core/Models | MSP logo base64 + MIME type; global, persisted in `branding.json` | +| `Core/Models/ReportBranding.cs` | Core/Models | Lightweight record assembled at export time; NOT persisted | +| `Core/Models/PagedUserResult.cs` | Core/Models | Page of `GraphUserResult` items + next-page cursor token | +| `Infrastructure/Persistence/BrandingRepository.cs` | Infrastructure | Atomic JSON write (mirrors SettingsRepository pattern exactly) | +| `Services/BrandingService.cs` | Services | Orchestrates file read → MIME detect → base64 → save | +| `Services/IGraphUserDirectoryService.cs` | Services | Contract for paginated tenant user enumeration | +| `Services/GraphUserDirectoryService.cs` | Services | Graph API user listing with `PageIterator` cursor pagination | + +**Existing files to modify (17), by risk level:** + +Medium risk (left-panel restructure or new async command): +- `ViewModels/Tabs/UserAccessAuditViewModel.cs` — add `IGraphUserDirectoryService` injection + browse mode state/commands +- `Views/Tabs/UserAccessAuditView.xaml` — add mode toggle + browse panel in left column + +Low risk (optional param or uniform inject-and-call pattern, batchable): +- All 5 `Services/Export/*HtmlExportService.cs` — add `ReportBranding? branding = null` optional parameter +- `PermissionsViewModel`, `StorageViewModel`, `SearchViewModel`, `DuplicatesViewModel` — add `BrandingService` injection + use in `ExportHtmlAsync` +- `SettingsViewModel.cs` — add MSP logo browse/preview/clear commands +- `ProfileManagementViewModel.cs` — add client logo browse/preview/clear commands +- `SettingsView.xaml`, `ProfileManagementDialog.xaml` — add logo UI sections +- `App.xaml.cs` — register 3 new services + +**Dependency-aware build phases:** + +| Phase | Scope | Risk | Gate | +|---|---|---|---| +| A — Models | BrandingSettings, ReportBranding, PagedUserResult, TenantProfile logo fields | None | POCOs; no dependencies | +| B — Services | BrandingRepository, BrandingService, IGraphUserDirectoryService, GraphUserDirectoryService | Low | Unit-testable with mocks; Phase A required | +| C — Export services | Add optional `ReportBranding?` to all 5 HTML export services | Low | Phase A required; regression tests: null branding produces identical HTML | +| D — Branding ViewModels | SettingsVM, ProfileManagementVM, 4 export VMs, App.xaml.cs registration | Low | Phase B+C required; steps are identical pattern, batch them | +| E — Directory ViewModel | UserAccessAuditViewModel browse mode state + commands | Medium | Phase B required; do after branding ViewModel pattern is proven | +| F — Branding Views | SettingsView.xaml, ProfileManagementDialog.xaml, base64→BitmapSource converter | Low | Phase D required; write converter once, reuse in both views | +| G — Directory View | UserAccessAuditView.xaml + code-behind SelectionChanged handler | Medium | Phase E required; do last, after ViewModel unit tests pass | + +Key architectural constraints (must not violate): +- **Client logo on `TenantProfile`, NOT in `BrandingSettings`.** Client logos are per-tenant; mixing them with global MSP settings makes per-profile deletion and serialization awkward. +- **Logos stored as base64 strings in JSON, not as file paths.** File paths become stale when the tool is redistributed to another machine. Decided once at Phase A; all downstream phases depend on it. +- **Export services use optional `ReportBranding?` parameter, not required.** All existing call sites compile unchanged; branding is injected only where desired. +- **No `IHtmlExportService` interface for this change.** The existing 5-concrete-classes pattern needs no interface for an optional parameter addition. +- **`GraphUserDirectoryService` is a new service, separate from `GraphUserSearchService`.** Different call patterns (no `startsWith` filter, different pagination), different cancellation needs. +- **Do NOT load the directory automatically on tab open.** Require explicit "Load Directory" button click to avoid blocking UI on large tenants. + +--- + +### Top Pitfalls and Prevention Strategies + +**v2.2-1 (Critical): Base64 logo bloat in every report** +Large source images (300-600 KB originals) become 400-800 KB of base64 inlined in every exported +HTML file, re-allocated on every export call. +Prevention: Enforce 512 KB max at import time in the settings UI. Store pre-encoded base64 in JSON +(computed once on import, never re-encoded). Inject the cached string directly into the `` tag. + +**v2.2-2 (Critical): Graph directory listing silently truncates at 999 users** +`GET /users` returns at most 999 per page. A 5,000-user tenant appears to have 999 users with no +error and no indication of truncation. +Prevention: Use `PageIterator` for all full directory fetches. Never +call `.GetAsync()` on the users collection without following `@odata.nextLink` until null. + +**v2.2-3 (Critical): Directory browse exposes guests, service accounts, and disabled accounts by default** +Raw `GET /users` returns all object types. An MSP tenant with 50+ guest collaborators and service +accounts produces a noisy, confusing directory. +Prevention: Default filter `accountEnabled eq true and userType eq 'Member'`. Expose an "Include +guest accounts" checkbox for explicit opt-in. Apply this filter at the service level, not the +ViewModel, so the ViewModel is not aware of Graph filter syntax. + +**v2.2-4 (Critical): Directory load hangs UI without progress feedback** +3,000-user tenant takes 3-8 seconds. Without count feedback, the user assumes the feature is +broken and may double-click the button (triggering concurrent Graph requests). +Prevention: `DirectoryLoadStatus` observable property updated via `IProgress` in the +PageIterator callback ("Loading... X users"). Guard `AsyncRelayCommand.CanExecute` during loading. +Add cancellation button wired to the same `CancellationToken` passed to `PageIterator.IterateAsync`. + +**v2.2-5 (Critical): Logo file format validation skipped — broken images in reports** +OpenFileDialog filter is not sufficient. Renamed non-image files, corrupted JPEGs, and SVG files +pass the filter but produce broken `` tags in generated reports. +Prevention: Validate by loading as `BitmapImage` in a try/catch before persisting. Check +`PixelWidth` and `PixelHeight` are non-zero. Use `BitmapCacheOption.OnLoad` + retry with +`IgnoreColorProfile` for EXIF-corrupt JPEGs. Reject SVG explicitly. + +**v2.2-6 (Critical): Logo file path stored in JSON becomes stale across machines** +Storing `C:\Users\admin\logos\msp-logo.png` works on the import machine only. After redistribution +or reinstall, the path is missing and logos silently disappear from new reports. +Prevention: Store base64 string directly in `AppSettings` and `TenantProfile` JSON. The original +file path is discarded after import. The settings file becomes fully portable. + +**Moderate pitfalls:** +- v2.2-7: Logo breaks HTML report print layout — apply `max-height: 60px; max-width: 200px` CSS and add `@media print` rules in the report `