# 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 `<img data-URI>` 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<User, UserCollectionResponse>` | 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 `<img>` 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<User, UserCollectionResponse>` 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<int>` 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 `<img>` 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 `<style>` block.
- v2.2-8: Logo cleared on profile overwrite — verify `ClientLogoBase64` and `ClientLogoMimeType` survive the profile save/reload cycle before shipping.
- v2.2-9: `DirectoryPageTokenNotFoundException` on Graph page iteration retry — use `PageIterator` (which handles retry token correctly) rather than a manual `@odata.nextLink` loop.

---

## Implications for Roadmap

### Suggested Phase Structure

The two features are architecturally independent. A single developer should follow phases A-G in
order. Two developers can run branding (A→D→F) and directory browse (A→B→E→G) in parallel after
Phase A completes.

**Phase 1 — Data foundation (models + repositories + services)**
Rationale: Unblocks both features simultaneously. Establishes the logo storage strategy (base64-in-
JSON) before any export service or ViewModel is written. Establishing this here prevents a full
re-architecture later if file-path storage is chosen first.
Delivers: Phases A + B from the build order above.
Key decision to make before starting: confirm base64-in-JSON as the storage strategy (not file
paths). Document the decision explicitly.
Pitfalls to avoid: v2.2-6 (file path portability). The wrong storage decision here propagates to
all downstream phases.

**Phase 2 — HTML export service extensions + branding ViewModel integration**
Rationale: Modifying the 5 export services with an optional parameter is low-risk and unblocks all
ViewModel callers. The 4 export ViewModel changes are an identical inject-and-call pattern — batch
them. The SettingsViewModel and ProfileManagementViewModel changes complete the logo management UX.
Delivers: Phases C + D from the build order above. All HTML reports support optional logo headers.
MSP logo manageable from Settings. Client logo manageable from ProfileManagementDialog.
Pitfalls to avoid: v2.2-1 (size limit at import), v2.2-5 (file format validation), v2.2-7 (print
layout CSS). All three must be implemented in this phase, not deferred.

**Phase 3 — Branding UI views**
Rationale: Views built after ViewModel behavior is unit-tested. Requires the base64→BitmapSource
converter, written once and reused in both views.
Delivers: Phase F from the build order. Settings branding section + ProfileManagementDialog logo
fields, both with live preview.

**Phase 4 — User directory browse ViewModel**
Rationale: `UserAccessAuditViewModel` is the highest-risk change. New async command with progress,
cancellation, and tenant-switch reset. Implement and unit-test before touching the View.
Delivers: Phase E from the build order. Full browse mode behavior is testable via unit tests before
any XAML is written.
Pitfalls to avoid: v2.2-2 (pagination), v2.2-3 (default filter), v2.2-4 (progress feedback),
v2.2-9 (PageIterator vs. manual nextLink loop).

**Phase 5 — Directory browse UI view**
Rationale: Left panel restructure in UserAccessAuditView.xaml is the highest-risk XAML change.
Done last, after all ViewModel behavior is proven by tests.
Delivers: Phase G from the build order. Complete browse mode UX.

**Phase 6 — Differentiators (after core features proven)**
Rationale: Auto-pull Entra branding, directory guest filter toggle, department/jobTitle columns,
session-scoped directory cache. These are enhancements, not blockers for the milestone.
Delivers: Zero-config client logo path, richer directory filtering, faster repeat access.
Pitfalls to avoid: Auto-pull Entra logo must handle empty-body response gracefully (not all tenants
have branding configured). Fall back silently to no logo rather than showing an error.

### Research Flags

Phases 1-5 are standard patterns verified by direct codebase inspection. No additional research
needed. The architecture file provides exact file locations, class signatures, and data flows.

Phase 6 (auto-pull Entra branding): MEDIUM confidence. Test the `bannerLogo` stream endpoint
against a real tenant with and without branding configured before committing to the implementation.
The Graph API documentation states the response is an empty stream (not a 404) when no logo is set
— verify this behavior live before building the error handling path around it.

---

## Open Questions for Product Decisions

These are not technical blockers but should be resolved before the phase that implements them:

1. **SVG logo support: anti-feature or bring-your-own-library feature?**
   Current recommendation: reject SVG (XSS risk in data-URIs, requires SharpVectors for rasterization). If SVG support is needed, SharpVectors adds a dependency. Decide before Phase 2.

2. **Client logo source priority when both auto-pull and manual import are configured?**
   Recommendation: manual import wins; auto-pull is the fallback when no manual logo is set.
   Implement as `ClientLogoSource` enum: `None | Imported | AutoPulled`. Decide before Phase 6.

3. **Session-scoped directory cache: ViewModel lifetime or shared service?**
   ViewModel-scoped = cache lost on tab navigation (ViewModel is transient). Service-scoped = cache
   survives tab switches. Recommendation: start with no cache (Refresh button), add service-level
   caching in Phase 6 only if user feedback indicates it is needed. Defers scope decision.

4. **Report header layout: logos side-by-side or MSP left + client right?**
   Visual design decision only; does not affect services or ViewModels. Current spec uses
   `display: flex; gap: 16px` (left-to-right). Can be changed at any time.

5. **"Load Directory" button placement: inside browse panel or tab-level toolbar?**
   Recommendation: inside the browse panel, visible only in Browse mode. Avoids confusion when in
   Search mode. Does not affect architecture.

---

## Confidence Assessment (v2.2)

| Area | Confidence | Basis |
|---|---|---|
| Stack (no new packages needed) | HIGH | Direct codebase inspection + BCL and Graph SDK documentation |
| Feature scope (table stakes vs. differentiators) | HIGH | Official Graph API docs + direct codebase inspection + MSP tool competitive research |
| Architecture (integration points, build order) | HIGH | Direct inspection of all affected files; exact class names and property signatures verified |
| Branding pitfalls (base64, file validation, portability) | HIGH | BCL behavior verified; file path portability pitfall is a well-known pattern |
| Graph pagination pitfalls | HIGH | Microsoft Learn PageIterator docs (updated 2025-08-06); DirectoryPageTokenNotFoundException documented |
| Directory filter behavior (accountEnabled, userType) | MEDIUM-HIGH | Graph docs confirm filter syntax; recommend verifying against a real tenant before shipping |
| Auto-pull Entra banner logo (Phase 6) | MEDIUM | API documented but empty-body behavior (no logo configured) needs live tenant verification |
| Print CSS behavior for logo header | MEDIUM | MDN/W3C verified; browser rendering varies; requires cross-browser manual test |

**Overall confidence:** HIGH for Phases 1-5. MEDIUM for Phase 6 (Entra auto-pull live behavior).

**Gaps to address during planning:**
- Confirm `$filter=accountEnabled eq true and userType eq 'Member'` works without `ConsistencyLevel: eventual` on the v1.0 `/users` endpoint. If eventual consistency is required, the `GraphUserDirectoryService` adds the `ConsistencyLevel` header and `$count=true` to this call path.
- Verify the Entra `bannerLogo` stream endpoint returns an empty response body (not HTTP 404) when tenant branding is not configured. This determines the error handling branch in the auto-pull code path.

---

## Sources (v2.2)

| Source | Confidence | Used In |
|---|---|---|
| Microsoft Learn — List users (Graph v1.0), updated 2025-07-23 | HIGH | STACK, FEATURES, PITFALLS |
| Microsoft Learn — Page through a collection (Graph SDKs), updated 2025-08-06 | HIGH | STACK, PITFALLS |
| Microsoft Learn — Get organizationalBranding (Graph v1.0), updated 2025-11-08 | HIGH | STACK, FEATURES |
| .NET BCL docs — Convert.ToBase64String, File.ReadAllBytesAsync | HIGH | STACK |
| Microsoft Learn — Graph throttling guidance | HIGH | PITFALLS |
| Direct codebase inspection (GraphClientFactory, HtmlExportService, TenantProfile, AppSettings, UserAccessAuditViewModel, SettingsViewModel, UserAccessAuditView.xaml, App.xaml.cs) | HIGH | ARCHITECTURE, STACK |
| Existing codebase CONCERNS.md audit (2026-04-02) | HIGH | PITFALLS |

---
---

# v1.0 Research Summary (Original — Preserved for Reference)

**Researched:** 2026-04-02
**Confidence:** HIGH

## Executive Summary

This project is a full rewrite of a PowerShell-based SharePoint Online administration toolbox into a standalone C#/WPF desktop application targeting MSP administrators who manage 10–30 client tenants simultaneously. The research confirms that the correct technical path is .NET 10 LTS with WPF, PnP.Framework (not PnP.Core SDK) as the SharePoint library, and CommunityToolkit.Mvvm for the MVVM layer. The key architectural constraint is that multi-tenant session caching — holding MSAL token caches per tenant with `MsalCacheHelper` — must be the very first infrastructure component built, because every single feature gates on it. The recommended architecture is a strict four-layer MVVM pattern (View → ViewModel → Service → Infrastructure) with no WPF types below the ViewModel layer, constructor-injected interfaces throughout, and `AsyncRelayCommand` for every SharePoint operation.

The feature scope is well-defined: parity with the existing PowerShell tool is the v1 MVP (permissions reports, storage metrics, file search, bulk operations, site templates, duplicate detection, error reporting, EN/FR localization). Three new features are justified for a v1.x release once core parity is validated — user access export across sites, simplified plain-language permissions view, and storage charts by file type. These represent genuine competitive differentiation against SaaS tools like ShareGate and ManageEngine, which are cloud-based, subscription-priced, and do not offer local offline operation or MSP-grade multi-tenant context switching.

The most dangerous risk is not technical complexity but porting discipline: the existing codebase has 38 silent catch blocks and no async discipline. The single highest-priority constraint for the entire project is that async patterns (`AsyncRelayCommand`, `IProgress<T>`, `CancellationToken`, `ExecuteQueryRetryAsync`) must be established in the foundation phase and enforced through code review before any feature work begins. Retrofitting these patterns after-the-fact is among the most expensive refactors possible in a WPF codebase. Similarly, the write-then-replace JSON persistence pattern and SharePoint pagination helpers must be built once in the foundation and reused everywhere — building these per-feature guarantees divergence and bugs.

## Key Findings

### Recommended Stack

The stack is fully resolved with high confidence. All package versions are confirmed on NuGet as of 2026-04-02. The runtime is .NET 10 LTS (EOL November 2028); .NET 8 was explicitly rejected because it reaches EOL in November 2026 — too soon for a new project. PnP.Framework 1.18.0 is the correct SharePoint library choice because this is a CSOM-heavy migration from PnP.PowerShell patterns and the PnP Provisioning Engine (required for site templates) lives only in PnP.Framework, not in PnP.Core SDK. Do not use `PublishTrimmed=true` — PnP.Framework and MSAL use reflection and are not trim-safe; the self-contained EXE will be approximately 150–200 MB, which is acceptable per project constraints.

**Core technologies:**
- **.NET 10 LTS + WPF**: Windows-only per constraint; richer MVVM binding than WinForms (the existing framework)
- **PnP.Framework 1.18.0**: CSOM operations, PnP Provisioning Engine, site templates — the direct C# equivalent of PnP.PowerShell
- **Microsoft.Graph 5.103.0**: Teams, groups, user enumeration across tenants — Graph-native operations only
- **MSAL.NET 4.83.1 + Extensions.Msal 4.83.3 + Desktop 4.82.1**: Multi-tenant token cache per tenant, Windows broker (WAM) support
- **CommunityToolkit.Mvvm 8.4.2**: Source-generated `[ObservableProperty]`, `[RelayCommand]`, `AsyncRelayCommand` — eliminates MVVM boilerplate
- **Microsoft.Extensions.Hosting 10.x**: DI container (`IServiceCollection`), app lifetime, `IConfiguration`
- **Serilog 4.3.1 + file sink**: Structured logging to rolling files in `%AppData%\SharepointToolbox\logs\` — essential for diagnosing the silent failures in the existing app
- **ScottPlot.WPF 5.1.57**: Pie and bar charts for storage metrics — stable MIT-licensed library (LiveCharts2 WPF is still RC as of April 2026)
- **System.Text.Json (built-in)**: JSON profiles, settings, templates — no Newtonsoft.Json dependency
- **CsvHelper**: CSV export — replaces manual string concatenation
- **.resx localization**: EN/FR compile-time-safe resource files

### Expected Features

The feature scope is well-researched. Competitive analysis against ShareGate, ManageEngine SharePoint Manager Plus, and AdminDroid confirms that local offline operation, instant multi-tenant switching, plain-language permissions, and folder structure provisioning are genuine differentiators that no competitor SaaS tool offers.

**Must have (table stakes — v1 parity):**
- Tenant profile registry + multi-tenant session caching — everything gates on this
- Permissions report (site-level) with CSV + HTML export
- Storage metrics per site
- File search across sites
- Bulk operations (member add, site creation, transfer) with progress and cancellation
- Site template management + folder structure provisioning
- Duplicate file detection
- Error reporting (replace 38 silent catch blocks with visible failures)
- Localization (EN/FR) — existing users depend on this

**Should have (competitive differentiators — v1.x):**
- User access export across selected sites — "everything User X can access across 15 sites" — no native M365 equivalent
- Simplified permissions view (plain language) — "can edit files" instead of "Contribute"
- Storage graph by file type (pie + bar toggle) via ScottPlot.WPF

**Defer (v2+):**
- Scheduled scan runs via Windows Task Scheduler (requires stable CLI/headless mode first)
- Permission comparison/diff between two time points (requires snapshot storage)
- XLSX export (CSV opens in Excel adequately for v1)

**Anti-features to reject outright:** real-time permission change alerts (requires persistent Azure service), automated remediation (liability risk), cloud sync, AI governance recommendations (Microsoft's own roadmap).

### Architecture Approach

The recommended architecture is a strict four-layer MVVM pattern hosted in `Microsoft.Extensions.Hosting`. The application is organized as: Views (XAML only, zero code-behind) → ViewModels (CommunityToolkit.Mvvm, one per feature tab) → Services (domain logic, stateless, constructor-injected via interfaces) → Infrastructure (PnP.Framework, Microsoft.Graph, local JSON files). Cross-ViewModel communication uses `WeakReferenceMessenger` (e.g., tenant-switched event resets all feature VM state). A singleton `SessionManager` is the only class that holds `ClientContext` objects — services request a context per operation and never store it. The `Core/` folder contains pure C# models and interfaces with no WPF references, making all services independently testable.

**Major components:**
1. **AuthService / SessionManager** — multi-tenant MSAL token cache, `TenantSession` per tenant, active profile state; singleton; every feature gates on this
2. **Feature Services (6)** — PermissionsService, StorageService, SearchService, TemplateService, DuplicateService, BulkOpsService — stateless, cancellable, progress-reporting; registered as transient
3. **ReportExportService + CsvExportService** — self-contained HTML reports (embedded JS/CSS) and CSV generation; called after operation completes
4. **SettingsService** — JSON profiles, templates, settings with write-then-replace pattern and `SemaphoreSlim` concurrency guard; singleton
5. **MainWindowViewModel** — shell navigation, tenant selector, log panel; delegates all feature logic to feature ViewModels via DI
6. **Feature ViewModels (7)** — one per tab (Permissions, Storage, Search, Templates, Duplicates, BulkOps, Settings); own `CancellationTokenSource` and `ObservableCollection<T>` per operation

### Critical Pitfalls

10 pitfalls were identified. All 10 are addressed in Phase 1 (Foundation) — none can be deferred to feature phases.

1. **Sync calls on the UI thread** — Never use `.Result`/`.Wait()` on the UI thread; every PnP call must use `await` with the async overload or `Task.Run`; use `AsyncRelayCommand` for all commands. Establish this pattern before any feature work begins or retrofitting costs will be severe.

2. **Porting silent error suppression** — The existing app has 38 empty catch blocks. Every `catch` in the C# rewrite must do one of three things: log-and-recover, log-and-rethrow, or log-and-surface to the user. Treat empty catch as a build defect from day one.

3. **SharePoint 5,000-item list view threshold** — All CSOM list enumeration must use `CamlQuery` with `RowLimit` ≤ 2,000 and `ListItemCollectionPosition` pagination. Build a shared pagination helper in Phase 1 and mandate its use in every feature that enumerates list items.

4. **Multi-tenant token cache race conditions** — Use `MsalCacheHelper` (Microsoft.Identity.Client.Extensions.Msal) for file-based per-tenant token cache serialization. Scope `IPublicClientApplication` per ClientId, not per tenant URL. Provide a "Clear cached sessions" UI action.

5. **JSON settings file corruption on concurrent writes** — Use write-then-replace (`filename.tmp` → validate → `File.Move`) plus `SemaphoreSlim(1)` per file. Implement before any feature persists data. Known bug in the existing app per CONCERNS.md.

6. **WPF `ObservableCollection` updates from background threads** — Collect results into `List<T>` on background thread, then assign `new ObservableCollection<T>(list)` atomically via `Dispatcher.InvokeAsync`. Use `IProgress<T>` for streaming. Never modify `ObservableCollection` from `Task.Run`.

7. **`async void` command handlers** — Use `AsyncRelayCommand` exclusively for async operations. `async void` swallows exceptions post-`await`. Wire `Application.DispatcherUnhandledException` and `TaskScheduler.UnobservedTaskException` as last-resort handlers.

8. **API throttling (429/503)** — Always use `ExecuteQueryRetryAsync` (never `ExecuteQuery`). For Graph SDK, the default retry handler respects `Retry-After` automatically. Surface retry events to the user as progress messages.

9. **`ClientContext` resource disposal gaps** — Always obtain `ClientContext` inside `using` or `await using`. Verify `Dispose()` is called on cancellation via unit tests.

10. **WPF trimming breaks self-contained EXE** — Never set `PublishTrimmed=true`. Accept the ~150–200 MB EXE size. Use `PublishReadyToRun=true` for startup performance instead.

## Implications for Roadmap

Based on the combined research, the dependency graph from ARCHITECTURE.md and FEATURES.md, and the pitfall-to-phase mapping from PITFALLS.md, the following phase structure is strongly recommended:

### Phase 1: Foundation and Infrastructure
**Rationale:** All 10 critical pitfalls must be resolved before feature work begins. The dependency graph in FEATURES.md shows that every feature requires the tenant profile registry and session caching layer. Establishing async patterns, error handling, DI container, logging, and JSON persistence now prevents the most expensive retrofits.
**Delivers:** Runnable WPF shell with tenant selector, multi-tenant session caching (MSAL + MsalCacheHelper), DI container wiring, Serilog logging, SettingsService with write-then-replace persistence, ResX localization scaffolding, shared pagination helper, shared `AsyncRelayCommand` pattern, global exception handlers.
**Research flag:** Standard patterns — no additional research needed.

### Phase 2: Permissions and Audit Core
**Rationale:** Permissions reporting is the highest-value daily-use feature and validates the auth layer and pagination helper under real conditions.
**Delivers:** Site-level permissions report with recursive scan, CSV export, self-contained HTML export, progress feedback, error surface for failed scans.
**Research flag:** Standard PnP Framework patterns — HIGH confidence.

### Phase 3: Storage Metrics and File Operations
**Rationale:** Storage metrics and file search reuse the auth session and export infrastructure from Phases 1–2. Duplicate detection depends on file enumeration built here.
**Delivers:** Storage metrics, file search (KQL), duplicate detection, storage data export.
**Research flag:** Duplicate detection at scale under Graph throttling may need targeted research.

### Phase 4: Bulk Operations and Provisioning
**Rationale:** Highest-complexity features (bulk writes to client tenants) benefit from stable async/cancel/progress infrastructure from Phase 1.
**Delivers:** Bulk member add/remove, bulk site creation, ownership transfer, site template capture and apply, folder structure provisioning.
**Research flag:** PnP Provisioning Engine for Teams-connected sites — edge cases need validation.

### Phase 5: New Differentiating Features (v1.x)
**Rationale:** New capabilities (not existing-tool parity) that depend on stable v1 infrastructure. Group here to avoid blocking the v1 release.
**Delivers:** User access export across sites, simplified plain-language permissions view, storage graph by file type.
**Research flag:** User access export — Graph API approach for enumerating all permissions for user X across N sites needs targeted research.

### Phase 6: Distribution and Hardening
**Rationale:** Packaging, end-to-end validation on clean machines, FR locale completeness, "looks done but isn't" checklist.
**Delivers:** Single self-contained EXE, validated on a machine with no .NET runtime, all checklist items verified.
**Research flag:** Standard `dotnet publish` configuration — no additional research needed.

## Confidence Assessment

| Area | Confidence | Notes |
|---|---|---|
| Stack | HIGH | All package versions verified on NuGet; .NET lifecycle dates confirmed; PnP.Framework vs PnP.Core SDK choice verified |
| Features | MEDIUM | Microsoft docs HIGH; competitor feature analysis from marketing pages MEDIUM; no direct API testing |
| Architecture | HIGH | MVVM patterns from Microsoft Learn; PnP Framework auth patterns from official PnP docs; MsalCacheHelper from official MSAL.NET docs |
| Pitfalls | HIGH | Critical pitfalls verified via official docs, PnP GitHub issues, and direct audit of existing codebase (CONCERNS.md) |

**Overall confidence:** HIGH

**Gaps to address:**
- PnP Provisioning Engine for Teams-connected sites: behavior not fully documented; validate during Phase 4 planning.
- User cross-site access enumeration via Graph API: multiple possible approaches with different throttling profiles; validate during Phase 5 planning.
- Graph API volume for duplicate detection at large scale: practical concurrency limits need validation.
- ScottPlot.WPF XAML integration: WpfPlot binding patterns less documented than WinForms equivalent; validate during Phase 5 planning.

## Sources

### Primary (HIGH confidence)
- Microsoft Learn: MSAL token cache serialization — https://learn.microsoft.com/en-us/entra/msal/dotnet/how-to/token-cache-serialization
- Microsoft Learn: Single-file publishing overview — https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview
- Microsoft Learn: AsyncRelayCommand (CommunityToolkit.Mvvm) — https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/asyncrelaycommand
- Microsoft Learn: SharePoint Online list view threshold — https://learn.microsoft.com/en-us/troubleshoot/sharepoint/lists-and-libraries/items-exceeds-list-view-threshold
- Microsoft Learn: Graph SDK paging — https://learn.microsoft.com/en-us/graph/sdks/paging
- Microsoft Learn: Graph throttling guidance — https://learn.microsoft.com/en-us/graph/throttling
- PnP Framework GitHub: https://github.com/pnp/pnpframework
- PnP Framework vs Core authoritative comparison: https://github.com/pnp/pnpframework/issues/620
- dotnet/wpf trimming issues: https://github.com/dotnet/wpf/issues/4216, /6096
- Existing codebase CONCERNS.md audit (2026-04-02)

### Secondary (MEDIUM confidence)
- ShareGate SharePoint audit tool feature page — https://sharegate.com/sharepoint-audit-tool
- ManageEngine SharePoint Manager Plus — https://www.manageengine.com/sharepoint-management-reporting/sharepoint-permission-auditing-tool.html
- AdminDroid SharePoint Online auditing — https://admindroid.com/microsoft-365-sharepoint-online-auditing

---

## v2.3 Tenant Management & Report Enhancements

**Researched:** 2026-04-09
**Confidence:** HIGH

### Stack Additions

**None.** All five features are delivered using existing dependencies:
- `Microsoft.Graph` 5.74.0 — app registration, service principal, admin consent, group member resolution
- `PnP.Framework` 1.18.0 — `Tenant.SetSiteAdmin` for auto-ownership
- BCL .NET 10 — LINQ consolidation, HTML5 `<details>/<summary>`

Do NOT add `Azure.Identity` — conflicts with existing MSAL PCA + MsalCacheHelper pattern.

### Feature Table Stakes

| Feature | Table Stakes | Differentiators |
|---------|-------------|-----------------|
| App Registration | Create app + SP + grant roles; guided fallback mandatory | Auto-detect admin permissions, single-click register |
| App Removal | Delete app + SP, revoke consent | Clear MSAL cache for removed app |
| Auto-Ownership | `Tenant.SetSiteAdmin` on access denied; global toggle OFF by default | Persistent cleanup list, startup warning for pending removals |
| Group Expansion | Resolve members at scan time; HTML5 details/summary | `transitiveMembers` for nested groups; pagination for large groups |
| Report Consolidation | Toggle per-export; merge same-user same-access rows | New `ConsolidatedUserAccessEntry` type (never modify existing) |

### Critical Pitfalls

1. **App registration requires `Application.ReadWrite.All` + `AppRoleAssignment.ReadWrite.All`** — MSP app likely doesn't have these consented. Guided fallback is first-class, not a degraded mode.
2. **`POST /applications` does NOT create service principal** — Must be 3-step atomic: create app → create SP → grant roles, with rollback on failure.
3. **Auto-ownership cleanup** — `try/finally` insufficient for hard termination. Need persistent JSON cleanup-pending list + startup warning.
4. **`$expand=members` caps at ~20 silently** — Must use `GET /groups/{id}/transitiveMembers?$top=999` with pagination.
5. **Consolidation is a schema change** — Must be off by default, opt-in per export.

### Suggested Build Order (5 phases, starting at 15)

1. **Phase 15** — Model extensions + PermissionConsolidator (zero API calls, data shapes)
2. **Phase 16** — Report consolidation toggle (first user-visible, pure LINQ)
3. **Phase 17** — Group expansion in HTML reports (Graph at export time, HTML5 details/summary)
4. **Phase 18** — Auto-take ownership (PnP Tenant.SetSiteAdmin, retry once, default OFF)
5. **Phase 19** — App registration + removal (highest blast radius, Entra changes, guided fallback default)

### Research Flags

- **Phase 19:** Admin consent grant appRole GUIDs need validation against real tenant
- **Phase 17:** Confirm `GroupMember.Read.All` scope availability on MSP app registration

---
*v1.0 research completed: 2026-04-02*
*v2.2 research synthesized: 2026-04-08*
*v2.3 research synthesized: 2026-04-09*
*Ready for roadmap: yes*
