- Added ReportBranding? branding = null to BuildHtml on all 5 services
- Added ReportBranding? branding = null after CancellationToken ct on all WriteAsync overloads
- Injected BrandingHtmlHelper.BuildBrandingHeader(branding) between <body> and <h1> in each
- StorageHtmlExportService both overloads updated (nodes-only and nodes+fileTypeMetrics)
- HtmlExportService both overloads updated (PermissionEntry and SimplifiedPermissionEntry)
- Build passes with 0 warnings — all existing callers compile unchanged via default null
- Add ReportBranding positional record bundling MspLogo and ClientLogo
- Add BrandingHtmlHelper static class generating flex branding header HTML
- Add BrandingHtmlHelperTests covering all 4 logo states (null, both null, single, both)
- Add InternalsVisibleTo for SharepointToolbox.Tests in project file
- Add IBrandingService interface with ImportLogoAsync, Save/Clear/GetMspLogoAsync
- Add BrandingService: PNG/JPEG magic byte detection, rejects unsupported formats with
descriptive error, auto-compresses files over 512 KB using WPF PresentationCore imaging
- Add BrandingServiceTests: 9 tests covering validation, rejection, compression, CRUD
- Deviation: used WPF BitmapEncoder/TransformedBitmap instead of System.Drawing.Bitmap
(System.Drawing.Common not available without new NuGet package; WPF PresentationCore
is in the existing stack per architectural decisions)
- GraphUserDirectoryService uses PageIterator<User, UserCollectionResponse> for pagination
- Filter: accountEnabled eq true and userType eq 'Member' (no ConsistencyLevel header)
- Cancellation checked in PageIterator callback (return false stops iteration)
- Progress reported via IProgress<int> with running count per user
- MapUser extracted as internal static for direct unit test coverage
- Tests: 5 unit tests for MapUser field mapping and fallback logic
- Integration-level tests (pagination/cancellation) skipped with rationale documented
- Note: test project compilation blocked by pre-existing BrandingServiceTests.cs (10-01 artifact)
- CamlQuery with RecursiveAll scope enumerates files across all non-hidden document libraries
- Paginated 500-item batches avoid list view threshold issues
- Files grouped by extension (case-insensitive) with summed size and count
- Results returned as IReadOnlyList<FileTypeMetric> sorted by TotalSizeBytes descending
- Existing CollectStorageAsync, LoadFolderNodeAsync, CollectSubfoldersAsync unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add CollectFileTypeMetricsAsync method signature to IStorageService
- Returns IReadOnlyList<FileTypeMetric> for chart visualization data
- Existing CollectStorageAsync signature unchanged
- CS0535 expected until StorageService implements in Plan 09-02
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add RiskLevelColors helper for risk-level color coding
- Add BuildHtml(IReadOnlyList<SimplifiedPermissionEntry>) with risk summary cards, Simplified column, and color-coded Risk badges
- Add WriteAsync overload for simplified entries
- Original PermissionEntry methods unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add BuildCsv(IReadOnlyList<SimplifiedPermissionEntry>) overload with SimplifiedLabels and RiskLevel columns
- Add WriteAsync overload for simplified entries
- Original PermissionEntry methods unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
People picker ListBox used MouseBinding which fires before SelectedItem
updates, causing null CommandParameter. Replaced with SelectionChanged
event handler in code-behind.
AuditUsersAsync created TenantProfile with empty ClientId, causing
ArgumentException in SessionManager. Added currentProfile parameter
to pass the authenticated tenant's ClientId through.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- BuildHtml produces self-contained HTML with inline CSS and JS
- Stats cards: Total Accesses, Users Audited, Sites Scanned, High Privilege, External Users
- Per-user summary cards with high-privilege border highlight and guest badge
- Dual-view toggle (By User / By Site) with JS toggleView()
- Collapsible group headers per user and per site via toggleGroup()
- Sortable columns via sortTable() within each group
- Text filter via filterTable() scoping to active view
- Color-coded access type badges: Direct (blue), Group (green), Inherited (gray)
- High-privilege rows with bold text and warning icon
- External user guest badge (orange pill)
- UTF-8 without BOM encoding (matching HtmlExportService pattern)
- Scans permissions via IPermissionsService.ScanSiteAsync per site
- Filters PermissionEntry results to matching target user logins (case-insensitive contains)
- Splits semicolon-delimited users/logins/levels into per-user UserAccessEntry rows
- Classifies AccessType: Inherited (!HasUniquePermissions), Group (GrantedThrough), Direct
- Flags IsHighPrivilege (Full Control, Site Collection Administrator) and IsExternalUser (#EXT#)
- BuildCsv per-user CSV with summary section (user, totals, sites, high-privilege, date)
- WriteAsync groups entries by UserLogin, writes one file per user (audit_{email}_{date}.csv)
- WriteSingleFileAsync combines all users in one file for SaveFileDialog export
- RFC 4180 CSV escaping, UTF-8 with BOM for Excel compatibility
- SanitizeFileName strips invalid path chars from email addresses
- IUserAccessAuditService.AuditUsersAsync: scan sites and filter by user logins
- IGraphUserSearchService.SearchUsersAsync: Graph API people-picker autocomplete
- GraphUserResult record: DisplayName, UserPrincipalName, Mail
- Replace GetSitePropertiesFromSharePoint("", true) with modern
GetSitePropertiesFromSharePointByFilters using null StartIndex
- Use ctx.Clone(adminUrl) instead of creating new AuthenticationManager
for admin URL, eliminating second browser auth prompt
Resolves: UAT issue "Must specify valid information for parsing in the string"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix two pre-existing blockers found during UAT:
- ProfileManagementViewModel: add NotifyCanExecuteChanged on property changes
- SessionManager: open browser in openBrowserCallback (was no-op)
Remaining blocker: SitePickerDialog parsing error from PnP Framework.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add null-conditional on CsvReader.Context.Parser to fix CS8602 warnings
- Guard ConflictCombo_SelectionChanged against null ViewModel during XAML init
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- BulkSiteService creates Team sites via TeamSiteCollectionCreationInformation with owners/members
- BulkSiteService creates Communication sites via CommunicationSiteCollectionCreationInformation with generated URL
- Per-site error handling via BulkOperationRunner with continue-on-error semantics
- SanitizeAlias generates URL-safe aliases from site names for Communication sites
- BulkSiteServiceTests: 3 pass (interface check + model defaults + CSV parsing), 3 skip (live SP)
- Fixed pre-existing BulkMemberService.cs Group type ambiguity (MSCSC.Group vs Graph.Models.Group)
- Replace stub with full grouped HTML export (port of PS Export-DuplicatesToHTML)
- One collapsible card per DuplicateGroup with item count badge and path table
- Uses System.IO.File explicitly per WPF project pattern
- 3/3 DuplicatesHtmlExportServiceTests pass; 9/9 total export tests pass
- Rule 1: Fixed ctx.Url read-only bug — use new TenantProfile with site URL for GetOrCreateContextAsync
- Rule 3: Added missing System.IO using to SearchCsvExportService and SearchHtmlExportService
- File mode: Search API KQL pagination matching SearchService pattern
- Folder mode: CAML FSObjType=1 via SharePointPaginationHelper.GetAllItemsAsync
- MakeKey composite key (name+size+dates+counts) matches DuplicatesServiceTests scaffold
- Groups only items with count >= 2, ordered by group size then name
- ExtractLibraryFromPath derives library name from path relative to site URL
- SelectProperties added per-item (StringCollection has no AddRange)
- KQL builder for extension, date, creator, editor, library filters
- Pagination via StartRow += 500, stops at MaxStartRow or MaxResults
- Filters _vti_history/ version history paths from results
- Client-side Regex filter on file name and title
- ValidateKqlLength enforces 4096-char SharePoint limit
- SelectProperties added one-by-one (StringCollection has no AddRange)
- Replace string.Empty stub with full BuildHtml implementation
- Self-contained HTML with inline CSS and JS — no external dependencies
- toggle(i) JS function with collapsible subfolder rows (sf-{i} IDs)
- _togIdx counter reset at start of each BuildHtml call (per PS pattern)
- RenderNode/RenderChildNode for recursive tree rendering
- FormatSize helper: B/KB/MB/GB adaptive display
- HtmlEncode via System.Net.WebUtility
- Add explicit System.IO using (required in WPF project)
- Add StorageService implementing IStorageService
- Load Folder.StorageMetrics, TimeLastModified, Name, ServerRelativeUrl in one CSOM round-trip per folder
- CollectStorageAsync returns one StorageNode per document library at IndentLevel=0
- With FolderDepth>0, CollectSubfoldersAsync recurses into child folders
- All CSOM calls use ExecuteQueryRetryHelper.ExecuteQueryRetryAsync (3 call sites)
- System/hidden lists skipped (Hidden=true or BaseType != DocumentLibrary)
- Forms/ and _-prefixed system folders skipped during subfolder recursion
- ct.ThrowIfCancellationRequested() called at top of every recursive step
- Stats cards: Total Entries, Unique Permission Sets, Distinct Users/Groups
- Type badges: site-coll (blue), site (green), list (amber), folder (gray)
- Unique/Inherited badges based on HasUniquePermissions flag
- User pills with external-user CSS class for #EXT# logins
- Inline JS filterTable() function for client-side row filtering
- WriteAsync uses UTF-8 without BOM for HTML
- All 3 HtmlExportServiceTests pass
[Rule 3 - Blocking] CsvExportService/HtmlExportService stubs added so export test
files compile. [Rule 1 - Bug] PermissionsService: removed Principal.Email (not on
Principal, only on User) and changed folder param from Folder to ListItem (SecurableObject).
- SiteInfo record added to Core/Models
- ISiteListService interface with GetSitesAsync signature
- SiteListService derives admin URL via Regex, connects via SessionManager
- Filters to Active sites only, excludes OneDrive personal (-my.sharepoint.com)
- Access denied ServerException wrapped as InvalidOperationException with actionable message
- DeriveAdminUrl marked internal static for unit testability
- InternalsVisibleTo added to AssemblyInfo.cs to expose internal to test project
- 2 DeriveAdminUrl tests pass; full suite: 53 pass, 4 skip, 0 fail
- SessionManager owns all ClientContexts; callers must not store references
- IsAuthenticated(tenantUrl) returns false before auth, true after GetOrCreateContextAsync
- ClearSessionAsync disposes ClientContext and removes state (idempotent for unknown tenants)
- GetOrCreateContextAsync validates null/empty TenantUrl and ClientId (ArgumentException)
- MsalClientFactory.GetCacheHelper() added — exposes helper for PnP tokenCacheCallback wiring
- 8 unit tests pass, 1 interactive-login test skipped (integration-only)