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

14 KiB

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