Files
Sharepoint-Toolbox/.planning/research/FEATURES.md
2026-04-08 10:57:27 +02:00

212 lines
14 KiB
Markdown

# 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 `<img src=` |
| Logo graceful absence (no logo configured = no broken image) | Admins will run the tool before configuring logos | Trivial | Conditional render — omit the `<img>` 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 `<body>` 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<GraphUserResult> 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