# Project Research Summary
**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 `