- Remove bin/obj/publish from git tracking
- Update .gitignore for .NET project (source only)
- Add release.ps1 local publish script (replaces Gitea workflow)
- Remove .gitea/workflows/release.yml
- Fix duplicate group names to show library names
- Fix HTML export to show Name column in duplicates report
- Fix consolidated permissions HTML to show folder/library names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove bin/obj/publish from git tracking
- Update .gitignore for .NET project (source only)
- Add release.ps1 local publish script (replaces Gitea workflow)
- Remove .gitea/workflows/release.yml
- Fix duplicate group names to show library names
- Fix HTML export to show Name column in duplicates report
- Fix consolidated permissions HTML to show folder/library names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ProfileManagementDialog.xaml: height 750, new Row 4 with Register/Remove buttons
- BooleanToVisibilityConverter added to Window.Resources
- Fallback instructions panel bound to ShowFallbackInstructions
- RegistrationStatus text block with StringToVisibilityConverter
- Buttons row shifted to Row 5
- ProfileManagementViewModelRegistrationTests: 7 unit tests, all passing
- ProfileManagementViewModelLogoTests: updated to 5-param constructor
- Add _settingsService and _ownershipService fields to PermissionsViewModel
- Add SettingsService? and IOwnershipElevationService? to both constructors
- Add DeriveAdminUrl internal static helper for admin URL derivation
- Add IsAccessDenied helper catching ServerUnauthorizedAccessException + WebException 403
- Add IsAutoTakeOwnershipEnabled async helper reading toggle from SettingsService
- Refactor RunOperationAsync with try/catch elevation pattern (read toggle before loop)
- Tag elevated entries with WasAutoElevated=true via record with expression
- Add PermissionsViewModelOwnershipTests (8 tests): toggle OFF propagates, toggle ON elevates+retries, no elevation on success, WasAutoElevated tagging, elevation throw propagates, DeriveAdminUrl theory
- Add _groupResolver field (ISharePointGroupResolver?) with constructor injection
- ISharePointGroupResolver added as optional last parameter in main constructor
- ExportHtmlAsync resolves SharePoint group names before calling WriteAsync
- Gracefully handles resolution failure with LogWarning, exports without expansion
- Both WriteAsync call sites pass groupMembers dict (standard and simplified paths)
- Add optional groupMembers parameter to both BuildHtml overloads and WriteAsync methods
- SharePoint group pills render as expandable with onclick toggleGroup when groupMembers provided
- Hidden member sub-rows injected after parent row with resolved member names
- Empty member list renders 'members unavailable' fallback label
- toggleGroup JS function added to inline script block in both overloads
- filterTable updated to skip data-group sub-rows
- CSS for .group-expandable added to both overloads
- Backward compatibility: null groupMembers produces identical output to pre-Phase 17
- ResolvedMember record in Core/Models with DisplayName and Login
- ISharePointGroupResolver interface with ResolveGroupsAsync contract
- SharePointGroupResolver: CSOM group user loading + Graph transitive AAD resolution
- Internal static helpers IsAadGroup, ExtractAadGroupId, StripClaims (all green unit tests)
- Graceful error handling: exceptions return empty list per group, never throw
- OrdinalIgnoreCase result dict; lazy Graph client creation on first AAD group
- Add mergePermissions parameter to BuildHtml and WriteAsync
- Early-return branch calls PermissionConsolidator.Consolidate and delegates to BuildConsolidatedHtml
- BuildConsolidatedHtml: by-user table with Sites column, expandable [N sites] badge with toggleGroup, hidden sub-rows (data-group=locN), inline title for single-location entries
- By-site view and btn-site omitted when mergePermissions=true
- Wire UserAccessAuditViewModel.ExportHtmlAsync to pass MergePermissions
- Fix existing branding test call site to use named parameter
- Added mergePermissions=false optional parameter to WriteSingleFileAsync
- Added early-return consolidated branch using PermissionConsolidator.Consolidate
- Consolidated CSV uses distinct header with Locations and LocationCount columns
- Locations column is semicolon-separated site titles for multi-location rows
- Existing non-consolidated code path is completely unchanged
- UserAccessAuditViewModel.ExportCsvAsync now passes MergePermissions to service
- RPT-03-f: mergePermissions=false produces byte-identical output to default call
- RPT-03-g: mergePermissions=true writes consolidated header and merged rows
- Edge case: single-location entry has LocationCount=1 with no semicolons in Locations
- Added Export Options GroupBox after Scan Options in UserAccessAuditView.xaml
- Added Export Options GroupBox after Display Options in PermissionsView.xaml
- Both checkboxes bind to MergePermissions with localized labels via TranslationSource
- Add 15-01-SUMMARY.md with task commits, decisions, and next phase readiness
- Update STATE.md with decisions and session position
- Update ROADMAP.md phase 15 progress (1/2 plans complete)
- Mark requirement RPT-04 complete in REQUIREMENTS.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MakeKey builds pipe-delimited case-insensitive key from UserLogin+PermissionLevel+AccessType+GrantedThrough
- Consolidate groups UserAccessEntry list by key, merges into ConsolidatedPermissionEntry rows
- Empty input short-circuits to Array.Empty
- Output ordered by UserLogin then PermissionLevel for deterministic results
Two plans for Phase 15: models + consolidator service (wave 1), unit tests + build verification (wave 2).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Research covers all five v2.3 features: automated app registration, app removal,
auto-take ownership, group expansion in HTML reports, and report consolidation toggle.
No new NuGet packages required. Build order and phase implications documented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 phases (10-14), 14 plans, 11/11 requirements complete.
Key features: HTML report branding with MSP/client logos, user directory
browse mode with paginated load and member/guest filtering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Mode toggle (Search/Browse) RadioButtons at top of left panel
- Search panel uses DataTrigger inverse visibility (collapses when IsBrowseMode=true)
- Browse panel with Load/Cancel buttons, IncludeGuests checkbox, filter TextBox, status/count
- Directory DataGrid with 5 columns (Name, Email, Department, Job Title, Type)
- Guest users highlighted in orange via DataTrigger on UserType
- SelectedUsers extracted to shared section visible in both modes
- DataGrid wired to DirectoryDataGrid_MouseDoubleClick handler
- Scan Options and Run/Export buttons remain always visible
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extracts GraphDirectoryUser from DataGrid.SelectedItem on double-click
- Invokes SelectDirectoryUserCommand to add user to audit pipeline
- Using added for SharepointToolbox.Core.Models
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- RelayCommand<GraphDirectoryUser> converts to GraphUserResult and adds to SelectedUsers
- Duplicate UPN check prevents adding same user twice
- Initialized in both DI and test constructors
- 4 new tests pass (add, skip duplicate, null, auditable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test 17: adds user to SelectedUsers
- Test 18: skips duplicates
- Test 19: null does nothing
- Test 20: user is auditable after selection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Inject IGraphUserDirectoryService into UserAccessAuditViewModel (both constructors)
- Add IsBrowseMode toggle, DirectoryUsers collection, DirectoryUsersView with sort/filter
- Add LoadDirectoryCommand with progress reporting, cancellation, and error handling
- Add IncludeGuests toggle for in-memory member/guest filtering (no new Graph request)
- Add DirectoryFilterText for DisplayName/UPN/Department/JobTitle text search
- Add DirectoryUserCount computed property reflecting filtered view count
- Update OnTenantSwitched to clear all directory state
- Add 16 comprehensive unit tests covering all directory browse behaviors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add string? UserType as last positional parameter to GraphDirectoryUser record
- Add bool includeGuests = false parameter to IGraphUserDirectoryService.GetUsersAsync
- Branch Graph filter: members-only (default) vs all users when includeGuests=true
- Add userType to Graph Select array for MapUser population
- Update MapUser to include UserType from Graph User object
- Add MapUser_PopulatesUserType and MapUser_NullUserType tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Increase dialog height from 480 to 620 to accommodate logo section
- Add new Row 3 with logo preview, Import/Clear/Pull from Entra buttons
- Image bound to ClientLogoPreview via Base64ToImageConverter
- Placeholder text shown when no logo configured via DataTrigger
- ValidationMessage displays feedback below logo buttons
- All logo buttons auto-disable when no profile selected
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Separator and MSP Logo label after data folder section
- Add Border with Grid containing Image preview and placeholder TextBlock
- Image bound to MspLogoPreview via Base64ToImageConverter with max 80x240
- DataTrigger toggles placeholder visibility when logo is null
- Import/Clear buttons bound to BrowseMspLogoCommand/ClearMspLogoCommand
- StatusMessage TextBlock in red, visible only when set
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Base64ToImageSourceConverter converts data URI strings to BitmapImage with null-safe error handling
- Registered converter in App.xaml as Base64ToImageConverter global resource
- Added 9 localization keys (EN+FR) for logo UI labels in Settings and Profile dialogs
- Added ClientLogoPreview string property to ProfileManagementViewModel with FormatLogoPreview helper
- Updated OnSelectedProfileChanged, BrowseClientLogoAsync, ClearClientLogoAsync, AutoPullClientLogoAsync
- 17 tests pass (6 converter + 11 profile VM logo tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add IBrandingService field and DI constructor parameter to all 5 ViewModels
- Add optional IBrandingService? parameter to test constructors (PermissionsViewModel, StorageViewModel, UserAccessAuditViewModel)
- Assemble ReportBranding from GetMspLogoAsync + _currentProfile.ClientLogo before each WriteAsync call
- Pass branding as last parameter to WriteAsync in all ExportHtmlAsync methods
- Guard clause: branding assembly skipped (branding = null) when _brandingService is null (test constructors)
- Build: 0 warnings, 0 errors; tests: 254 passed / 0 failed / 26 skipped
- HtmlExportServiceTests: 3 new tests (MSP logo only, null branding no img, both logos)
- SearchExportServiceTests: 1 new branding test (img tag present when branding provided)
- StorageHtmlExportServiceTests: 1 new branding test (img tag present)
- DuplicatesHtmlExportServiceTests: 1 new branding test (img tag present)
- UserAccessHtmlExportServiceTests: 1 new branding test (img tag present)
- MakeBranding helper added to each test class
- All 45 export tests pass; full suite 247/247 with 0 failures
- 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)
Adds v2.2 milestone section (Report Branding & User Directory) while
preserving the original v1.0 summary. Covers stack additions (none),
feature table stakes vs. differentiators, architecture integration
points with dependency-aware build order, top 6 critical pitfalls with
prevention strategies, suggested roadmap phase structure, open product
questions, and confidence assessment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 7 tests covering chart series from metrics, bar series structure,
donut/bar toggle, top-10+Other aggregation, no-Other for <=10,
tenant switch cleanup, and empty data handling
- Added LiveChartsCore.SkiaSharpView.WPF to test project
- Uses reflection to set FileTypeMetrics (private setter) directly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update StorageView.xaml: DataGrid top, GridSplitter, chart panel bottom
- Add PieChart and CartesianChart with MultiDataTrigger visibility
- Add radio buttons for donut/bar chart toggle in left panel
- Create BytesLabelConverter for chart tooltip formatting
- Add stor.chart.* localization keys in EN and FR resx files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SUMMARY.md with implementation details and deviation log
- STATE.md updated to plan 2 of 4, 92% progress
- ROADMAP.md and REQUIREMENTS.md updated (VIZZ-02 complete)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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 09-01-SUMMARY.md with task details and self-check
- Update STATE.md position to Phase 9, Plan 1 of 4
- Update ROADMAP.md and REQUIREMENTS.md progress
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 LiveChartsCore.SkiaSharpView.WPF 2.0.0-rc5.4 package reference
- Create FileTypeMetric record with Extension, TotalSizeBytes, FileCount
- Include DisplayLabel computed property for chart label binding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- IsSimplifiedMode default false, toggle rebuilds SimplifiedResults
- IsDetailView toggle does not re-compute simplified data
- Summaries contains correct risk breakdown after toggle
- Helper method CreateViewModelWithResults for test reuse
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ExportCsvAsync branches on IsSimplifiedMode to call simplified WriteAsync overload
- ExportHtmlAsync branches on IsSimplifiedMode to call simplified WriteAsync overload
- Standard PermissionEntry export path unchanged when simplified mode is off
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 6 keys to Strings.resx: chk.simplified.mode, grp.display.opts, lbl.detail.level, rad.detail.detailed, rad.detail.simple, lbl.summary.users
- Add matching French translations to Strings.fr.resx with proper XML entities for accented characters
- Wire hardcoded "user(s)" text in PermissionsView.xaml summary cards to lbl.summary.users localization key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Created 08-03-SUMMARY.md with task results and self-check
- Updated STATE.md with metrics and decisions
- Updated ROADMAP.md plan progress for phase 08
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SUMMARY.md with task commits and decisions
- STATE.md updated to plan 4 of 6
- ROADMAP.md progress updated
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 Display Options GroupBox with Simplified Mode toggle and Simple/Detailed radio buttons
- Add summary panel with color-coded risk level cards bound to Summaries collection
- DataGrid binds to ActiveItemsSource, rows color-coded by RiskLevel via DataTriggers
- SimplifiedLabels column visible only in simplified mode via BooleanToVisibilityConverter
- DataGrid collapses in Simple mode via MultiDataTrigger on IsSimplifiedMode+IsDetailView
- Create InvertBoolConverter for radio button inverse binding
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>
- IsSimplifiedMode toggle switches between raw and simplified labels
- IsDetailView toggle controls individual vs summary row display
- SimplifiedResults and Summaries computed from cached Results
- ActiveItemsSource provides correct collection for DataGrid binding
- Mode toggles rebuild from cache without re-running scan
- OnTenantSwitched resets simplified state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SUMMARY.md with self-check passed
- STATE.md updated to Phase 8, Plan 1 complete
- ROADMAP.md progress updated for Phase 8
- SIMP-01 and SIMP-02 requirements marked complete
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- RiskLevel enum with High, Medium, Low, ReadOnly tiers
- PermissionLevelMapping maps 11 standard SharePoint roles to plain-language labels
- Case-insensitive lookup with Medium fallback for unknown roles
- GetHighestRisk and GetSimplifiedLabels for row-level formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plans cover plain-language permission labels, risk-level color coding,
summary counts, detail-level toggle, export integration, and unit tests.
PermissionEntry record is NOT modified — uses wrapper pattern.
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>
- Extended CreateViewModel helper to return (vm, auditMock, graphMock) 3-tuple
- Updated all 8 existing tests to use _ discard for the new graphMock slot
- Added Test 9: SearchQuery_debounced_calls_SearchUsersAsync verifying that
setting SearchQuery to "Ali" calls SearchUsersAsync after 300ms debounce
- All 9 ViewModel tests pass; full suite 177 passed / 22 skipped
- Convert User column to DataGridTemplateColumn with orange 'Guest' pill badge on IsExternalUser=true
- Add ObjectType DataGridTextColumn between Object and Permission Level
- Convert Permission Level column to DataGridTemplateColumn with red warning icon on IsHighPrivilege=true
- 12 tests: user filtering, claim format matching, Direct/Group/Inherited
access type classification, Full Control + SCA high-privilege detection,
external user flagging (#EXT#), semicolon user/level splitting, multi-site scan
- Add 17 audit.* keys and tab.userAccessAudit to Strings.resx (English)
- Add matching French translations with proper Unicode accented characters to Strings.fr.resx
- Add UserAccessAuditTabItem to MainWindow.xaml TabControl before SettingsTabItem
- Wire UserAccessAuditView content and SitePickerDialog factory in MainWindow.xaml.cs
- Register IUserAccessAuditService, IGraphUserSearchService, export services, ViewModel and View in App.xaml.cs
- Create UserAccessAuditView.xaml with two-panel layout: people picker, site picker, scan options, color-coded DataGrid with grouping, summary banner
- Create UserAccessAuditView.xaml.cs code-behind with ViewModel constructor injection
- [Rule 3] UserAccessAuditView was missing (07-05 not executed); created inline to unblock 07-07
- Two-panel layout (290px left + * right) following PermissionsView pattern
- Left panel: people picker with autocomplete list + removable user pills, site picker button, scan option checkboxes, run/cancel/export buttons
- Right panel: 3-card summary banner (TotalAccessCount, SitesCount, HighPrivilegeCount), filter TextBox, group-by ToggleButton, color-coded DataGrid
- DataGrid: color-coded rows by AccessType (Direct=blue, Group=green, Inherited=gray), warning icon for high privilege, Guest badge for external users, access type icons
- GroupStyle with Expander headers showing group name + item count
- Status bar with ProgressBar + StatusMessage
- 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
- UserAccessEntry record with 12 fields for user-centric audit results
- AccessType enum: Direct, Group, Inherited
- Pre-computed IsHighPrivilege and IsExternalUser fields for grid display
All Phase 6 global site selection features verified:
- Toolbar button, site count label, single/multi-site pre-fill
- Transfer pre-fill, local override, clear-reverts, tenant switch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- Message broadcast: GlobalSitesChangedMessage carries site list to receivers
- Base class: FeatureViewModelBase.GlobalSites updated on message receive
- Storage tab: SiteUrl pre-filled from first global site
- Storage tab: local override prevents global from overwriting SiteUrl
- Storage tab: clearing SiteUrl reverts to global site (override reset)
- Permissions tab: SelectedSites pre-populated from global sites
- Permissions tab: local picker override blocks subsequent global updates
- Tenant switch: resets local override so new global sites apply cleanly
- Transfer tab: SourceSiteUrl pre-filled from first global site
- MainWindowViewModel: GlobalSitesSelectedLabel reflects site count
- Add 06-04-SUMMARY.md with all task details and self-check
- Update STATE.md: progress bar 80%, decisions, session record
- Update ROADMAP.md: phase 6 now 4/5 plans complete (In Progress)
- Mark SITE-02 complete in REQUIREMENTS.md
- TransferViewModel: add _hasLocalSourceSiteOverride field
- Override OnGlobalSitesChanged to pre-fill SourceSiteUrl from first global site
- Add OnSourceSiteUrlChanged partial to detect local user input
- Reset _hasLocalSourceSiteOverride on tenant switch
- BulkMembersViewModel confirmed excluded: no SiteUrl field, CSV-driven, no OnGlobalSitesChanged override added
- SUMMARY.md created for plan 06-03
- STATE.md updated: progress 60%, decisions logged, session recorded
- ROADMAP.md updated: phase 6 now 3/5 summaries (In Progress)
- StorageViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- SearchViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- DuplicatesViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- FolderStructureViewModel: add OnGlobalSitesChanged, OnSiteUrlChanged, _hasLocalSiteOverride
- All four VMs pre-fill SiteUrl from first global site; local typing sets override flag
- Tenant switch resets _hasLocalSiteOverride in all four VMs
- Add Separator + Select Sites button (bound to OpenGlobalSitePickerCommand) to ToolBar
- Add TextBlock bound to GlobalSitesSelectedLabel for site count display
- Wire viewModel.OpenGlobalSitePickerDialog factory in MainWindow.xaml.cs using DI
- Add using SharepointToolbox.Core.Models for TenantProfile in code-behind
- Replace hardcoded EN strings with TranslationSource.Instance lookups
- Uses toolbar.globalSites.count (formatted) and toolbar.globalSites.none keys
- Follows same pattern as PermissionsViewModel.SitesSelectedLabel
- Add _hasLocalSiteOverride field to track local user selection
- Override OnGlobalSitesChanged to pre-populate SelectedSites from global sites
- Set _hasLocalSiteOverride=true when user picks sites via site picker dialog
- Reset _hasLocalSiteOverride=false on tenant switch (OnTenantSwitched)
- Add toolbar.selectSites, toolbar.selectSites.tooltip, toolbar.selectSites.tooltipDisabled
- Add toolbar.globalSites.count and toolbar.globalSites.none to both Strings.resx and Strings.fr.resx
- New ValueChangedMessage<IReadOnlyList<SiteInfo>> following TenantSwitchedMessage pattern
- Carries snapshot of globally selected sites (IReadOnlyList — immutable by design)
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:19:03 +02:00
1041 changed files with 39914 additions and 14010 deletions
- Keep original format — PNG stays PNG, JPEG stays JPEG (no format conversion)
- Compress until under 512 KB
- **Dimension limits:** None — the 512 KB cap and compression handle naturally
- **Validation errors:** Specific messages for format rejection (format-related only, since size is auto-handled)
### 3. Profile Deletion & Duplication Behavior
**Decision:** Warn about logo loss on deletion; do NOT copy logo on duplication.
- **Deletion:** When deleting a tenant profile that has a client logo, the confirmation message explicitly mentions it: "This will also remove its client logo." The logo is embedded in the profile JSON, so deletion is automatic — no orphaned files.
- **Duplication:** Duplicating a tenant profile copies connection fields (Name, TenantUrl, ClientId) but starts with a blank client logo. The user must re-import or auto-pull for the new profile. Rationale: duplicated profiles are typically for different tenants, so the logo shouldn't carry over.
## Deferred Ideas (out of scope for Phase 10)
- Logo preview in Settings UI (Phase 12)
- Auto-pull client logo from Entra branding API (Phase 11/12)
- Report header layout with logos side-by-side (Phase 11)
| ProfileRepository | `SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs` | Already handles TenantProfile persistence — will serialize new logo field |
| GraphUserSearchService | `SharepointToolbox/Services/GraphUserSearchService.cs` | Reference for Graph SDK usage, auth, and query patterns |
| GraphClientFactory | `SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs` | Provides `GraphServiceClient` for new directory service |
| DI registration | `SharepointToolbox/App.xaml.cs` (lines 73-163) | Register new BrandingRepository, BrandingService, GraphUserDirectoryService |
| Profile deletion UI | `SharepointToolbox/ViewModels/ProfileManagementViewModel.cs` | Update deletion confirmation message to mention logo |
**Orphaned code:**`FeatureTabBase.xaml` — Phase 1 placeholder, now superseded by full tab views. Harmless dead code.
---
## Tech Debt & Deferred Items
### From Phase Verifications
| Item | Source | Severity | Description |
|------|--------|----------|-------------|
| Hardcoded export button text | Phase 2 | Info | `PermissionsView.xaml` uses `Content="Export CSV"` / `"Export HTML"` instead of `rad.csv.perms` / `rad.html.perms` localization keys. French users see English button labels. |
| Missing Designer.cs property | Phase 2 | Info | `Strings.Designer.cs` lacks `tab_permissions` typed accessor. Runtime binding via `TranslationSource` works fine. |
| No invalid-row highlighting | Phase 4 | Warning | `BulkMembersView.xaml`, `BulkSitesView.xaml`, `FolderStructureView.xaml` show IsValid as text column but lack `RowStyle` + `DataTrigger` for visual red highlighting on invalid rows. |
| FeatureTabBase dead code | Phase 1→all | Info | `Views/Controls/FeatureTabBase.xaml` is no longer imported by any tab view after all phases replaced stubs. |
| Cancel test locale mismatch | Phase 3 (03-08) | Info | `FeatureViewModelBaseTests.CancelCommand_DuringOperation_SetsStatusMessageToCancelled` asserts `.Contains("cancel")` but app returns French string "Opération annulée". Pre-existing; deferred. |
### Deferred v2 Requirements
These are explicitly out of scope for v1 and tracked in REQUIREMENTS.md:
A full C#/WPFrewrite of an existing PowerShell-based SharePoint Online administration and auditing tool. The app lets IT administrators manage permissions, analyze storage, search files, detect duplicates, manage site templates, and perform bulk operations across SharePoint Online and Teams sites. It's a local desktop tool used by MSPs and IT teams managing multiple client tenants.
A C#/WPFdesktop application for IT administrators and MSPs to audit and manage SharePoint Online permissions, storage, files, and sites across multiple client tenants. Replaces a 6,400-line monolithic PowerShell script with a structured 10,071-line MVVM application shipping as a single self-contained EXE.
## Core Value
Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application.
## Current State
**Shipped:** v2.2 Report Branding & User Directory (2026-04-09)
**Status:** Active — v2.3 Tenant Management & Report Enhancements
## Current Milestone: v2.3 Tenant Management & Report Enhancements
**Goal:** Streamline tenant onboarding with automated app registration, add self-healing ownership for access-denied sites, and enhance report output with group expansion and entry consolidation.
**Target features:**
- App registration on target tenant (auto via Graph API + guided fallback) during profile create/edit
- App removal from target tenant
- Auto-take ownership of SharePoint sites on access denied (global toggle)
- Expand groups in HTML reports (clickable to show members)
- Report consolidation toggle (merge duplicate user entries across locations)
<details>
<summary>v2.2 shipped features</summary>
- HTML report branding with MSP logo (global) and client logo (per tenant)
- Auto-pull client logo from Entra branding API
- Logo validation (PNG/JPG, 512 KB limit) with auto-compression
- User directory browse mode in user access audit tab with paginated load
- Member/guest filter and department/job title columns
- Directory user selection triggers existing audit pipeline
</details>
<details>
<summary>v1.1 shipped features</summary>
- Global multi-site selection in toolbar (pick sites once, all tabs use them)
- User access audit tab with Graph API people-picker, direct/group/inherited access distinction
- **Current features to port:** Permissions reports, storage metrics, site templates, file search, duplicate detection, bulk operations (transfer, site creation, member addition), folder structure creation, localization (EN/FR)
- **SharePoint integration:** Currently uses PnP.PowerShell module; C# rewrite will use PnP Framework / Microsoft Graph SDK
- **Authentication:** Currently interactive Azure AD OAuth via PnP; new version needs multi-tenant session caching
- **Known issues in current app:** 38 silent catch blocks, 27 error suppressions, resource cleanup issues, UI freezes on large datasets, no operation cancellation
- **Localization:** English and French supported, key-based translation system
- **Report exports:** CSV and interactive HTML reports with embedded JS for sorting/filtering
- **v1.0 shipped** with full feature parity: permissions, storage, search, duplicates, bulk operations, templates, folder provisioning
- **v1.1 shipped** with enhanced reports: user access audit, simplified permissions, storage charts, global site selection
- **v2.2 shipped** with report branding (logos in HTML exports) and user directory browse mode
- **Localization:** 230+ EN/FR keys, full parity verified
**Core Value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application.
## v1 Requirements
## v2.3 Requirements
Requirements for initial release. Each maps to roadmap phases.
Requirements for v2.3 Tenant Management & Report Enhancements. Each maps to roadmap phases.
### Foundation
### App Registration
- [x]**FOUND-01**: Application built with C#/WPF (.NET 10 LTS) using MVVM architecture
- [x]**FOUND-02**: Multi-tenant profile registry — user can create, rename, delete, and switch between tenant profiles (tenant URL, client ID, display name)
- [x]**FOUND-03**: Multi-tenant session caching — user stays authenticated across tenant switches without re-logging in (MSAL token cache per tenant)
- [x]**FOUND-04**: Interactive Azure AD OAuth login via browser — no client secrets or certificates stored
- [x]**FOUND-05**: All long-running operations report progress to the UI in real-time
- [x]**FOUND-06**: User can cancel any long-running operation mid-execution
- [x]**FOUND-07**: All errors surface to the user with actionable messages — no silent failures
- [x]**FOUND-08**: Structured logging for diagnostics (Serilog or equivalent)
- [x]**FOUND-09**: Localization system supporting English and French with dynamic language switching
- [x]**FOUND-10**: JSON-based local storage for profiles, settings, and templates (compatible with current app's format for migration)
- [x]**FOUND-11**: Self-contained single EXE distribution — no .NET runtime dependency for end users
- [x]**FOUND-12**: Configurable data output folder for exports
- [x]**APPREG-01**: User can register the app on a target tenant from the profile create/edit dialog
- [x]**APPREG-02**: App auto-detects if user has Global Admin permissions before attempting registration
- [x]**APPREG-03**: App creates Azure AD application + service principal + grants required permissions atomically (with rollback on failure)
- [x]**APPREG-04**: User sees guided fallback instructions when auto-registration is not possible (insufficient permissions)
- [x]**APPREG-05**: User can remove the app registration from a target tenant
- [x]**APPREG-06**: App clears cached tokens and sessions when app registration is removed
### Permissions
### Site Ownership
- [x]**PERM-01**: User can scan permissions on a single SharePoint site with configurable depth
- [x]**PERM-02**: User can scan permissions across multiple selected sites in one operation
- [x]**PERM-03**: Permissions scan includes owners, members, guests, external users, and broken inheritance
- [x]**PERM-04**: User can choose to include or exclude inherited permissions
- [x]**PERM-05**: User can export permissions report to CSV (raw data)
- [x]**PERM-06**: User can export permissions report to interactive HTML (sortable, filterable, groupable by user)
- [x]**PERM-07**: SharePoint 5,000-item list view threshold handled via pagination — no silent failures on large libraries
- [x]**OWN-01**: User can enable/disable auto-take-ownership in application settings (global toggle, OFF by default)
- [x]**OWN-02**: App automatically takes site collection admin ownership when encountering access denied during scans (when toggle is ON)
### Storage
### Report Enhancements
- [x]**STOR-01**: User can view storage consumption per library on a site
- [x]**STOR-02**: User can view storage consumption per site with configurable folder depth
- [x]**STOR-03**: Storage metrics include total size, version size, item count, and last modified date
- [x]**STOR-04**: User can export storage metrics to CSV
- [x]**STOR-05**: User can export storage metrics to interactive HTML with collapsible tree view
- [x]**RPT-01**: User can expand SharePoint groups in HTML reports to see group members
- [x]**RPT-02**: Group member resolution uses transitive membership to include nested group members
- [x]**RPT-03**: User can enable/disable entry consolidation per export (toggle in export settings)
- [x]**RPT-04**: Consolidated reports merge rows for the same user with identical access levels across multiple locations into a single row
### File Search
## Future Requirements
- [x]**SRCH-01**: User can search files across sites using multiple criteria (extension, name/regex, dates, creator, editor)
- [x]**SRCH-02**: User can configure maximum search results (up to 50,000)
- [x]**SRCH-03**: User can export search results to CSV
- [x]**SRCH-04**: User can export search results to interactive HTML (sortable, filterable)
### Site Ownership (deferred)
### Duplicate Detection
- [x]**DUPL-01**: User can scan for duplicate files by name, size, creation date, modification date
- [x]**DUPL-02**: User can scan for duplicate folders by name, subfolder count, file count
- [x]**DUPL-03**: User can export duplicate report to HTML with grouped display and visual indicators
### Site Templates
- [x]**TMPL-01**: User can capture site structure (libraries, folders, permission groups, logo, settings) as a template
- [x]**TMPL-02**: User can apply template to create new Communication or Teams site
- [x]**TMPL-03**: Templates persist locally as JSON
- [x]**TMPL-04**: User can manage templates (create, rename, delete)
### Folder Structure
- [x]**FOLD-01**: User can create folder structures on a site from a CSV template
- [x]**FOLD-02**: Example CSV templates provided for common structures
### Bulk Operations
- [x]**BULK-01**: User can transfer files and folders between sites with progress tracking
- [x]**BULK-02**: User can add members to groups in bulk from CSV
- [x]**BULK-03**: User can create multiple sites in bulk from CSV
- [x]**BULK-04**: All bulk operations support cancellation mid-execution
- **Atomic commits per task**: Each plan produced clear, reviewable commit history
### What Was Inefficient
- **Phase 03 missing verification**: The only phase without a VERIFICATION.md — caught during milestone audit, but should have been produced during execution
- **Stale audit notes**: Phase 2 verification reported export buttons as hardcoded English, but they were actually already localized by the time of the audit — suggests verification reads code at a point-in-time snapshot that may not reflect later fixes
- **Cancel test locale mismatch**: The French locale test failure was flagged in Phase 3 plan 08 summary but deferred until post-milestone cleanup — should have been fixed inline
### Patterns Established
- Write-then-replace JSON persistence with SemaphoreSlim for thread safety
- TranslationSource singleton with PropertyChanged(string.Empty) for runtime culture switching
- ExecuteQueryRetryHelper for throttle-aware CSOM calls (429/503 detection)
- SharePointPaginationHelper with ListItemCollectionPosition for 5,000+ item lists
- CsvValidationService with auto-delimiter detection (comma/semicolon) and BOM handling
- DataGrid RowStyle DataTrigger for invalid-row visual highlighting
### Key Lessons
- Establish shared infrastructure patterns (auth, retry, pagination, progress) in Phase 1 — every subsequent phase benefits
- Test scaffolds (Wave 0) eliminate the "no tests until the end" anti-pattern
- Phase verifications should be mandatory during execution, not optional — catching Phase 03's gap at audit time is late
- Localization tests (key parity + diacritic spot-checks) are cheap and catch real bugs
- [x]**Phase 15: Consolidation Data Model** (2 plans) — PermissionConsolidator service and merged-row model; zero API calls, pure data shapes (completed 2026-04-09)
- [x]**Phase 17: Group Expansion in HTML Reports** (2 plans) — Clickable group expansion in HTML exports with transitive membership resolution (completed 2026-04-09)
- [x]**Phase 18: Auto-Take Ownership** (2 plans) — Global toggle and automatic site collection admin elevation on access denied (completed 2026-04-09)
**Goal**: The application shell runs, users can authenticate to multiple tenants and switch between them without re-logging in, all long-running operations are cancellable and report progress, all errors surface visibly, and the infrastructure patterns that prevent the existing app's 10 known pitfalls are in place before any feature work begins.
**Goal**: The data shape and merge logic for report consolidation exist and are fully testable in isolation before any UI touches them
**Depends on**: Nothing (no API calls, no UI dependencies)
**Requirements**: RPT-04
**Success Criteria** (what must be TRUE):
1.User can create, rename, delete, and switch between tenant profiles via the UI — each profile stores tenant URL, client ID, and display name in a JSON file
2.User can authenticate to a tenant via interactive browser login and the session persists across tenant switches without re-entering credentials (MSAL token cache per tenant)
3.User can see real-time progress on any long-running operation and cancel it mid-execution with a button — the operation stops cleanly with no silent continuation
4.When any operation fails, the user sees an actionable error message in the UI — no operation fails silently or swallows an exception
5. UI language switches between English and French dynamically without restarting the application
**Plans**: 8 plans
1.A `ConsolidatedPermissionEntry` model exists that represents a single user's merged access across multiple locations with identical access levels
2.A `PermissionConsolidator` service accepts a flat list of permission rows and returns a consolidated list where duplicate user+level rows are merged
3.Consolidation logic has unit test coverage — a known 10-row input with 3 duplicate pairs produces the expected 7-row output
4.Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off)
**Plans:** 2/2 plans complete
Plans:
- []01-01-PLAN.md — Solution scaffold: WPF project + xUnit test project with Generic Host entry point
- [ ] 01-08-PLAN.md — Checkpoint: full test suite + visual verification of running application
- [x] 15-01-PLAN.md — Models (LocationInfo, ConsolidatedPermissionEntry) + PermissionConsolidator service
- [x] 15-02-PLAN.md — Unit tests (10 test cases) + full solution build verification
### Phase 2: Permissions
**Goal**: Users can scan SharePoint permissions on one or many sites and export the results as both a raw CSV and a sortable, filterable HTML report — with no silent failures on large libraries and full control over scan scope.
**Goal**: Users can choose to merge duplicate permission entries per export through a toggle in the export settings dialog
**Depends on**: Phase 15
**Requirements**: RPT-03
**Success Criteria** (what must be TRUE):
1.User can select one site or multiple sites and run a permissions scan that returns owners, members, guests, external users, and broken inheritance items
2.User can choose configurable scan depth and whether to include or exclude inherited permissions before running
3.User can export the permissions results to a CSV file with all raw permission data
4.User can export the permissions results to an interactive HTML report where rows are sortable, filterable, and groupable by user
5. Scanning a library with more than 5,000 items completes successfully — the tool paginates automatically and does not silently truncate or fail
**Plans**: 7 plans
1.A consolidation toggle is visible in the export settings dialog (or export options panel) and defaults to OFF
2.When the toggle is OFF, the exported HTML report is byte-for-byte identical to the pre-v2.3 output
3.When the toggle is ON, the exported HTML report merges rows for the same user with identical access levels into a single row showing all affected locations
4.The toggle state is remembered for the session (does not reset between exports within the same session)
**Plans:** 2/2 plans complete
Plans:
- [x]02-01-PLAN.md — Wave 0: test scaffolds (PermissionsService, ViewModel, classification, CSV, HTML export tests) + PermissionEntryHelper
- []16-02-PLAN.md — HTML consolidated rendering with expandable location sub-lists + full test verification
### Phase 3: Storage and File Operations
**Goal**: Users can view and export storage metrics per site and library, search for files across sites using multiple criteria, and detect duplicate files and folders — all with consistent export options and no silent failures on large datasets.
**Goal**: Users can expand SharePoint group entries in HTML reports to see the group's members, including members of nested groups
**Depends on**: Phase 16
**Requirements**: RPT-01, RPT-02
**Success Criteria** (what must be TRUE):
1.User can view storage consumption per library and per site (with configurable folder depth), including total size, version size, item count, and last modified date
2.User can export storage metrics to CSV and to an interactive HTML with a collapsible tree view
3.User can search for files across sites using at least extension, name/regex, date range, creator, and editor as criteria — with a configurable result cap up to 50,000 items
4.User can export file search results to CSV and to an interactive sortable/filterable HTML
5. User can scan for duplicate files (by name, size, creation date, modification date) and duplicate folders (by name, subfolder count, file count) and export the results to an HTML with grouped display
**Plans**: 8 plans
1.SharePoint group rows in the HTML report render as expandable — clicking a group name reveals its member list inline
2.Member resolution includes transitive membership: nested groups are recursively resolved so every leaf user is shown
3.Group expansion is triggered at export time via Graph API — the permission scan itself is unchanged
4.When Graph cannot resolve a group's members (throttled or insufficient scope), the report shows the group row with a "members unavailable" label rather than failing the export
**Goal**: Users can execute bulk write operations (member additions, site creation, file transfer) with per-item error reporting and cancellation, capture site structures as reusable templates, apply templates to create new sites, and provision folder structures from CSV — all without silent partial failures.
**Goal**: Users can enable automatic site collection admin elevation so that access-denied sites during scans no longer block audit progress
**Depends on**: Phase 15
**Requirements**: OWN-01, OWN-02
**Success Criteria** (what must be TRUE):
1.User can transfer files and folders between sites with real-time progress tracking and can cancel mid-operation — transferred items are confirmed and failures are reported per-item
2.User can add members to groups in bulk from a CSV file — each row that fails is reported individually, not silently skipped
3.User can create multiple sites in bulk from a CSV file with per-site error reporting and mid-operation cancellation
4.User can capture an existing site's structure (libraries, folders, permission groups, logo, settings) as a named template stored in JSON, then apply that template to create a new Communication or Teams site
5. User can manage saved templates (create, rename, delete) and create folder structures on a target site from a CSV template
**Plans**: 10 plans
1.A global "Auto-take ownership on access denied" toggle exists in application settings and defaults to OFF
2.When the toggle is OFF, access-denied sites produce the same error behavior as before v2.3 (no regression)
3.When the toggle is ON and a scan hits access denied on a site, the app automatically calls `Tenant.SetSiteAdmin` to elevate ownership and retries the site without interrupting the scan
4.The scan result for an auto-elevated site is visually distinguishable from a normally-scanned site (e.g., a flag or icon in the results)
**Goal**: The application ships as a single self-contained EXE that runs on a machine with no .NET runtime installed, all previously identified reliability constraints are verified end-to-end (5,000-item pagination, JSON corruption recovery, throttling retry, cancellation), and the French locale is complete and tested.
**Depends on**: Phase 4
**Requirements**: FOUND-11
### Phase 19: App Registration & Removal
**Goal**: Users can register and remove the Toolbox's Azure AD application on a target tenant directly from the profile dialog, with a guided fallback when permissions are insufficient
1.Running the published EXE on a clean machine with no .NET runtime installed launches the application and all features function correctly
2.The application recovers gracefully when a SharePoint API call is throttled (429/503) — the user sees a retry progress message and the operation eventually completes or surfaces a clear failure
3.The French locale is complete for all UI strings — no English fallback text appears when the language is set to French
4. A scan against a library with more than 5,000 items returns complete, correct results with no silent truncation verified against a known dataset
**Plans**: 3 plans
1.A "Register App" action is available in the profile create/edit dialog and is the recommended path for new tenant onboarding
2.Before attempting registration, the app checks for Global Admin role and surfaces a clear message if the signed-in user lacks the required permissions, then presents step-by-step manual registration instructions as a fallback
3.Registration creates the Azure AD application, service principal, and grants all required API permissions in a single atomic operation — if any step fails, all partial changes are rolled back and the user sees a specific error explaining what failed and why
4. A "Remove App" action in the profile dialog removes the Azure AD application registration from the target tenant
5. After removal, all cached MSAL tokens and session state for that tenant are cleared, and subsequent operations require re-authentication
last_activity: 2026-04-09 — Roadmap created for v2.3 (phases 15-19)
progress:
total_phases: 5
completed_phases: 5
total_plans: 36
completed_plans: 36
percent: 100
total_plans: 10
completed_plans: 10
---
# Project State
## Project Reference
See: .planning/PROJECT.md (updated 2026-04-02)
See: .planning/PROJECT.md (updated 2026-04-09)
**Core value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application.
| 15 | Consolidation Data Model | RPT-04 | Not started |
| 16 | Report Consolidation Toggle | RPT-03 | Not started |
| 17 | Group Expansion in HTML Reports | RPT-01, RPT-02 | Not started |
| 18 | Auto-Take Ownership | OWN-01, OWN-02 | Not started |
| 19 | App Registration & Removal | APPREG-01..06 | Not started |
## Accumulated Context
### Decisions
Decisions are logged in PROJECT.md Key Decisions table.
Recent decisions affecting current work:
- Foundation: Use PnP.Framework 1.18.0 (not PnP.Core SDK) — PnP Provisioning Engine lives only in PnP.Framework
-Foundation: Use MsalCacheHelper for per-tenant token cache serialization — scope IPublicClientApplication per ClientId
-Foundation: Never set PublishTrimmed=true — PnP.Framework and MSAL use reflection; accept ~150-200 MB EXE
-Foundation: Establish AsyncRelayCommand + IProgress<T> + CancellationToken patterns before any feature work — retrofitting is the most expensive WPF refactor
-[Phase 01-foundation]: Upgraded MSAL from 4.83.1 to 4.83.3 — Extensions.Msal 4.83.3 requires MSAL >= 4.83.3; minor patch with no behavioral difference
-[Phase 01-foundation]: Test project targets net10.0-windows with UseWPF=true — required to reference WPF main project; net10.0 is framework-incompatible
- [Phase 01-foundation]: Solution uses .slnx format (new .NET 10 XML solution) — dotnet new sln creates .slnx by default in .NET 10 SDK
- [Phase 01-foundation]: TenantProfile is a plain mutable class (not record) — System.Text.Json requires settable properties; field names Name/TenantUrl/ClientId match JSON schema exactly
- [Phase 01-foundation]: SharePointPaginationHelper uses [EnumeratorCancellation] on ct — required for correct WithCancellation() forwarding in async iterators
- [Phase 01-foundation]: Explicit System.IO using required in WPF project — WPF temp build project does not include System.IO in implicit usings; all persistence classes need explicit import
- [Phase 01-foundation]: SettingsService validates only 'en' and 'fr' language codes — throws ArgumentException for unsupported codes
- [Phase 01-foundation]: LoadAsync on corrupt JSON throws InvalidDataException (not silent empty) — explicit failure protects against silent data loss
- [Phase 01-foundation]: Strings.Designer.cs maintained manually — ResXFileCodeGenerator is VS-only, not run by dotnet build; only ResourceManager accessor needed
- [Phase 01-foundation]: EmbeddedResource uses Update not Include in SDK-style project — SDK auto-includes all .resx; Include causes NETSDK1022 duplicate error
- [Phase 01-foundation]: MsalClientFactory stores MsalCacheHelper per clientId and exposes GetCacheHelper() — PnP creates its own internal PCA so tokenCacheCallback is the bridge for shared persistent cache
- [Phase 01-foundation]: SessionManager is the single holder of ClientContext instances — callers must not store returned contexts
- [Phase 01-foundation]: CacheDirectory is a constructor parameter (no-arg defaults to AppData) — enables test isolation without real filesystem writes
- [Phase 01-foundation]: Interactive login test marked Skip in unit suite — browser/WAM MSAL flow cannot run in automated CI
- [Phase 01-foundation]: ObservableRecipient lambda receivers need explicit cast to FeatureViewModelBase for virtual dispatch
- [Phase 01-foundation]: FeatureViewModelBase declared as abstract partial class — CommunityToolkit.Mvvm source generator requires partial keyword
- [Phase 01-foundation]: OpenFolderDialog (Microsoft.Win32) used in WPF instead of FolderBrowserDialog (System.Windows.Forms)
- [Phase 01-foundation]: LogPanel exposed via GetLogPanel() method — x:Name generates field in XAML partial class, property with same name causes CS0102
- [Phase 01-foundation]: ProfileManagementViewModel dialog factory pattern — ViewModel exposes Func<Window>? OpenProfileManagementDialog set by View layer; avoids Window/DI coupling in ViewModel
- [Phase 01-foundation]: IServiceProvider injected into MainWindow constructor — resolves DI-registered ProfileManagementDialog and SettingsView at runtime
- [Phase 01-foundation]: ProfileManagementDialog and SettingsView registered as Transient — fresh instance with fresh ViewModel per dialog open or tab init
- [Phase 01-foundation]: Solution file is .slnx (not .sln) — dotnet build/test commands must use SharepointToolbox.slnx
- [Phase 02-permissions]: DeriveAdminUrl is internal static — enables direct unit testing of admin URL regex without live tenant
- [Phase 02-permissions]: InternalsVisibleTo added to AssemblyInfo.cs — required for test project to access internal DeriveAdminUrl; plan omitted this assembly attribute
- [Phase 02-permissions]: Export service stubs created in Plan 02-01 so test project compiles before Plan 03 implementation
- [Phase 02-permissions]: Principal.Email removed from CSOM load expression — Email only exists on User subtype, not Principal base class
- [Phase 02-permissions]: Folder is not a SecurableObject in CSOM — ListItem used for permission extraction — Required by CSOM type system; Folder inherits from ClientObject not SecurableObject
- [Phase 02-permissions]: Principal.Email excluded from CSOM Include — email not needed for PermissionEntry — Principal base type has no Email property; only User subtype does; avoids CS1061
- [Phase 02-permissions]: CsvExportService uses UTF-8 with BOM for Excel compatibility; HtmlExportService uses UTF-8 without BOM
- [Phase 02-permissions]: ISessionManager interface extracted from concrete SessionManager — required for Moq-based unit testing of PermissionsViewModel
- [Phase 02-permissions]: PermissionsView code-behind wires Func<TenantProfile, SitePickerDialog> factory via DI — avoids Window coupling in ViewModel, keeps ViewModel testable
- [Phase 02-permissions]: ISessionManager -> SessionManager DI registration was missing from App.xaml.cs — added in plan 02-07 (auto-detected Rule 3 blocker)
- [Phase 02-permissions]: MainWindow.xaml uses x:Name on Permissions TabItem; MainWindow.xaml.cs sets Content at runtime from DI — same pattern as SettingsView
- [Phase 03-storage]: Storage display uses flat DataGrid with IndentLevel -> Margin IValueConverter (not WPF TreeView) — better UI virtualization for large sites
- [Phase 03-storage]: StorageNode.VersionSizeBytes is a derived property (TotalSizeBytes - FileStreamSizeBytes, Math.Max 0) — not stored separately
- [Phase 03-storage]: SearchService uses KeywordQuery + SearchExecutor (Microsoft.SharePoint.Client.Search.Query) — transitive dep of PnP.Framework; no new NuGet package
- [Phase 03-storage]: Search pagination: StartRow += 500, hard cap StartRow <= 50,000 (SharePoint Search boundary) = 50,000 max results
- [Phase 03-storage]: DuplicatesService uses CAML FSObjType=1 (not FileSystemObjectType) for folder queries — wrong name returns zero results silently
- [Phase 03-storage]: Phase 3 export services are separate classes from Phase 2 (StorageCsvExportService, SearchCsvExportService, etc.) — different schemas
- [Phase 03-storage]: StorageNode.VersionSizeBytes is a derived property (Math.Max(0, TotalSizeBytes - FileStreamSizeBytes)) — not stored separately
- [Phase 03-storage]: MakeKey composite key logic tested inline in DuplicatesServiceTests before Plan 03-04 creates the real class — avoids skipping all duplicate logic tests
- [Phase 03-storage]: Export service stubs return string.Empty until implemented — compile-only skeletons for Plans 03-03 and 03-05
- [Phase 03-storage 03-02]: StorageService.LastModified uses StorageMetrics.LastModified with fallback to Folder.TimeLastModified — StorageMetrics.LastModified may be DateTime.MinValue for empty libraries
- [Phase 03-storage 03-02]: System folder filter uses Forms/ and _-prefix heuristic — matches SharePoint standard hidden folder naming convention
- [Phase 03-storage]: Explicit System.IO using required in StorageCsvExportService and StorageHtmlExportService — WPF project does not include System.IO in implicit usings (established project pattern)
- [Phase 03-storage 03-04]: SearchService uses SelectProperties.Add per-item loop — StringCollection has no AddRange(string[]) overload in this SDK version
- [Phase 03-storage 03-04]: DuplicatesService.MakeKey internal static method matches inline test helper in DuplicatesServiceTests exactly — deliberate design to ensure test parity
- [Phase 03-storage 03-04]: DuplicatesService file mode re-implements pagination inline — avoids coupling between services with different result models (DuplicateItem vs SearchResult)
- [Phase 03-storage]: ClientContext.Url is read-only in CSOM — site URL override done via new TenantProfile with site URL for GetOrCreateContextAsync
- [Phase 03-storage]: IndentConverter/BytesConverter/InverseBoolConverter registered in App.xaml Application.Resources — accessible to all views without per-UserControl declaration
- [Phase 03-storage]: SearchCsvExportService uses UTF-8 BOM for Excel compatibility — consistent with Phase 2 CsvExportService pattern
- [Phase 03-storage]: DuplicatesHtmlExportService always uses badge-dup (red) for all groups — ok/diff distinction removed from final DUPL-03 spec
- [Phase 03]: SearchViewModel and DuplicatesViewModel use TenantProfile site URL override pattern — ctx.Url is read-only in CSOM (established pattern from StorageViewModel)
- [Phase 03]: DuplicateRow flat DTO wraps DuplicateItem with GroupName and GroupSize for DataGrid display
- [Phase 04-bulk-operations-and-provisioning]: ITemplateService uses ModelSiteTemplate alias — SiteTemplate is ambiguous between SharepointToolbox.Core.Models and Microsoft.SharePoint.Client; resolved with using alias
- [Phase 04-bulk-operations-and-provisioning]: ICsvValidationService and BulkResultCsvExportService require explicit System.IO using — WPF project does not include System.IO in implicit usings (established project pattern)
- [Phase 04-bulk-operations-and-provisioning]: BulkSiteService uses Core.Helpers.ExecuteQueryRetryHelper not Infrastructure.Auth — plan had wrong using; correct namespace is Core.Helpers (established pattern from Phase 2/3 services)
- [Phase 04-bulk-operations-and-provisioning]: Design-time MSBuild compile used for build verification — dotnet build WinFX BAML step fails in bash shell; C# compilation verified via dotnet msbuild -t:Compile -p:DesignTimeBuild=true with 0 errors; DLL-based test run confirms 4 pass / 3 skip
- [Phase 04-bulk-operations-and-provisioning]: CsvValidationService uses DetectDelimiter=true — handles both comma and semicolon CSV files without format-specific code paths
- [Phase 04-bulk-operations-and-provisioning]: TemplateRepository uses same atomic write pattern as SettingsRepository (tmp + File.Move + round-trip JSON validation)
- [Phase 04-bulk-operations-and-provisioning 04-04]: GraphClientFactory uses GetOrCreateAsync (async) — MsalClientFactory only exposes async method with SemaphoreSlim locking; plan had incorrect sync reference GetOrCreateClient
- [Phase 04-bulk-operations-and-provisioning 04-04]: AuthGraphClientFactory alias resolves CS0104 — Microsoft.Graph.GraphClientFactory conflicts with SharepointToolbox.Infrastructure.Auth.GraphClientFactory when both namespaces imported
- [Phase 04-bulk-operations-and-provisioning 04-04]: Microsoft.SharePoint.Client.Group? fully qualified in AddToClassicGroupAsync — Microsoft.Graph.Models.Group also in scope in BulkMemberService; explicit namespace resolves ambiguity
- [Phase 04-bulk-operations-and-provisioning]: TemplateService uses ModelSiteTemplate alias — consistent with ITemplateService; CSOM SiteTemplate and Core.Models.SiteTemplate are both in scope
- [Phase 04-bulk-operations-and-provisioning]: FolderStructureService.BuildUniquePaths sorts by slash count for parent-first ordering — ensures intermediate folders exist before children when using Folders.Add
- [Phase 04-bulk-operations-and-provisioning]: TemplateService.ApplyTemplateAsync creates new ClientContext for new site URL — adminCtx.Url points to admin site, new site needs separate context
- [Phase 04-bulk-operations-and-provisioning]: FolderBrowserDialog uses Core.Helpers.ExecuteQueryRetryHelper (not Infrastructure.Auth) — consistent with established project namespace pattern
- [Phase 04-bulk-operations-and-provisioning]: Example CSV files placed in Resources/ and registered as EmbeddedResource — accessible via Assembly.GetManifestResourceStream without file system dependency
- [Phase 04-bulk-operations-and-provisioning]: SitePickerDialog.SelectedUrls.FirstOrDefault() used for single-site transfer selection — avoids new dialog variant while reusing existing dialog
- [Phase 04-bulk-operations-and-provisioning]: EnumBoolConverter added for RadioButton-to-enum binding (TransferMode); ConverterParameter=EnumValueName pattern established for future ViewModels
- [Phase 04-bulk-operations-and-provisioning 04-09]: BulkMembersViewModel passes _currentProfile.ClientId to AddMembersAsync — IBulkMemberService interface requires clientId for Graph API authentication; plan code omitted this parameter
- [Phase 04-bulk-operations-and-provisioning 04-09]: Duplicate standalone converter files (EnumBoolConverter.cs etc.) removed — already defined in IndentConverter.cs which is the established project pattern for all converters
- [Phase 04-bulk-operations-and-provisioning]: All value converters (EnumBoolConverter, StringToVisibilityConverter, ListToStringConverter) added to IndentConverter.cs — consistent co-location pattern
- [Phase 05-distribution-and-hardening]: PublishSingleFile PropertyGroup is conditional on '' == 'true' — regular dotnet build and dotnet test are unaffected
- [Phase 05-distribution-and-hardening]: IncludeNativeLibrariesForSelfExtract=true required — PnP.Framework has native binaries that must bundle into the EXE
- [Phase 05-distribution-and-hardening]: IsThrottleException only checks top-level Message (not InnerException) — documented via nested-throttle test asserting false
- [Phase 05-distribution-and-hardening]: FR diacritics already present in Strings.fr.resx — FrStrings_ContainExpectedDiacritics test passes immediately (no diacritic repair needed for those 5 keys)
**v2.3 notable constraints:**
-Phase 19 has the highest blast radius (Entra changes) — must be last
-Phase 15 is zero-API-call foundation; unblocks Phase 16 (consolidation) and Phase 18 (ownership) independently
-Group expansion (Phase 17) calls Graph at export time, not at scan time — scan pipeline unchanged
-App registration must be atomic with rollback; partial Entra state is worse than no state
- [Phase 15]: MakeKey declared internal for test access via InternalsVisibleTo without exposing as public API
- [Phase 15]: LINQ GroupBy+Select for consolidation merge instead of mutable dictionary — consistent with functional codebase style
- [Phase 15-consolidation-data-model]: RPT-04-g test data uses 11 rows (not 10) to produce 7 consolidated rows — plan description had a counting error; 4 unique rows + 3 merged groups = 7
- [Phase 16-01]: PermissionsViewModel gets MergePermissions as no-op placeholder reserved for future use
- [Phase 16-report-consolidation-toggle]: BuildConsolidatedHtml is a private method via early-return in BuildHtml — existing code path completely untouched
- [Phase 16-report-consolidation-toggle]: Separate locIdx counter for location groups (loc0, loc1...) distinct from grpIdx for user groups (ugrp0...) prevents ID collision
- [Phase 17]: Static helpers IsAadGroup/ExtractAadGroupId/StripClaims declared internal to enable unit testing via InternalsVisibleTo without polluting public API
- [Phase 17]: Graph client created lazily on first AAD group encountered to avoid unnecessary auth overhead for groups with no nested AAD members
- [Phase 17]: groupMembers optional param in HtmlExportService — null produces identical pre-Phase-17 output; ISharePointGroupResolver injected as optional last param in PermissionsViewModel; resolution failure degrades gracefully with LogWarning
- [Phase 18-auto-take-ownership]: OwnershipElevationService uses Tenant.SetSiteAdmin from PnP.Framework
- [Phase 18-auto-take-ownership]: WasAutoElevated last positional param with default=false preserves all existing PermissionEntry callsites
- [Phase 18-auto-take-ownership]: Toggle read before scan loop (not in exception filter) — await in when clause unsupported; pre-read bool preserves semantics
- [Phase 18-auto-take-ownership]: WasAutoElevated DataTrigger last in RowStyle.Triggers — amber wins over RiskLevel color
- [Phase 19-app-registration-removal]: AppRegistrationService uses AppGraphClientFactory alias to disambiguate from Microsoft.Graph.GraphClientFactory
- [Phase 19-app-registration-removal]: BuildRequiredResourceAccess declared internal to enable direct unit testing without live Graph calls
- [Phase 19-app-registration-removal]: SharePoint AllSites.FullControl GUID marked LOW confidence — must be verified against live tenant
- [Phase 19-app-registration-removal]: ProfileManagementViewModel constructor gains IAppRegistrationService as last param — existing logo tests updated to 5-param
- [Phase 19-app-registration-removal]: TranslationSource.Instance used directly in ViewModel for status strings (consistent with runtime locale switching)
- [Phase 19-app-registration-removal]: BooleanToVisibilityConverter declared in Window.Resources (WPF built-in, no custom converter needed)
### Pending Todos
None yet.
None.
### Blockers/Concerns
- Phase 4 planning: PnP Provisioning Engine behavior for Teams-connected modern sites — edge cases need validation spike before planning
- Phase 5: User access export (v2 requirement UACC-01/02) depends on Phase 2 PermissionsService — confirm scope before Phase 5 planning
trigger: "SitePickerDialog shows 'Must specify valid information for parsing in the string' error when trying to load sites after a successful tenant connection."
created: 2026-04-07T00:00:00Z
updated: 2026-04-07T00:00:00Z
---
## Current Focus
hypothesis: ROOT CAUSE CONFIRMED — two bugs in SiteListService.GetSitesAsync
test: code reading confirmed via PnP source
expecting: fixing both issues will resolve the error
next_action: apply fix to SiteListService.cs
## Symptoms
expected: After connecting to a SharePoint tenant (https://contoso.sharepoint.com format), clicking "Select Sites" opens SitePickerDialog and loads the list of tenant sites.
actual: SitePickerDialog opens but shows error "Must specify valid information for parsing in the string" instead of loading sites.
errors: "Must specify valid information for parsing in the string" — this is an ArgumentException thrown by CSOM when it tries to parse an empty string as a site URL cursor
found: Uses GetSitePropertiesFromSharePointByFilters with StartIndex = null (for first page); OLD commented-out approach used GetSitePropertiesFromSharePoint(null, includeDetail) — note: null, not ""
implication: SiteListService passes "" which is wrong — should be null for first page
- timestamp: 2026-04-07
checked: Error message "Must specify valid information for parsing in the string"
found: This is ArgumentException thrown by Enum.Parse or string cursor parsing when given "" (empty string); CSOM's GetSitePropertiesFromSharePoint internally parses the startIndex string as a URL/cursor; passing "" triggers parse failure
implication: Direct cause of exception confirmed
- timestamp: 2026-04-07
checked: How PnP creates admin context from regular context
found: PnP uses clientContext.Clone(adminSiteUrl) — clones existing authenticated context to admin URL without triggering new auth flow
implication: SiteListService creates a SECOND AuthenticationManager and triggers second interactive login unnecessarily; should use Clone instead
## Resolution
root_cause: |
SiteListService.GetSitesAsync has two bugs:
BUG 1 (direct cause of error): Line 50 calls tenant.GetSitePropertiesFromSharePoint("", true)
with empty string "". CSOM expects null for the first page (no previous cursor), not "".
Passing "" causes CSOM to attempt parsing it as a URL cursor, throwing
ArgumentException: "Must specify valid information for parsing in the string."
BUG 2 (design problem): GetSitesAsync creates a separate TenantProfile for the admin URL
and calls SessionManager.GetOrCreateContextAsync(adminProfile) which creates a NEW
AuthenticationManager with interactive login. This triggers a SECOND browser auth flow
just to access the admin URL. The correct approach is to clone the existing authenticated
context to the admin URL using clientContext.Clone(adminUrl), which reuses the same tokens.
fix: |
1. Replace GetOrCreateContextAsync(adminProfile) with GetOrCreateContextAsync(profile) to
get the regular context, then clone it to the admin URL.
2. Replace GetSitePropertiesFromSharePointByFilters with proper pagination (StartIndex=null).
The admin URL context is obtained via: adminCtx = ctx.Clone(adminUrl)
The site listing uses: GetSitePropertiesFromSharePointByFilters with proper filter object.
**Orphaned code:**`FeatureTabBase.xaml` — Phase 1 placeholder, now superseded by full tab views. Harmless dead code.
---
## Tech Debt & Deferred Items
### From Phase Verifications
| Item | Source | Severity | Description |
|------|--------|----------|-------------|
| Hardcoded export button text | Phase 2 | Info | `PermissionsView.xaml` uses `Content="Export CSV"` / `"Export HTML"` instead of `rad.csv.perms` / `rad.html.perms` localization keys. French users see English button labels. |
| Missing Designer.cs property | Phase 2 | Info | `Strings.Designer.cs` lacks `tab_permissions` typed accessor. Runtime binding via `TranslationSource` works fine. |
| No invalid-row highlighting | Phase 4 | Warning | `BulkMembersView.xaml`, `BulkSitesView.xaml`, `FolderStructureView.xaml` show IsValid as text column but lack `RowStyle` + `DataTrigger` for visual red highlighting on invalid rows. |
| FeatureTabBase dead code | Phase 1→all | Info | `Views/Controls/FeatureTabBase.xaml` is no longer imported by any tab view after all phases replaced stubs. |
| Cancel test locale mismatch | Phase 3 (03-08) | Info | `FeatureViewModelBaseTests.CancelCommand_DuringOperation_SetsStatusMessageToCancelled` asserts `.Contains("cancel")` but app returns French string "Opération annulée". Pre-existing; deferred. |
### Deferred v2 Requirements
These are explicitly out of scope for v1 and tracked in REQUIREMENTS.md:
For current requirements, see `.planning/REQUIREMENTS.md`.
---
# Requirements: SharePoint Toolbox v2
**Defined:** 2026-04-02
**Core Value:** Administrators can audit and manage SharePoint/Teams permissions and storage across multiple client tenants from a single, reliable desktop application.
## v1 Requirements
Requirements for initial release. Each maps to roadmap phases.
### Foundation
- [x]**FOUND-01**: Application built with C#/WPF (.NET 10 LTS) using MVVM architecture
- [x]**FOUND-02**: Multi-tenant profile registry — user can create, rename, delete, and switch between tenant profiles (tenant URL, client ID, display name)
- [x]**FOUND-03**: Multi-tenant session caching — user stays authenticated across tenant switches without re-logging in (MSAL token cache per tenant)
- [x]**FOUND-04**: Interactive Azure AD OAuth login via browser — no client secrets or certificates stored
- [x]**FOUND-05**: All long-running operations report progress to the UI in real-time
- [x]**FOUND-06**: User can cancel any long-running operation mid-execution
- [x]**FOUND-07**: All errors surface to the user with actionable messages — no silent failures
- [x]**FOUND-08**: Structured logging for diagnostics (Serilog or equivalent)
- [x]**FOUND-09**: Localization system supporting English and French with dynamic language switching
- [x]**FOUND-10**: JSON-based local storage for profiles, settings, and templates (compatible with current app's format for migration)
- [x]**FOUND-11**: Self-contained single EXE distribution — no .NET runtime dependency for end users
- [x]**FOUND-12**: Configurable data output folder for exports
### Permissions
- [x]**PERM-01**: User can scan permissions on a single SharePoint site with configurable depth
- [x]**PERM-02**: User can scan permissions across multiple selected sites in one operation
- [x]**PERM-03**: Permissions scan includes owners, members, guests, external users, and broken inheritance
- [x]**PERM-04**: User can choose to include or exclude inherited permissions
- [x]**PERM-05**: User can export permissions report to CSV (raw data)
- [x]**PERM-06**: User can export permissions report to interactive HTML (sortable, filterable, groupable by user)
- [x]**PERM-07**: SharePoint 5,000-item list view threshold handled via pagination — no silent failures on large libraries
### Storage
- [x]**STOR-01**: User can view storage consumption per library on a site
- [x]**STOR-02**: User can view storage consumption per site with configurable folder depth
- [x]**STOR-03**: Storage metrics include total size, version size, item count, and last modified date
- [x]**STOR-04**: User can export storage metrics to CSV
- [x]**STOR-05**: User can export storage metrics to interactive HTML with collapsible tree view
### File Search
- [x]**SRCH-01**: User can search files across sites using multiple criteria (extension, name/regex, dates, creator, editor)
- [x]**SRCH-02**: User can configure maximum search results (up to 50,000)
- [x]**SRCH-03**: User can export search results to CSV
- [x]**SRCH-04**: User can export search results to interactive HTML (sortable, filterable)
### Duplicate Detection
- [x]**DUPL-01**: User can scan for duplicate files by name, size, creation date, modification date
- [x]**DUPL-02**: User can scan for duplicate folders by name, subfolder count, file count
- [x]**DUPL-03**: User can export duplicate report to HTML with grouped display and visual indicators
### Site Templates
- [x]**TMPL-01**: User can capture site structure (libraries, folders, permission groups, logo, settings) as a template
- [x]**TMPL-02**: User can apply template to create new Communication or Teams site
- [x]**TMPL-03**: Templates persist locally as JSON
- [x]**TMPL-04**: User can manage templates (create, rename, delete)
### Folder Structure
- [x]**FOLD-01**: User can create folder structures on a site from a CSV template
- [x]**FOLD-02**: Example CSV templates provided for common structures
### Bulk Operations
- [x]**BULK-01**: User can transfer files and folders between sites with progress tracking
- [x]**BULK-02**: User can add members to groups in bulk from CSV
- [x]**BULK-03**: User can create multiple sites in bulk from CSV
- [x]**BULK-04**: All bulk operations support cancellation mid-execution
- [x]**Phase 2: Permissions** - Permissions scan (single and multi-site), CSV and HTML report export
- [x]**Phase 3: Storage and File Operations** - Storage metrics, file search, and duplicate detection (completed 2026-04-02)
- [x]**Phase 4: Bulk Operations and Provisioning** - Bulk member/site/transfer operations, site templates, folder structure provisioning (completed 2026-04-03)
- [x]**Phase 5: Distribution and Hardening** - Self-contained EXE packaging, end-to-end validation, FR locale completeness (completed 2026-04-03)
## Phase Details
### Phase 1: Foundation
**Goal**: The application shell runs, users can authenticate to multiple tenants and switch between them without re-logging in, all long-running operations are cancellable and report progress, all errors surface visibly, and the infrastructure patterns that prevent the existing app's 10 known pitfalls are in place before any feature work begins.
1. User can create, rename, delete, and switch between tenant profiles via the UI — each profile stores tenant URL, client ID, and display name in a JSON file
2. User can authenticate to a tenant via interactive browser login and the session persists across tenant switches without re-entering credentials (MSAL token cache per tenant)
3. User can see real-time progress on any long-running operation and cancel it mid-execution with a button — the operation stops cleanly with no silent continuation
4. When any operation fails, the user sees an actionable error message in the UI — no operation fails silently or swallows an exception
5. UI language switches between English and French dynamically without restarting the application
**Plans**: 8 plans
Plans:
- [ ] 01-01-PLAN.md — Solution scaffold: WPF project + xUnit test project with Generic Host entry point
- [ ] 01-08-PLAN.md — Checkpoint: full test suite + visual verification of running application
### Phase 2: Permissions
**Goal**: Users can scan SharePoint permissions on one or many sites and export the results as both a raw CSV and a sortable, filterable HTML report — with no silent failures on large libraries and full control over scan scope.
1. User can select one site or multiple sites and run a permissions scan that returns owners, members, guests, external users, and broken inheritance items
2. User can choose configurable scan depth and whether to include or exclude inherited permissions before running
3. User can export the permissions results to a CSV file with all raw permission data
4. User can export the permissions results to an interactive HTML report where rows are sortable, filterable, and groupable by user
5. Scanning a library with more than 5,000 items completes successfully — the tool paginates automatically and does not silently truncate or fail
**Plans**: 7 plans
Plans:
- [x] 02-01-PLAN.md — Wave 0: test scaffolds (PermissionsService, ViewModel, classification, CSV, HTML export tests) + PermissionEntryHelper
**Goal**: Users can view and export storage metrics per site and library, search for files across sites using multiple criteria, and detect duplicate files and folders — all with consistent export options and no silent failures on large datasets.
1. User can view storage consumption per library and per site (with configurable folder depth), including total size, version size, item count, and last modified date
2. User can export storage metrics to CSV and to an interactive HTML with a collapsible tree view
3. User can search for files across sites using at least extension, name/regex, date range, creator, and editor as criteria — with a configurable result cap up to 50,000 items
4. User can export file search results to CSV and to an interactive sortable/filterable HTML
5. User can scan for duplicate files (by name, size, creation date, modification date) and duplicate folders (by name, subfolder count, file count) and export the results to an HTML with grouped display
**Goal**: Users can execute bulk write operations (member additions, site creation, file transfer) with per-item error reporting and cancellation, capture site structures as reusable templates, apply templates to create new sites, and provision folder structures from CSV — all without silent partial failures.
1. User can transfer files and folders between sites with real-time progress tracking and can cancel mid-operation — transferred items are confirmed and failures are reported per-item
2. User can add members to groups in bulk from a CSV file — each row that fails is reported individually, not silently skipped
3. User can create multiple sites in bulk from a CSV file with per-site error reporting and mid-operation cancellation
4. User can capture an existing site's structure (libraries, folders, permission groups, logo, settings) as a named template stored in JSON, then apply that template to create a new Communication or Teams site
5. User can manage saved templates (create, rename, delete) and create folder structures on a target site from a CSV template
**Goal**: The application ships as a single self-contained EXE that runs on a machine with no .NET runtime installed, all previously identified reliability constraints are verified end-to-end (5,000-item pagination, JSON corruption recovery, throttling retry, cancellation), and the French locale is complete and tested.
**Depends on**: Phase 4
**Requirements**: FOUND-11
**Success Criteria** (what must be TRUE):
1. Running the published EXE on a clean machine with no .NET runtime installed launches the application and all features function correctly
2. The application recovers gracefully when a SharePoint API call is throttled (429/503) — the user sees a retry progress message and the operation eventually completes or surfaces a clear failure
3. The French locale is complete for all UI strings — no English fallback text appears when the language is set to French
4. A scan against a library with more than 5,000 items returns complete, correct results with no silent truncation verified against a known dataset
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.