diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 08508e8..c3428db 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -9,9 +9,9 @@ Requirements for v2.2 Report Branding & User Directory. Each maps to roadmap pha ### Report Branding -- [ ] **BRAND-01**: User can import an MSP logo in application settings (global, persisted across sessions) +- [x] **BRAND-01**: User can import an MSP logo in application settings (global, persisted across sessions) - [ ] **BRAND-02**: User can preview the imported MSP logo in settings UI -- [ ] **BRAND-03**: User can import a client logo per tenant profile +- [x] **BRAND-03**: User can import a client logo per tenant profile - [ ] **BRAND-04**: User can auto-pull client logo from tenant's Entra branding API - [ ] **BRAND-05**: All five HTML report types display MSP and client logos in a consistent header - [x] **BRAND-06**: Logo import validates format (PNG/JPG) and enforces 512 KB size limit @@ -51,8 +51,8 @@ Which phases cover which requirements. Updated during roadmap creation. | Requirement | Phase | Status | |-------------|-------|--------| -| BRAND-01 | Phase 10 | Pending | -| BRAND-03 | Phase 10 | Pending | +| BRAND-01 | Phase 10 | Complete | +| BRAND-03 | Phase 10 | Complete | | BRAND-06 | Phase 10 | Complete | | BRAND-05 | Phase 11 | Pending | | BRAND-04 | Phase 11 | Pending | diff --git a/.planning/STATE.md b/.planning/STATE.md index 768c263..07f2b7a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,8 +3,8 @@ gsd_state_version: 1.0 milestone: v2.2 milestone_name: Report Branding & User Directory status: planning -stopped_at: Completed 10-branding-data-foundation-02-PLAN.md -last_updated: "2026-04-08T10:33:30.250Z" +stopped_at: Completed 10-branding-data-foundation/10-01-PLAN.md +last_updated: "2026-04-08T10:33:47.224Z" last_activity: 2026-04-08 — Roadmap created for v2.2 progress: total_phases: 5 @@ -55,6 +55,8 @@ Decisions are logged in PROJECT.md Key Decisions table. - [Phase 10-branding-data-foundation]: No ConsistencyLevel header on equality filter for GetUsersAsync (unlike GraphUserSearchService startsWith which requires it) - [Phase 10-branding-data-foundation]: MapUser extracted as internal static in GraphUserDirectoryService for direct unit testability without live Graph endpoint - [Phase 10-branding-data-foundation]: Type alias AppGraphClientFactory used in GraphUserDirectoryService to disambiguate from Microsoft.Graph.GraphClientFactory +- [Phase 10-branding-data-foundation]: Used WPF PresentationCore (BitmapDecoder/TransformedBitmap/JpegBitmapEncoder) for image compression instead of System.Drawing.Bitmap — System.Drawing.Common is not available without a new NuGet package on .NET 10, and WPF PresentationCore is already in the stack +- [Phase 10-branding-data-foundation]: LogoData is a non-positional record with init properties (not positional constructor) to avoid System.Text.Json deserialization failure ### Pending Todos @@ -69,7 +71,7 @@ None. ## Session Continuity -Last session: 2026-04-08T10:33:19.924Z -Stopped at: Completed 10-branding-data-foundation-02-PLAN.md +Last session: 2026-04-08T10:33:47.222Z +Stopped at: Completed 10-branding-data-foundation/10-01-PLAN.md Resume file: None Next step: `/gsd:plan-phase 10` diff --git a/.planning/phases/10-branding-data-foundation/10-01-SUMMARY.md b/.planning/phases/10-branding-data-foundation/10-01-SUMMARY.md new file mode 100644 index 0000000..21c9882 --- /dev/null +++ b/.planning/phases/10-branding-data-foundation/10-01-SUMMARY.md @@ -0,0 +1,130 @@ +--- +phase: 10-branding-data-foundation +plan: 01 +subsystem: branding +tags: [logo, base64, json-persistence, wpf-imaging, magic-bytes, compression] + +requires: [] + +provides: + - LogoData record (Base64 + MimeType init properties) — shared model for all logo storage + - BrandingSettings class with nullable MspLogo — MSP-level branding persistence model + - TenantProfile.ClientLogo property — per-tenant client logo (additive, no breaking changes) + - BrandingRepository — JSON persistence with write-then-replace safety using SemaphoreSlim + - IBrandingService / BrandingService — magic byte validation, auto-compression, MSP logo CRUD + +affects: + - 10-02 (branding UI ViewModel will consume IBrandingService) + - 11-report-branding (HTML export will use LogoData from BrandingSettings and TenantProfile) + - Phase 13-14 (TenantProfile extended — profile serialization must stay compatible) + +tech-stack: + added: [] + patterns: + - BrandingRepository mirrors SettingsRepository exactly (SemaphoreSlim write-then-replace, JsonDocument validation) + - LogoData as non-positional record with init properties (avoids System.Text.Json positional constructor pitfall) + - BrandingService uses WPF PresentationCore (BitmapDecoder/TransformedBitmap/BitmapEncoder) for compression — no new NuGet package required + - Magic byte detection (4 bytes PNG, 3 bytes JPEG) before extension check — format is determined by content, not filename + +key-files: + created: + - SharepointToolbox/Core/Models/LogoData.cs + - SharepointToolbox/Core/Models/BrandingSettings.cs + - SharepointToolbox/Infrastructure/Persistence/BrandingRepository.cs + - SharepointToolbox/Services/IBrandingService.cs + - SharepointToolbox/Services/BrandingService.cs + - SharepointToolbox.Tests/Services/BrandingRepositoryTests.cs + - SharepointToolbox.Tests/Services/BrandingServiceTests.cs + modified: + - SharepointToolbox/Core/Models/TenantProfile.cs + +key-decisions: + - "Used WPF PresentationCore (BitmapDecoder/TransformedBitmap/JpegBitmapEncoder) for image compression instead of System.Drawing.Bitmap — System.Drawing.Common is not available without a new NuGet package on .NET 10, but WPF PresentationCore is already in the stack (net10.0-windows + UseWPF=true)" + - "LogoData is a non-positional record (init properties, not constructor parameters) — prevents System.Text.Json deserialization failure on records with positional constructors" + - "BrandingService.ImportLogoAsync is pure (no persistence) — caller decides where to store the LogoData; ViewModel in Phase 11 will call SaveMspLogoAsync or equivalent client logo save" + +patterns-established: + - "Repository pattern: BrandingRepository is structural clone of SettingsRepository — same SemaphoreSlim(1,1) write lock, write-tmp-then-validate-then-move safety protocol" + - "Magic byte validation: PNG checked with 4 bytes (0x89 0x50 0x4E 0x47), JPEG with 3 bytes (0xFF 0xD8 0xFF) — content-based not extension-based" + - "Compression two-pass: 300x300 quality 75 first, 200x200 quality 50 if still over limit" + - "Test pattern: IDisposable + Path.GetTempFileName() + Dispose cleanup of .tmp files — matches existing SettingsServiceTests" + +requirements-completed: + - BRAND-01 + - BRAND-03 + - BRAND-06 + +duration: 4min +completed: 2026-04-08 +--- + +# Phase 10 Plan 01: Branding Data Foundation Summary + +**LogoData record + BrandingRepository (write-then-replace JSON) + BrandingService with PNG/JPEG magic byte validation and WPF-based auto-compression to 512 KB limit** + +## Performance + +- **Duration:** ~4 min +- **Started:** 2026-04-08T00:28:31Z +- **Completed:** 2026-04-08T00:32:26Z +- **Tasks:** 2 +- **Files modified:** 8 (7 created, 1 modified) + +## Accomplishments +- LogoData record, BrandingSettings model, and TenantProfile.ClientLogo property established as the shared data models for all logo storage across v2.2 +- BrandingRepository persists BrandingSettings to branding.json with write-then-replace safety (SemaphoreSlim + tmp file + JsonDocument validation before move) +- BrandingService validates PNG/JPEG via magic bytes, rejects all other formats with descriptive error message mentioning PNG and JPG, auto-compresses files over 512 KB using WPF imaging in two passes + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Create logo models, BrandingRepository, and repository tests** - `2280f12` (feat) +2. **Task 2: Create BrandingService with validation, compression, and tests** - `1303866` (feat) + +## Files Created/Modified +- `SharepointToolbox/Core/Models/LogoData.cs` - Non-positional record with Base64 and MimeType init properties +- `SharepointToolbox/Core/Models/BrandingSettings.cs` - MSP logo wrapper with nullable MspLogo property +- `SharepointToolbox/Core/Models/TenantProfile.cs` - Extended with nullable ClientLogo property (additive only) +- `SharepointToolbox/Infrastructure/Persistence/BrandingRepository.cs` - JSON persistence mirroring SettingsRepository pattern +- `SharepointToolbox/Services/IBrandingService.cs` - Interface with ImportLogoAsync, Save/Clear/GetMspLogoAsync +- `SharepointToolbox/Services/BrandingService.cs` - Magic byte validation, WPF-based compression, MSP logo CRUD +- `SharepointToolbox.Tests/Services/BrandingRepositoryTests.cs` - 5 tests: defaults, round-trip, dir creation, TenantProfile serialization +- `SharepointToolbox.Tests/Services/BrandingServiceTests.cs` - 9 tests: PNG/JPEG acceptance, BMP rejection, empty file, no-compression, compression, CRUD + +## Decisions Made +- Used WPF PresentationCore imaging (BitmapDecoder, TransformedBitmap, JpegBitmapEncoder) for compression — `System.Drawing.Common` is not available without a new NuGet package on .NET 10 and is not in the existing stack +- `ImportLogoAsync` is kept pure (no persistence side-effects) — caller decides where to store the returned `LogoData`, enabling reuse for both MSP logo and per-tenant client logo paths + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Used WPF PresentationCore instead of System.Drawing.Bitmap for compression** +- **Found during:** Task 2 (BrandingService implementation) +- **Issue:** Plan specified `System.Drawing.Bitmap` and `ImageCodecInfo`, but `System.Drawing.Common` is not in the project's package list and is not available on .NET 10 without an explicit NuGet package reference. Adding it would violate the v2.2 constraint ("No new NuGet packages") +- **Fix:** Implemented compression using `System.Windows.Media.Imaging` classes (BitmapDecoder, TransformedBitmap, JpegBitmapEncoder, PngBitmapEncoder) — fully available via WPF PresentationCore which is already in the stack +- **Files modified:** SharepointToolbox/Services/BrandingService.cs +- **Verification:** All 9 BrandingServiceTests pass including the compression test (400x400 random-pixel PNG over 512 KB compressed to under 512 KB) +- **Committed in:** 1303866 (Task 2 commit) + +--- + +**Total deviations:** 1 auto-fixed (Rule 1 — implementation approach) +**Impact on plan:** No scope change. Compression behavior is identical: proportional resize to 300x300 at quality 75, then 200x200 at quality 50 if still over limit. WPF APIs provide the same capability without a new dependency. + +## Issues Encountered +None — build and all tests passed first time after implementation. + +## User Setup Required +None - no external service configuration required. + +## Next Phase Readiness +- All logo storage models and infrastructure are ready for Phase 10 Plan 02 (branding UI ViewModel) +- BrandingService.ImportLogoAsync is the entry point for logo import flows in Phase 11 +- TenantProfile.ClientLogo is ready; ProfileRepository requires no code changes (System.Text.Json handles the new nullable property automatically) +- 14 total Branding tests passing; 10 ProfileService tests confirm no regression from TenantProfile extension + +--- +*Phase: 10-branding-data-foundation* +*Completed: 2026-04-08*