# Feature Landscape **Domain:** MSP IT admin desktop tool — SharePoint audit report branding + user directory browse **Milestone:** v2.2 — Report Branding & User Directory **Researched:** 2026-04-08 **Overall confidence:** HIGH (verified via official Graph API docs + direct codebase inspection) --- ## Scope Boundary This file covers only the two net-new features in v2.2: 1. HTML report branding (MSP logo + client logo per tenant) 2. User directory browse mode in the user access audit tab Everything else is already shipped. Dependencies on existing code are called out explicitly. --- ## Feature 1: HTML Report Branding ### Table Stakes Features an MSP admin expects without being asked. If missing, the reports feel unfinished and unprofessional to hand to a client. | Feature | Why Expected | Complexity | Notes | |---------|--------------|------------|-------| | MSP global logo in report header | Every white-label MSP tool shows the MSP's own brand on deliverables | Low | Single image stored in AppSettings or a dedicated branding settings section | | Client (per-tenant) logo in report header | MSP reports are client-facing; client should see their own logo next to the MSP's | Medium | Stored in TenantProfile; 2 sources: import from file or pull from tenant | | Logo renders in self-contained HTML (no external URL) | Reports are often emailed or archived; external URLs break offline | Low | Base64-encode and embed as `data:image/...;base64,...` inline in `` block entirely when no logo is set | | Consistent placement across all HTML export types | App already ships 5+ HTML exporters; logos must appear in all of them | Medium | Extract a shared header-builder method or inject a branding context into each export service | ### Differentiators Features not expected by default, but add meaningful value once table stakes are covered. | Feature | Value Proposition | Complexity | Notes | |---------|-------------------|------------|-------| | Auto-pull client logo from Microsoft Entra tenant branding | Zero-config for tenants that already have a banner logo set in Entra ID | Medium | Graph API: `GET /organization/{id}/branding/localizations/default/bannerLogo` returns raw image bytes. Least-privileged scope is `User.Read` (delegated, already in use). Returns empty body or 404 when not configured — must handle gracefully. | | Report timestamp and tenant display name in header | Contextualizes archived reports without needing to inspect the filename | Low | TenantProfile.TenantUrl already available; display name derivable from domain | ### Anti-Features Do not build these. They add scope without proportionate MSP value. | Anti-Feature | Why Avoid | What to Do Instead | |--------------|-----------|-------------------| | Color theme / CSS customization per tenant | Complexity explodes — per-tenant CSS is a design system problem, not an admin tool feature | Stick to a single professional neutral theme; logo is sufficient branding | | PDF export with embedded logo | PDF generation requires a third-party library (iTextSharp, QuestPDF, etc.) adding binary size to the 200 MB EXE | Document in release notes that users can print-to-PDF from browser | | Animated or SVG logo support | MIME handling complexity; SVG in data-URIs introduces XSS risk | Support PNG/JPG/GIF only; reject SVG at import time | | Logo URL field (hotlinked) | Reports break when URL becomes unavailable; creates external dependency for a local-first tool | Force file import with base64 embedding | ### Feature Dependencies ``` AppSettings + MspLogoBase64 (string?, nullable) TenantProfile + ClientLogoBase64 (string?, nullable) + ClientLogoSource (enum: None | Imported | AutoPulled) Shared branding helper → called by HtmlExportService, UserAccessHtmlExportService, StorageHtmlExportService, DuplicatesHtmlExportService, SearchHtmlExportService Auto-pull code path → Graph API call via existing GraphClientFactory Logo import UI → WPF OpenFileDialog -> File.ReadAllBytes -> Convert.ToBase64String -> stored in profile JSON via existing ProfileRepository ``` **Key existing code note:** All 5+ HTML export services currently build their `` independently with no shared header. Branding requires one of: - (a) a `ReportBrandingContext` record passed into each exporter's `BuildHtml` method, or - (b) a `HtmlReportHeaderBuilder` static/injectable helper all exporters call. Option (b) is lower risk — it does not change method signatures that existing unit tests already call. ### Complexity Assessment | Sub-task | Complexity | Reason | |----------|------------|--------| | AppSettings + TenantProfile model field additions | Low | Trivial nullable-string fields; JSON serialization already in place | | Settings UI: MSP logo upload + preview | Low | WPF OpenFileDialog + BitmapImage from base64, standard pattern | | ProfileManagementDialog: client logo upload per tenant | Low | Same pattern as MSP logo | | Shared HTML header builder with logo injection | Low-Medium | One helper; replaces duplicated header HTML in 5 exporters | | Auto-pull from Entra `bannerLogo` endpoint | Medium | Async Graph call; must handle 404, empty stream, no branding configured | | Localization keys EN/FR for new labels | Low | ~6-10 new keys; 220+ already managed | --- ## Feature 2: User Directory Browse Mode ### Table Stakes Features an admin expects when a "browse all users" mode is offered alongside the existing search. | Feature | Why Expected | Complexity | Notes | |---------|--------------|------------|-------| | Full directory listing (all member users, paginated) | Browse implies seeing everyone, not just name-search hits | Medium | Graph `GET /users` with `$top=100`, follow `@odata.nextLink` until null. Max page size is 999 but 100 pages give better progress feedback | | Searchable/filterable within the loaded list | Once loaded, admins filter locally without re-querying | Low | In-memory filter on DisplayName, UPN, Mail — same pattern used in PermissionsView DataGrid | | Sortable columns (Name, UPN) | Standard expectation for any directory table | Low | WPF DataGrid column sorting, already used in other tabs | | Select user from list to run access audit | The whole point — browse replaces the people-picker for users the admin cannot spell | Low | Bind selected item; reuse the existing IUserAccessAuditService pipeline unchanged | | Loading indicator with progress count | Large tenants (5k+ users) take several seconds to page through | Low | Existing OperationProgress pattern; show "Loaded X users..." counter | | Toggle between Browse mode and Search (people-picker) mode | Search is faster for known users; browse is for discovery | Low | RadioButton or ToggleButton in the tab toolbar; visibility-toggle two panels | ### Differentiators | Feature | Value Proposition | Complexity | Notes | |---------|-------------------|------------|-------| | Filter by account type (member vs guest) | MSPs care about guest proliferation; helps scope audit targets | Low | Graph returns `userType` field; add a toggle filter. Include in `$select` | | Department / Job Title columns | Helps identify the right user in large tenants with common names | Low-Medium | Include `department`, `jobTitle` in `$select`; optional columns in DataGrid | | Session-scoped directory cache | Avoids re-fetching full tenant list on every tab visit | Medium | Store list in ViewModel or session-scoped service; invalidate on TenantSwitchedMessage | ### Anti-Features | Anti-Feature | Why Avoid | What to Do Instead | |--------------|-----------|-------------------| | Eager load on tab open | Large tenants (10k+ users) block UI and risk Graph throttling on every tab navigation | Lazy-load on explicit "Load Directory" button click; show a clear affordance | | Delta query / incremental sync | Delta queries are for maintaining a local replica over time; wrong pattern for a one-time audit session | Single paginated GET per session; add a Refresh button | | Multi-user bulk select for simultaneous audit | The audit pipeline is per-user by design; multi-user requires a fundamentally different results model | Out of scope; single-user selection only | | Export the user directory to CSV | That is an identity reporting feature (AdminDroid et al.), not an access audit feature | Out of scope for this milestone | | Show disabled accounts by default | Disabled users do not have active SharePoint access; pollutes the list for audit purposes | Default `$filter=accountEnabled eq true`; optionally expose a toggle | ### Feature Dependencies ``` New IGraphDirectoryService + GraphDirectoryService → GET /users?$select=displayName,userPrincipalName,mail,jobTitle,department,userType &$filter=accountEnabled eq true &$top=100 → Follow @odata.nextLink in a loop until null → Uses existing GraphClientFactory (DI, unchanged) UserAccessAuditViewModel additions: + IsBrowseMode (bool property, toggle) + DirectoryUsers (ObservableCollection or new DirectoryUserEntry model) + DirectoryFilterText (string, filters in-memory) + LoadDirectoryCommand (async, cancellable) + IsDirectoryLoading (bool) + SelectedDirectoryUser → feeds into existing audit execution path TenantSwitchedMessage handler in ViewModel: clear DirectoryUsers, reset IsBrowseMode UserAccessAuditView.xaml: + Toolbar toggle (Search | Browse) + Visibility-collapsed people-picker panel when in browse mode + New DataGrid panel for browse mode ``` **Key existing code note:** `GraphUserSearchService` does filtered search only (`startsWith` filter + `ConsistencyLevel: eventual`). Directory listing is a different call pattern — no filter, plain pagination without `ConsistencyLevel`. A separate `GraphDirectoryService` is cleaner than extending the existing service; search and browse have different cancellation and retry needs. ### Complexity Assessment | Sub-task | Complexity | Reason | |----------|------------|--------| | IGraphDirectoryService + GraphDirectoryService (pagination loop) | Low-Medium | Standard Graph paging; same GraphClientFactory in DI | | ViewModel additions (browse toggle, load command, filter, loading state) | Medium | New async command with progress, cancellation on tenant switch | | View XAML: toggle + browse DataGrid panel | Medium | Visibility-toggle two panels; DataGrid column definitions | | In-memory filter + column sort | Low | DataGrid pattern already used in PermissionsView | | Loading indicator integration | Low | OperationProgress + IsLoading used by every tab | | Localization keys EN/FR | Low | ~8-12 new keys | | Unit tests for GraphDirectoryService | Low | Same mock pattern as GraphUserSearchService tests | | Unit tests for ViewModel browse mode | Medium | Async load command, pagination mock, filter behavior | --- ## Cross-Feature Dependencies Both features touch the same data models. Changes must be coordinated: ``` TenantProfile model — gains fields for branding (ClientLogoBase64, ClientLogoSource) AppSettings model — gains MspLogoBase64 ProfileRepository — serializes/deserializes new TenantProfile fields (JSON, backward-compat) SettingsRepository — serializes/deserializes new AppSettings field GraphClientFactory — used by both features (no changes needed) TenantSwitchedMessage — consumed by UserAccessAuditViewModel to clear directory cache ``` Neither feature requires new NuGet packages. The Graph SDK, MSAL, and System.Text.Json are already present. No new binary dependencies means no EXE size increase. --- ## MVP Recommendation Build in this order, each independently releasable: 1. **MSP logo in HTML reports** — highest visible impact, lowest complexity. AppSettings field + Settings UI upload + shared header builder. 2. **Client logo in HTML reports (import from file)** — completes the co-branding pattern. TenantProfile field + ProfileManagementDialog upload UI. 3. **User directory browse (load + select + filter)** — core browse UX. Toggle, paginated load, in-memory filter, pipe into existing audit. 4. **Auto-pull client logo from Entra branding** — differentiator, zero-config polish. Build after manual import works so the fallback path is proven. 5. **Directory: guest filter + department/jobTitle columns** — low-effort differentiators; add after core browse is stable. Defer to a later milestone: - Directory session caching across tab switches — a Refresh button is sufficient for v2.2. - Logo on CSV exports — CSV has no image support; not applicable. --- ## Sources - Graph API List Users (v1.0 official): https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0 — HIGH confidence - Graph API Get organizationalBranding (v1.0 official): https://learn.microsoft.com/en-us/graph/api/organizationalbranding-get?view=graph-rest-1.0 — HIGH confidence - Graph API bannerLogo stream: `GET /organization/{id}/branding/localizations/default/bannerLogo` — HIGH confidence (verified in official docs) - Graph pagination concepts: https://learn.microsoft.com/en-us/graph/paging — HIGH confidence - ControlMap co-branding (MSP + client logo pattern): https://help.controlmap.io/hc/en-us/articles/24174398424347 — MEDIUM confidence - ManageEngine ServiceDesk Plus MSP per-account branding: https://www.manageengine.com/products/service-desk-msp/rebrand.html — MEDIUM confidence - SolarWinds MSP report customization: http://allthings.solarwindsmsp.com/2013/06/customize-your-branding-on-client.html — MEDIUM confidence - Direct codebase inspection: HtmlExportService.cs, GraphUserSearchService.cs, AppSettings.cs, TenantProfile.cs — HIGH confidence