From 5e47bca82fc4df6540d4dcccfdd391e41b0485e7 Mon Sep 17 00:00:00 2001 From: Chaim Marcus Date: Mon, 11 May 2026 09:21:39 +0000 Subject: [PATCH] chore: bootstrap Task Master with 9-task PRD breakdown Adds the Task Master AI workspace per ~/espocrm-extensions/CLAUDE.md convention (per-extension .taskmaster/). The PRD parses into 9 tasks matching the 6-week implementation plan: #1 Windows runner + code signing infra (high) #2 Core services: EspoCrmClient, DPAPI, settings, logger (high) #3 Feature 1: File mail to case (high) #4 Feature 2: Reading-pane sidebar (medium) #5 Feature 3: Compose from case (medium) #6 Feature 4: Folder auto-sync (medium) #7 Localization + RTL (medium) #8 ClickOnce publish + release pipeline (high) #9 Documentation + rollout (high) Config uses provider=claude-code (free) per workspace convention. Removed task-master init's overaggressive .gitignore entries (*.sln, *.sw?, .vscode, .idea, *.ntvs*, *.njsproj, node_modules/) that collided with this repo's VS solution file. Refs Task Master: see .taskmaster/tasks/tasks.json Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 12 ++++ .gitignore | 11 +++ .taskmaster/docs/prd.txt | 47 +++++++++++++ .taskmaster/tasks/tasks.json | 130 +++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 .env.example create mode 100644 .taskmaster/docs/prd.txt create mode 100644 .taskmaster/tasks/tasks.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..60bd23e --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# API Keys (Required to enable respective provider) +ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-... +PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-... +OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-... +GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models. +MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models. +XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models. +GROQ_API_KEY="YOUR_GROQ_KEY_HERE" # Optional, for Groq models. +OPENROUTER_API_KEY="YOUR_OPENROUTER_KEY_HERE" # Optional, for OpenRouter models. +AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json). +OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication. +GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_... \ No newline at end of file diff --git a/.gitignore b/.gitignore index f06a21b..38e9b05 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,14 @@ desktop.ini .taskmaster/state.json .taskmaster/reports/ .taskmaster/templates/ + +# Task Master env / debug +.env +.taskmaster/dev-debug.log +.taskmaster/npm-debug.log* + +# (NOTE: removed `*.sln`, `*.sw?`, `.vscode`, `.idea`, `*.ntvs*`, `*.njsproj`, +# `node_modules/` that Task Master init added — they collide with this +# repo's VS solution file (OutlookAddin.sln) and we don't have Node/IDE +# directories here.) + diff --git a/.taskmaster/docs/prd.txt b/.taskmaster/docs/prd.txt new file mode 100644 index 0000000..7b42f80 --- /dev/null +++ b/.taskmaster/docs/prd.txt @@ -0,0 +1,47 @@ +# OutlookAddin — Product Requirements Document + +## Overview +A Windows VSTO/COM Outlook Add-in (C#/.NET Framework 4.8) that integrates Outlook desktop with EspoCRM. Targets 10 lawyers at marcus-law.co.il who run Outlook desktop with IMAP-connected Gmail mailboxes. The server-side `MailRouter v1.2.0` extension is already deployed and exposes `POST /api/v1/MailRouter/file` for filing parsed emails into Case/Lead/Contact/Account records. + +The add-in must support Microsoft 365, Office 2021, and Office 2019 (not 2016). Distribution via ClickOnce hosted at https://platform.dev.marcus-law.co.il/outlook-addin/, signed with a self-signed code certificate trusted via GPO push on company-managed PCs. + +## Tech stack +- .NET Framework 4.8 (VSTO host) — net48 for Core/UI/Tests projects (SDK-style csproj) +- WPF for dialogs (ElementHost inside WinForms UserControl for CustomTaskPane) +- HttpClient + Polly for HTTP (retry, timeout, circuit breaker) +- System.Text.Json (NuGet polyfill for 4.8) +- Serilog with File sink (daily rolling, 14-day retention) +- CommunityToolkit.Mvvm (source generators) +- xUnit + Moq + FluentAssertions for tests +- DPAPI for API key storage (`%APPDATA%\MarcusLaw\OutlookAddin\creds.dat`) +- Settings as JSON in `%APPDATA%\MarcusLaw\OutlookAddin\settings.json` +- Logs in `%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs\` + +## Tasks + +### Task 1: Infrastructure setup (Week 0) +Provision a Windows runner on Coolify (Windows 11 Pro VM, 4 vCPU/8GB) with VS Build Tools (workloads: VisualStudioExtension + VSTO). Install gitea/act_runner as a service, registered against gitea.dev.marcus-law.co.il with label `windows`. Generate a self-signed code-signing certificate (`New-SelfSignedCertificate`, EKU=CodeSigning, 3-year validity). Install the cert PFX in the runner's `CurrentUser\My` store. Push the public cert to all 10 lawyer machines via GPO into Trusted Publishers + Trusted Root CA. Add CODESIGN_THUMBPRINT, CODESIGN_PFX_BASE64, CODESIGN_PASSWORD, PLATFORM_DEV_SSH_KEY to Infisical `/outlook-addin` (dev env). Verify the runner can build the existing solution scaffold by pushing a no-op commit. Dependencies: none. Priority: high. + +### Task 2: EspoCrmClient + Core services (Week 1) +Implement `MarcusLaw.OutlookAddin.Core.Services.EspoCrmClient` — a typed HTTP wrapper for the EspoCRM REST API. Methods: `FileMailAsync(FileEmailRequest)`, `GlobalSearchAsync(query)`, `EmailAddressSearchAsync(addr, entityType)`, `GetCaseAsync(id)`, `GetAccountAsync(id)`, `GetContactAsync(id)`, `TestConnectionAsync()` (calls GET /api/v1/App/user). Use HttpClient with Polly chain: Timeout(15s) → Retry(3, exponential 500ms) → CircuitBreaker(5 fails / 30s). Auth via `Espo-Authorization: ` header. Add corresponding DTOs in `Core/Models/`: FileEmailRequest, FileEmailResponse, EspoEntityRef, SearchResultGroup. Implement DpapiCredentialStore (ProtectedData.Protect with sha256 entropy). Implement SettingsManager (load/save JSON to %APPDATA%). Implement LoggerFactory (Serilog with daily rolling sink). Write xUnit tests with mocked HttpMessageHandler covering happy path + 401 + 409 + network failure. Dependencies: Task 1. Priority: high. + +### Task 3: Feature 1 — File existing mail to case (Week 1-2) +Implement the primary "File to EspoCRM" feature. UI: Explorer ribbon button "File to EspoCRM" (enabled when 1+ MailItems selected). FileToCaseDialog (WPF, RTL, 600x500): debounced live-search calling GlobalSearch, grouped results by entity type, "Recent" sidebar (10 cached in settings.json), "Create new Contact from sender" button. FilingService.FileAsync extracts each MailItem's properties on the STA thread (subject, from, to/cc/bcc, body, dateSent, PR_INTERNET_MESSAGE_ID via PropertyAccessor), reads attachments via SaveAsFile-to-temp + base64-encode, then POSTs to /MailRouter/file. On success: set MailItem.Categories += "Filed: " (create the category in the store first). On 401: re-enter API key modal. On 409: treat as success. On failure: enqueue in DiskRetryQueue. Toast summary at the end. Dependencies: Task 2. Priority: high. + +### Task 4: Feature 2 — Reading-pane sidebar (Week 3) +Implement the CustomTaskPane sidebar that shows matched Case/Contact for the selected email's sender. Register CustomTaskPane for olExplorer, docked right, 320px. WPF view bound to ReadingPaneViewModel. ExplorerSelectionHandler debounces SelectionChange (200ms), captures MailItem.SenderEmailAddress on STA, calls MatchingService.LookupAsync. MatchingService: 1) GET /EmailAddress/search?q=&entityType=Contact, 2) if 1 hit, follow accountId + recent cases via GET /Case?where[linkedWith]=contacts:&maxSize=5, 3) cache 10 min in MemoryCache. Show match cards with "Open in EspoCRM" deep link (https://crm.../#Case/view/), "File to this", "File elsewhere". Empty state with inline search. Dependencies: Task 3. Priority: medium. + +### Task 5: Feature 3 — Compose from Case (Week 4) +Implement bidirectional compose flow. Outlook→EspoCRM: Inspector ribbon button "Compose from Case" visible only on MailItem compose windows. ComposeFromCaseDialog (filtered to Case results). On confirm: GET /Case/ → set MailItem.To from primary contact's email, MailItem.Subject = "[] ", append HTML footer with case deep link. Stamp UserProperty `EspoCaseId` = caseId. EspoCRM→Outlook: register URL protocol `outlookaddin://` under HKCU\Software\Classes at install time. Build a small OutlookAddinProtocolHandler.exe that writes to a named pipe `\\.\pipe\MarcusLaw.OutlookAddin`. The Add-in opens the pipe on startup; pipe message triggers compose flow. ItemSend handler: when MailItem.UserProperties["EspoCaseId"] is set, after send completes (need to handle IMAP latency via Items.ItemAdd watcher on Sent Items with 60s fallback), auto-file via FilingService. Dependencies: Task 3. Priority: medium. + +### Task 6: Feature 4 — Folder auto-sync (Week 5) +Implement folder watching. Settings tab "Folders": TreeView mirroring Outlook's folder hierarchy with checkboxes. Per watched folder: default parent (optional dropdown via entity search), mode (Auto-file when confident / Notify only), confidence threshold. FolderWatcher singleton holds List in a STATIC field (critical to prevent GC unsubscribe). For each watched folder: subscribe ItemAdd. On ItemAdd: capture EntryID immediately, dedup via HashSet, queue to STA-bound TaskScheduler, call MatchingService.LookupAsync(sender). 1 hit + auto-mode → FilingService. Else → MailItem.Categories += "Needs filing" + Windows toast via Microsoft.Toolkit.Uwp.Notifications. Persist (StoreId, FolderPath, EntryID) in settings.json. Re-bind on startup. Dependencies: Task 4. Priority: medium. + +### Task 7: Localization + RTL theme (Week 6) +Implement Hebrew + English localization. LocalizationManager singleton holding current CultureInfo, exposes Get(key). WPF markup extension `{l:Loc Key=...}` for XAML bindings. Strings.he.resx + Strings.en.resx covering all dialog text, error messages, ribbon labels, toast titles. RTL themes (FlowDirection=RightToLeft for all WPF root elements). Hebrew font stack (Segoe UI, David, Frank Ruehl) in Themes/RtlFonts.xaml. Default culture from CultureInfo.CurrentUICulture, override-able in Settings (requires restart). Test all 4 features render correctly in he-IL. Dependencies: Task 3, 4, 5, 6 (UI must exist before localizing). Priority: medium. + +### Task 8: ClickOnce publish + release pipeline (Week 6) +Wire up the full release pipeline. Update .gitea/workflows/build.yml: invoke mage.exe to create ClickOnce manifests, sign .vsto + .application + DLLs with signtool using the cert from Infisical. Rsync publish/ to platform.dev.marcus-law.co.il:/var/www/outlook-addin/ via SSH (key from Infisical /outlook-addin/PLATFORM_DEV_SSH_KEY). Configure platform.dev nginx to serve /outlook-addin/* with proper MIME types (.vsto, .application, .manifest). Post Mattermost notification to #git-verelasim on successful release (reuse existing webhook from Infisical /mattermost). Tag the first release v1.0.0. Test end-to-end install on a clean Windows VM. Document the manual GPO trust deployment in docs/IT-SETUP.md. Dependencies: Task 1, 7. Priority: high. + +### Task 9: Documentation + rollout (Week 6) +Finalize docs/ONBOARDING.md (Hebrew, lawyer-facing) and docs/IT-SETUP.md (admin: cert generation, GPO push, API key provisioning). Record a 5-minute screen capture demoing all 4 features. Coordinate with IT to push the self-signed cert to all 10 machines via GPO BEFORE the install link goes out. Generate API keys for all 10 lawyers in EspoCRM Admin → API Users (or per-user keys via User Profile). Distribute the install link + per-user API key by individual secure email. Provide office hours for the first 48 hours. Dependencies: Task 8. Priority: high. diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json new file mode 100644 index 0000000..b40fac9 --- /dev/null +++ b/.taskmaster/tasks/tasks.json @@ -0,0 +1,130 @@ +{ + "master": { + "tasks": [ + { + "id": 1, + "title": "Provision Windows CI runner and code-signing infrastructure", + "description": "Set up Windows runner on Coolify with VS Build Tools + VSTO workload, generate self-signed code-signing certificate, configure Gitea Actions runner, and store credentials in Infisical.", + "details": "1. Provision Windows 11 Pro VM on Coolify dev server (4 vCPU/8GB)\n2. Install VS Build Tools 2022 with workloads: VisualStudioExtension + Microsoft.VisualStudio.Workload.Office\n3. Install gitea/act_runner as Windows service, register with gitea.dev.marcus-law.co.il using label 'windows'\n4. Generate self-signed code-signing certificate:\n ```powershell\n $cert = New-SelfSignedCertificate -Subject \"CN=Marcus-Law OutlookAddin\" -Type CodeSigningCert -CertStoreLocation Cert:\\CurrentUser\\My -NotAfter (Get-Date).AddYears(3)\n $thumbprint = $cert.Thumbprint\n $pfxPassword = ConvertTo-SecureString -String \"\" -Force -AsPlainText\n Export-PfxCertificate -Cert $cert -FilePath \"C:\\codesign.pfx\" -Password $pfxPassword\n ```\n5. Export public cert (.cer) for GPO distribution to lawyer PCs\n6. Store in Infisical /outlook-addin (dev env):\n - CODESIGN_THUMBPRINT (string)\n - CODESIGN_PFX_BASE64 (base64-encoded PFX bytes)\n - CODESIGN_PASSWORD (string)\n - PLATFORM_DEV_SSH_KEY (SSH private key for rsync to platform.dev)\n7. Add Gitea Actions secrets referencing Infisical keys\n8. Verify runner by pushing a no-op commit and watching build.yml execute\n9. Document GPO cert deployment in docs/IT-SETUP.md (import to Trusted Publishers + Trusted Root CA on all 10 lawyer machines)", + "testStrategy": "Push a test commit that triggers .gitea/workflows/build.yml. Verify: (1) job runs on windows runner, (2) nuget restore succeeds, (3) msbuild compiles all 4 projects without errors, (4) signtool can access certificate via thumbprint from secrets. Check Gitea Actions logs for green build.", + "priority": "high", + "dependencies": [], + "status": "pending", + "subtasks": [] + }, + { + "id": 2, + "title": "Implement Core services: EspoCrmClient, CredentialStore, SettingsManager, Logger", + "description": "Build the foundational Core layer services for HTTP communication with EspoCRM, encrypted credential storage via DPAPI, JSON settings persistence, and Serilog file logging.", + "details": "In OutlookAddin.Core project:\n\n1. **EspoCrmClient** (`Services/EspoCrmClient.cs`):\n - Constructor accepts (baseUrl, username, apiKey, ILogger)\n - Auth: `Espo-Authorization` header = base64(username:apiKey)\n - HttpClient with Polly resilience pipeline:\n ```csharp\n var pipeline = new ResiliencePipelineBuilder()\n .AddTimeout(TimeSpan.FromSeconds(15))\n .AddRetry(new RetryStrategyOptions {\n MaxRetryAttempts = 3,\n Delay = TimeSpan.FromMilliseconds(500),\n BackoffType = DelayBackoffType.Exponential,\n ShouldHandle = new PredicateBuilder()\n .Handle()\n .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)\n })\n .AddCircuitBreaker(new CircuitBreakerStrategyOptions {\n FailureRatio = 0.5,\n MinimumThroughput = 5,\n BreakDuration = TimeSpan.FromSeconds(30)\n })\n .Build();\n ```\n - Methods:\n - `Task FileMailAsync(FileEmailRequest req)` → POST /api/v1/MailRouter/file\n - `Task GlobalSearchAsync(string query)` → GET /api/v1/GlobalSearch?q={query}\n - `Task> EmailAddressSearchAsync(string addr, string? entityType)` → GET /api/v1/EmailAddress/search?q={addr}&entityType={type}\n - `Task GetCaseAsync(string id)` → GET /api/v1/Case/{id}\n - `Task GetAccountAsync(string id)` → GET /api/v1/Account/{id}\n - `Task GetContactAsync(string id)` → GET /api/v1/Contact/{id}\n - `Task TestConnectionAsync()` → GET /api/v1/App/user\n - Handle 401 (throw UnauthorizedException), 409 (treat as success for filing)\n\n2. **DTOs** (`Models/` folder):\n - FileEmailRequest: { parentType, parentId, subject, from, to, cc, bcc, body, dateSent, messageId, attachments: [{name, mimeType, contents (base64)}] }\n - FileEmailResponse: { id, status }\n - SearchResult: { list: SearchResultGroup[] } where SearchResultGroup: { name (entity type), list: EspoEntityRef[] }\n - EspoEntityRef: { id, name, entityType }\n - EmailAddressMatch, CaseEntity, AccountEntity, ContactEntity, UserInfo\n\n3. **DpapiCredentialStore** (`Services/DpapiCredentialStore.cs`):\n - Save(username, apiKey): encrypt with ProtectedData.Protect (DataProtectionScope.CurrentUser, entropy = SHA256(\"MarcusLaw.OutlookAddin\")), write to %APPDATA%\\MarcusLaw\\OutlookAddin\\creds.dat\n - Load(): decrypt, return (username, apiKey) tuple or null\n - Delete(): remove file\n\n4. **SettingsManager** (`Services/SettingsManager.cs`):\n - Load(): deserialize from %APPDATA%\\MarcusLaw\\OutlookAddin\\settings.json using System.Text.Json\n - Save(Settings): serialize and write\n - Settings model: { EspoCrmUrl, RecentEntities (List, max 10), WatchedFolders (List), Locale (\"he-IL\"/\"en-US\") }\n\n5. **LoggerFactory** (`Services/LoggerFactory.cs`):\n - Init(): returns ILogger from Serilog.Log.Logger configured with:\n ```csharp\n Log.Logger = new LoggerConfiguration()\n .MinimumLevel.Information()\n .WriteTo.File(\n Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"MarcusLaw\", \"OutlookAddin\", \"logs\", \"addin-.log\"),\n rollingInterval: RollingInterval.Day,\n retainedFileCountLimit: 14,\n outputTemplate: \"{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}\")\n .CreateLogger();\n ```\n\n6. **Unit tests** (`OutlookAddin.Tests/Core/`):\n - EspoCrmClientTests: mock HttpMessageHandler, test happy path + 401 + 409 + network failure + circuit breaker trip\n - DpapiCredentialStoreTests: verify encrypt/decrypt round-trip, file creation\n - SettingsManagerTests: verify JSON serialization with Hebrew strings (RTL test)", + "testStrategy": "Run xUnit tests in OutlookAddin.Tests via `dotnet test`. All tests must pass. Mock HttpMessageHandler to return canned EspoCRM JSON responses. Verify: (1) Polly retries transient failures 3x, (2) circuit breaker opens after 5 consecutive failures, (3) DPAPI round-trip preserves credentials, (4) Settings JSON correctly handles Hebrew UTF-8. Use FluentAssertions for readable assertions.", + "priority": "high", + "dependencies": [ + 1 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 3, + "title": "Implement Feature 1: File existing mail to Case/Contact", + "description": "Build the primary filing feature: Explorer ribbon button, FileToCaseDialog with live search, FilingService that extracts MailItem properties on STA thread and POSTs to EspoCRM, and category stamping on success.", + "details": "1. **Ribbon XML** (`src/OutlookAddin/Ribbon/ExplorerRibbon.xml`):\n - Add button \"File to EspoCRM\" (Hebrew: \"תייק ל-EspoCRM\") in Mail tab, enabled when 1+ MailItems selected\n - Button callback: `FileToCaseDialog_Click`\n\n2. **FileToCaseDialog** (`src/OutlookAddin.UI/Views/FileToCaseDialog.xaml`):\n - WPF Window, 600x500, FlowDirection=RightToLeft\n - Search TextBox with 250ms debounce (use DispatcherTimer)\n - DataGrid grouped by entity type (Case/Contact/Lead/Account)\n - \"Recent\" ListView sidebar (load from SettingsManager.RecentEntities)\n - \"Create new Contact from sender\" button (if sender email not matched)\n - OK/Cancel buttons\n - ViewModel: `FileToCaseViewModel` (CommunityToolkit.Mvvm source generators)\n - `ObservableCollection SearchResults`\n - `RelayCommand SearchCommand` → calls EspoCrmClient.GlobalSearchAsync\n - `RelayCommand SelectCommand` → sets SelectedEntity, closes dialog\n\n3. **FilingService** (`src/OutlookAddin.Core/Services/FilingService.cs`):\n - `Task FileAsync(List items, EspoEntityRef target)`\n - MailItemRef is a DTO with primitives (Subject, SenderEmailAddress, Body, etc.) extracted on STA thread BEFORE passing to FileAsync\n - For each item:\n - Build FileEmailRequest:\n - subject, from, to/cc/bcc (string arrays), body (HTML), dateSent\n - messageId: extract via `mailItem.PropertyAccessor.GetProperty(\"http://schemas.microsoft.com/mapi/proptag/0x1035001F\")` (PR_INTERNET_MESSAGE_ID)\n - Attachments: iterate mailItem.Attachments, SaveAsFile to %TEMP%, read bytes, base64-encode, delete temp file\n - POST to EspoCrmClient.FileMailAsync\n - 200/201: success\n - 409: treat as success (already filed)\n - 401: throw UnauthorizedException → triggers re-auth dialog in UI layer\n - Other error: enqueue in DiskRetryQueue (see below)\n - After all items: marshal back to STA thread (via captured SynchronizationContext), stamp MailItem.Categories += \"Filed: {target.name}\"\n - Create category if not exists: `application.Session.Categories.Add(\"Filed: {name}\", OlCategoryColor.olCategoryColorBlue)`\n - Return FilingResult: { SuccessCount, FailedCount, QueuedCount }\n\n4. **DiskRetryQueue** (`src/OutlookAddin.Core/Services/DiskRetryQueue.cs`):\n - Enqueue(FileEmailRequest, target): serialize to %LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\retry\\{guid}.json\n - ProcessQueue(): background task (30s interval), retry all queued items, delete on success\n - Max 3 retries per item, then move to failed/ subfolder\n\n5. **Toast notification** (use Windows.UI.Notifications via Microsoft.Toolkit.Uwp.Notifications NuGet):\n - Show summary: \"Filed 3 of 4 emails. 1 queued for retry.\"\n\n6. **Wire up in ThisAddIn.cs**:\n - Composition root: instantiate FilingService, DiskRetryQueue, start queue processor\n - Ribbon callback: capture selected MailItems on STA, extract primitives, show FileToCaseDialog, on OK call FilingService.FileAsync, show toast\n\n7. **Tests** (`OutlookAddin.Tests/Core/FilingServiceTests.cs`):\n - Mock EspoCrmClient, verify FileMailAsync called with correct payload\n - Test 401 handling (throw UnauthorizedException)\n - Test 409 handling (count as success)\n - Test retry queue enqueue on network failure", + "testStrategy": "Manual: F5 in VS, Outlook launches with add-in. Select 1-2 emails, click \"File to EspoCRM\", search for a case, confirm. Verify: (1) dialog shows search results, (2) email filed successfully, (3) category stamped, (4) toast displayed. Unit tests: verify FilingService logic with mocked HTTP client. Check logs in %LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\logs\\ for request/response details.", + "priority": "high", + "dependencies": [ + 2 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 4, + "title": "Implement Feature 2: Reading-pane sidebar with sender matching", + "description": "Build CustomTaskPane that displays matched Case/Contact for the selected email's sender, with live lookup via EspoCRM EmailAddress/search API and in-memory caching.", + "details": "1. **CustomTaskPane registration** (in `ThisAddIn.cs`):\n - In ThisAddIn_Startup, for each Explorer window:\n ```csharp\n var explorer = Globals.ThisAddIn.Application.ActiveExplorer();\n var wpfView = new ReadingPaneView(); // WPF UserControl\n var winFormsHost = new System.Windows.Forms.Integration.ElementHost { Child = wpfView, Dock = DockStyle.Fill };\n var taskPane = CustomTaskPanes.Add(winFormsHost, \"Case Info\", explorer);\n taskPane.DockPosition = Office.MsoCTPDockPosition.msoCTPDockPositionRight;\n taskPane.Width = 320;\n taskPane.Visible = true;\n ```\n\n2. **ReadingPaneView** (`src/OutlookAddin.UI/Views/ReadingPaneView.xaml`):\n - WPF UserControl, FlowDirection=RightToLeft\n - Bound to `ReadingPaneViewModel`\n - States:\n - Loading: ProgressRing\n - Match found: Card with contact name, account name, recent cases (max 5), buttons: \"Open in EspoCRM\" (deep link), \"File to this\", \"File elsewhere\"\n - No match: \"No match found\" + inline search TextBox\n - Error: error message\n\n3. **ReadingPaneViewModel** (`src/OutlookAddin.UI/ViewModels/ReadingPaneViewModel.cs`):\n - `ObservableProperty` for MatchResult (ContactEntity + AccountEntity + List)\n - `RelayCommand FileToThisCommand` → triggers FilingService with pre-selected case\n - `RelayCommand FileElsewhereCommand` → opens FileToCaseDialog\n - `RelayCommand OpenInEspoCrmCommand` → `Process.Start(\"https://crm.../#Case/view/{id}\")`\n\n4. **ExplorerSelectionHandler** (`src/OutlookAddin/Handlers/ExplorerSelectionHandler.cs`):\n - Subscribe to Explorer.SelectionChange\n - Debounce 200ms (use System.Threading.Timer)\n - On timer fire: marshal to STA, get Selection.Item(1) as MailItem, extract SenderEmailAddress (primitive string)\n - Pass to MatchingService.LookupAsync on threadpool\n\n5. **MatchingService** (`src/OutlookAddin.Core/Services/MatchingService.cs`):\n - `Task LookupAsync(string senderEmail)`:\n 1. Check MemoryCache (key = senderEmail, expiration = 10 min)\n 2. If miss:\n a. GET /api/v1/EmailAddress/search?q={senderEmail}&entityType=Contact\n b. If exactly 1 hit:\n - GET /api/v1/Contact/{id}?select=name,accountId\n - If accountId: GET /api/v1/Account/{accountId}?select=name\n - GET /api/v1/Case?where[linkedWith]=contacts:{id}&maxSize=5&orderBy=createdAt&order=desc\n c. Build MatchResult, cache, return\n 3. If 0 or 2+ hits: return null\n\n6. **Wire up**:\n - In ThisAddIn_Startup: instantiate MatchingService, ExplorerSelectionHandler\n - ExplorerSelectionHandler updates ReadingPaneViewModel on lookup completion\n\n7. **Tests** (`OutlookAddin.Tests/Core/MatchingServiceTests.cs`):\n - Mock EspoCrmClient, verify correct API sequence\n - Test cache hit (2nd call should not hit HTTP)\n - Test 0 matches, 1 match, 2+ matches (ambiguous)", + "testStrategy": "Manual: F5, open Outlook, click on an email in inbox. Verify: (1) sidebar appears on right, (2) after 200ms, displays contact/case info if sender is known, (3) \"Open in EspoCRM\" link works, (4) \"File to this\" triggers filing. Unit tests: verify MatchingService cache behavior, API call sequence. Use Moq to verify MemoryCache.Set called with 10min expiration.", + "priority": "medium", + "dependencies": [ + 3 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 5, + "title": "Implement Feature 3: Compose from Case (bidirectional)", + "description": "Enable composing emails from Outlook linked to EspoCRM Case (Inspector ribbon button + outlookaddin:// URL protocol), and auto-file on send via ItemSend handler with Sent Items watcher for IMAP latency.", + "details": "1. **Inspector Ribbon** (`src/OutlookAddin/Ribbon/InspectorRibbon.xml`):\n - Button \"Compose from Case\" (Hebrew: \"כתוב מתיק\"), visible only on MailItem compose windows (not read/reply)\n - Callback: `ComposeFromCase_Click`\n\n2. **ComposeFromCaseDialog** (`src/OutlookAddin.UI/Views/ComposeFromCaseDialog.xaml`):\n - Similar to FileToCaseDialog but filter results to entityType=Case only\n - On OK: returns selected CaseEntity (id, name, number, primary contact email)\n\n3. **Compose logic** (in Inspector ribbon callback or URL protocol handler):\n - GET /api/v1/Case/{id}?select=name,number,contactsIds,accountId\n - If contactsIds[0] exists: GET /api/v1/Contact/{contactsIds[0]}?select=emailAddress\n - Set MailItem properties on STA thread:\n ```csharp\n mailItem.To = contactEmail;\n mailItem.Subject = $\"[{caseNumber}] \";\n mailItem.HTMLBody += $\"

תיק: {caseName}

\";\n ```\n - Stamp UserProperty (custom MAPI property):\n ```csharp\n var prop = mailItem.UserProperties.Add(\"EspoCaseId\", Outlook.OlUserPropertyType.olText, false);\n prop.Value = caseId;\n var guidProp = mailItem.UserProperties.Add(\"OutlookAddinGuid\", Outlook.OlUserPropertyType.olText, false);\n guidProp.Value = Guid.NewGuid().ToString(); // for dedup in Sent Items watcher\n ```\n\n4. **URL protocol handler** (`src/OutlookAddinProtocolHandler/`):\n - New console app project (net48): OutlookAddinProtocolHandler.csproj\n - Entry point parses `outlookaddin://compose?caseId=`\n - Writes JSON message to named pipe `\\\\.\\pipe\\MarcusLaw.OutlookAddin`: `{ \"action\": \"compose\", \"caseId\": \"\" }`\n - Add to OutlookAddin.sln\n\n5. **Named pipe listener** (in `ThisAddIn.cs`):\n - In ThisAddIn_Startup: spawn background thread (or Task.Run loop) with NamedPipeServerStream\n - On message received: marshal to STA, trigger compose flow\n\n6. **Registry setup** (in ClickOnce installer or manual via .reg file):\n - HKCU\\Software\\Classes\\outlookaddin\n - (Default) = \"URL:OutlookAddin Protocol\"\n - URL Protocol = \"\"\n - HKCU\\Software\\Classes\\outlookaddin\\shell\\open\\command\n - (Default) = \"\\\"C:\\\\Users\\\\\\\\AppData\\\\Local\\\\Apps\\\\OutlookAddinProtocolHandler.exe\\\" \\\"%1\\\"\"\n\n7. **ItemSend handler**:\n - Subscribe Application.ItemSend\n - Read MailItem.UserProperties[\"EspoCaseId\"] and [\"OutlookAddinGuid\"]\n - If EspoCaseId present:\n - Let send complete (Cancel = false)\n - Subscribe to Sent Items folder's Items.ItemAdd:\n ```csharp\n var sentItems = application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail);\n sentItems.Items.ItemAdd += SentItemsWatcher;\n ```\n - In SentItemsWatcher: check if newly added MailItem has matching OutlookAddinGuid\n - If match found (within 60s timeout): extract primitives, call FilingService.FileAsync\n - Unsubscribe after match or timeout\n\n8. **Tests** (`OutlookAddin.Tests/Integration/`):\n - Mock named pipe message parsing\n - Unit test UserProperty stamping logic (hard to test without real Outlook COM)", + "testStrategy": "Manual: (1) Outlook compose window, click \"Compose from Case\", select a case, verify To/Subject/footer populated and UserProperty stamped. (2) Send email, wait 10s, check if it auto-files to the case (check EspoCRM via API or logs). (3) Test URL protocol: run `start outlookaddin://compose?caseId=` in cmd, verify Outlook compose window opens with case pre-filled. Unit tests: verify UserProperty stamping logic with mocked MailItem (if feasible).", + "priority": "medium", + "dependencies": [ + 3 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 6, + "title": "Implement Feature 4: Folder auto-sync with confidence-based filing", + "description": "Build folder watching system with TreeView settings UI, per-folder configuration (auto-file mode, confidence threshold), FolderWatcher singleton that subscribes to Items.ItemAdd with static field references to prevent GC, and toast notifications for manual review.", + "details": "1. **SettingsDialog** (`src/OutlookAddin.UI/Views/SettingsDialog.xaml`):\n - TabControl with tabs: General, Folders, About\n - **Folders tab**:\n - TreeView mirroring Outlook folder hierarchy (use Application.Session.Folders recursively)\n - Each node: checkbox (watched or not), if checked:\n - ComboBox: \"Default parent\" (nullable, search via entity picker)\n - ComboBox: \"Mode\" (Auto-file when confident / Notify only)\n - Slider: \"Confidence threshold\" (0.0-1.0, default 0.8)\n - Save button → persist to SettingsManager.Settings.WatchedFolders\n\n2. **WatchedFolder model** (`src/OutlookAddin.Core/Models/WatchedFolder.cs`):\n ```csharp\n public class WatchedFolder {\n public string StoreId { get; set; } // Store.StoreID\n public string FolderPath { get; set; } // e.g., \"\\\\Inbox\\\\Clients\"\n public string? DefaultParentId { get; set; }\n public string? DefaultParentType { get; set; }\n public WatchMode Mode { get; set; } // AutoFile | NotifyOnly\n public double ConfidenceThreshold { get; set; } = 0.8;\n }\n public enum WatchMode { AutoFile, NotifyOnly }\n ```\n\n3. **FolderWatcher singleton** (`src/OutlookAddin/Services/FolderWatcher.cs`):\n - **CRITICAL:** Store Items references in a STATIC field to prevent GC unsubscribe:\n ```csharp\n private static readonly List _itemsRefs = new();\n ```\n - `Initialize(List folders)`:\n - For each folder:\n - Resolve Folder via GetFolderFromPath(StoreId, FolderPath)\n - Get Items collection: `folder.Items`\n - Add to _itemsRefs\n - Subscribe Items.ItemAdd += OnItemAdd\n - `OnItemAdd(object item)`:\n - Cast to MailItem\n - Capture EntryID immediately (string primitive)\n - Check HashSet _processedEntryIds for dedup\n - Queue work to STA-bound TaskScheduler (use TaskScheduler.FromCurrentSynchronizationContext captured at init)\n - Extract SenderEmailAddress\n - Call MatchingService.LookupAsync(sender)\n - If exactly 1 match + confidence >= threshold + mode == AutoFile:\n - Extract MailItem primitives\n - Call FilingService.FileAsync with default parent or matched entity\n - Else:\n - Set MailItem.Categories += \"Needs filing\" (on STA)\n - Show toast: \"New email needs manual filing: {subject}\"\n\n4. **GetFolderFromPath helper**:\n - Recursively traverse Application.Session.Stores[storeId].GetRootFolder().Folders\n - Match by FolderPath (split by \\\\)\n\n5. **Wire up in ThisAddIn.cs**:\n - In ThisAddIn_Startup: instantiate FolderWatcher, load settings, call Initialize\n - On settings change: call FolderWatcher.Reinitialize (unsubscribe old, subscribe new)\n\n6. **Toast notifications** (Microsoft.Toolkit.Uwp.Notifications):\n - Template: \"OutlookAddin: New email in {folderName} needs filing. Subject: {subject}. Click to open.\"\n - Toast click action: activate Outlook and select the MailItem by EntryID\n\n7. **Tests** (`OutlookAddin.Tests/Integration/FolderWatcherTests.cs`):\n - Hard to unit test without real Outlook COM\n - Integration test: verify dedup logic (HashSet), verify ItemAdd callback triggers\n - Mock MatchingService, verify auto-file vs notify logic", + "testStrategy": "Manual: F5, open SettingsDialog → Folders tab, check a folder (e.g., Inbox\\Clients), set mode=Auto-file, threshold=0.8. Send a test email to that folder from a known contact. Verify: (1) MatchingService lookup triggered, (2) if 1 match with confidence >= 0.8, email auto-filed, (3) else, category \"Needs filing\" stamped + toast shown. Check logs for ItemAdd events. Integration test: verify Items reference in static field prevents GC (can't be fully unit tested, rely on manual testing).", + "priority": "medium", + "dependencies": [ + 4 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 7, + "title": "Implement Hebrew/English localization with RTL theme support", + "description": "Build LocalizationManager with resource files (Strings.he.resx, Strings.en.resx), XAML markup extension for bindings, RTL themes for all WPF views, and Hebrew font stack.", + "details": "1. **Resource files** (`src/OutlookAddin.UI/Resources/`):\n - `Strings.he.resx`: Hebrew strings for all UI text (dialog titles, button labels, error messages, ribbon labels, toast titles)\n - Examples:\n - FileToCaseDialog_Title = \"תייק מייל לתיק\"\n - SearchPlaceholder = \"חפש תיק, לקוח, איש קשר...\"\n - FileButton = \"תייק\"\n - CancelButton = \"ביטול\"\n - ErrorFileEmail = \"שגיאה בתיוק המייל: {0}\"\n - `Strings.en.resx`: English equivalents\n - Set `Strings.resx` as default (fallback to English)\n\n2. **LocalizationManager** (`src/OutlookAddin.UI/Services/LocalizationManager.cs`):\n - Singleton holding current CultureInfo\n - `string Get(string key)`: returns localized string from Strings.resx via ResourceManager\n - `void SetCulture(CultureInfo culture)`: changes current culture, raises PropertyChanged to rebind all UI\n - Default culture: `CultureInfo.CurrentUICulture` (OS default, usually he-IL in Israel)\n - Override via SettingsManager.Settings.Locale (\"he-IL\" / \"en-US\"), requires restart\n\n3. **XAML Markup Extension** (`src/OutlookAddin.UI/Markup/LocExtension.cs`):\n ```csharp\n [MarkupExtensionReturnType(typeof(string))]\n public class LocExtension : MarkupExtension {\n public string Key { get; set; }\n public override object ProvideValue(IServiceProvider serviceProvider) {\n return LocalizationManager.Instance.Get(Key);\n }\n }\n ```\n - Usage in XAML: ``\n\n4. **RTL Themes** (`src/OutlookAddin.UI/Themes/RtlFonts.xaml`):\n - ResourceDictionary with styles:\n ```xaml\n \n \n ```\n - Merge in App.xaml (or each view's Resources)\n\n5. **Apply to all views**:\n - FileToCaseDialog.xaml, ComposeFromCaseDialog.xaml, SettingsDialog.xaml, ReadingPaneView.xaml: replace hardcoded strings with `{l:Loc Key=...}`\n - Set `FlowDirection=\"RightToLeft\"` on root element (or inherit from theme)\n\n6. **Ribbon localization**:\n - Ribbon XML supports `getLabel` callback attribute\n - Implement dynamic label callback in ribbon code-behind:\n ```csharp\n public string GetFileToEspoCrmLabel(Office.IRibbonControl control) {\n return LocalizationManager.Instance.Get(\"Ribbon_FileToEspoCrm\");\n }\n ```\n - Add to Strings.resx: `Ribbon_FileToEspoCrm = \"תייק ל-EspoCRM\"` (he), `\"File to EspoCRM\"` (en)\n\n7. **Toast localization**:\n - Wrap toast messages with `LocalizationManager.Get(\"Toast_FilingSuccess\", successCount, queuedCount)`\n - Use String.Format for placeholders\n\n8. **Test both locales**:\n - Add Settings → General tab → \"Language\" ComboBox (he-IL / en-US)\n - On change: SettingsManager.Save, show message \"Restart Outlook to apply\"\n - Manual testing in both locales\n\n9. **Update tests**:\n - Verify Strings.he.resx contains all keys (no missing translations)\n - Unit test LocalizationManager.Get with he-IL and en-US cultures", + "testStrategy": "Manual: F5 with OS locale = he-IL, verify all dialogs, ribbons, toasts display Hebrew text, RTL layout, correct font. Change Settings → Language to en-US, restart Outlook, verify English text, LTR layout. Unit test: set Thread.CurrentThread.CurrentUICulture = new CultureInfo(\"he-IL\"), verify LocalizationManager.Get(\"SearchPlaceholder\") returns Hebrew string. Verify all 4 features (Tasks 3-6) render correctly in Hebrew.", + "priority": "medium", + "dependencies": [ + 3, + 4, + 5, + 6 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 8, + "title": "Finalize ClickOnce publish pipeline with code signing and automated deployment", + "description": "Complete the .gitea/workflows/build.yml publish job: invoke mage.exe to generate ClickOnce manifests, sign with signtool using cert from Infisical, rsync to platform.dev nginx, post Mattermost notification, and tag first release v1.0.0.", + "details": "1. **Update .gitea/workflows/build.yml publish job** (replace TODOs):\n\n a. **ClickOnce publish step**:\n ```yaml\n - name: Publish ClickOnce\n run: |\n $publishDir = \"publish\"\n $appFiles = \"src/OutlookAddin/bin/Release\"\n \n # Copy binaries to publish/Application Files/OutlookAddin_1_0_0_0/\n $version = \"1.0.0.0\" # TODO: extract from AssemblyInfo or tag\n $versionDir = \"$publishDir/Application Files/OutlookAddin_$($version.Replace('.', '_'))\"\n New-Item -ItemType Directory -Force -Path $versionDir\n Copy-Item -Recurse \"$appFiles/*\" $versionDir\n \n # Create application manifest\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -New Application `\n -ToFile \"$versionDir\\MarcusLaw.OutlookAddin.dll.manifest\" `\n -Name \"OutlookAddin\" `\n -Version $version `\n -FromDirectory $versionDir `\n -TrustLevel FullTrust\n \n # Sign application manifest\n $thumb = \"${{ secrets.CODESIGN_THUMBPRINT }}\"\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -Sign \"$versionDir\\MarcusLaw.OutlookAddin.dll.manifest\" `\n -CertHash $thumb `\n -TimeStampUri http://timestamp.digicert.com\n \n # Create deployment manifest (.application)\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -New Deployment `\n -ToFile \"$publishDir\\OutlookAddin.application\" `\n -Name \"Marcus-Law Outlook Add-in\" `\n -Version $version `\n -AppManifest \"$versionDir\\MarcusLaw.OutlookAddin.dll.manifest\" `\n -Install true `\n -Publisher \"Marcus-Law\" `\n -ProviderUrl \"https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.application\" `\n -UpdateEnabled true `\n -UpdateMode Foreground `\n -UpdateInterval 7 `\n -UpdateUnit days\n \n # Sign deployment manifest\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\mage.exe\" `\n -Sign \"$publishDir\\OutlookAddin.application\" `\n -CertHash $thumb `\n -TimeStampUri http://timestamp.digicert.com\n \n # Create setup.exe bootstrapper\n & \"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\x86\\setupbuilder.exe\" `\n -out \"$publishDir\\setup.exe\" `\n -appManifest \"$publishDir\\OutlookAddin.application\" `\n -prereqPackage Microsoft.Net.Framework.4.8\n ```\n\n b. **Upload to platform.dev step**:\n ```yaml\n - name: Upload to platform.dev\n env:\n SSH_KEY: ${{ secrets.PLATFORM_DEV_SSH_KEY }}\n run: |\n # Write SSH key to file\n $sshKeyPath = \"$env:TEMP\\platform_ssh_key\"\n [System.IO.File]::WriteAllBytes($sshKeyPath, [System.Convert]::FromBase64String($env:SSH_KEY))\n \n # rsync via WSL or use WinSCP (if available on runner)\n # Assuming WSL is installed on runner:\n wsl rsync -avz --delete -e \"ssh -i /mnt/c/Users/runneradmin/AppData/Local/Temp/platform_ssh_key -o StrictHostKeyChecking=no\" `\n ./publish/ `\n root@platform.dev.marcus-law.co.il:/var/www/outlook-addin/\n \n Remove-Item $sshKeyPath\n ```\n\n c. **Notify Mattermost step**:\n ```yaml\n - name: Notify Mattermost\n if: success()\n run: |\n # Fetch webhook URL from Infisical /mattermost/WEBHOOK_RELEASES (or hardcode as secret)\n $webhookUrl = \"${{ secrets.MATTERMOST_WEBHOOK_RELEASES }}\"\n $payload = @{\n text = \"✅ OutlookAddin ${{ github.ref_name }} released: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\"\n channel = \"git-verelasim\"\n } | ConvertTo-Json\n Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $payload -ContentType \"application/json\"\n ```\n\n2. **Configure platform.dev nginx**:\n - SSH to platform.dev.marcus-law.co.il\n - Add nginx config `/etc/nginx/sites-available/outlook-addin.conf`:\n ```nginx\n server {\n listen 443 ssl http2;\n server_name platform.dev.marcus-law.co.il;\n \n location /outlook-addin/ {\n alias /var/www/outlook-addin/;\n types {\n application/x-ms-application application;\n application/x-ms-manifest manifest;\n application/octet-stream vsto;\n application/octet-stream deploy;\n }\n add_header Cache-Control \"no-cache, must-revalidate\";\n }\n }\n ```\n - `sudo systemctl reload nginx`\n\n3. **Tag first release**:\n - After publish job succeeds, manually:\n ```bash\n git tag v1.0.0\n git push origin v1.0.0\n ```\n - Or automate in workflow: add step to create GitHub/Gitea release with release notes\n\n4. **Test end-to-end**:\n - On a clean Windows VM (not dev machine), navigate to https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n - Run setup.exe, verify ClickOnce installer prompts for install\n - Verify certificate is trusted (no security warning, because GPO pushed cert to Trusted Publishers)\n - After install, open Outlook, verify add-in loads\n - Check for auto-update: close Outlook, wait 7 days (or manually trigger update check in ClickOnce settings)\n\n5. **Document**:\n - Update docs/IT-SETUP.md with:\n - GPO cert deployment steps (copy .cer to \\\\\\domain\\SYSVOL\\...\\Scripts\\certs\\\\ + Group Policy → Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies → Trusted Publishers + Trusted Root CA)\n - Verify cert installed: `certutil -store -user TrustedPublisher` should list \"Marcus-Law OutlookAddin\"\n\n6. **Add secrets to Gitea Actions**:\n - Go to repo Settings → Secrets\n - Add: CODESIGN_THUMBPRINT, PLATFORM_DEV_SSH_KEY, MATTERMOST_WEBHOOK_RELEASES\n - Values from Infisical `/outlook-addin` and `/mattermost`", + "testStrategy": "1. Push a commit to main branch, verify build job succeeds. 2. Create and push tag v1.0.0, verify publish job runs and completes without errors. 3. Check platform.dev.marcus-law.co.il/outlook-addin/ via browser, verify setup.exe and .application files exist. 4. On a Windows VM with GPO cert, run setup.exe, verify no security warnings, Outlook add-in installs successfully. 5. Check Mattermost #git-verelasim channel for release notification. 6. Logs: check Gitea Actions logs for each step's output, verify signtool and mage.exe succeed.", + "priority": "high", + "dependencies": [ + 1, + 7 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 9, + "title": "Create documentation, coordinate IT rollout, and provide user onboarding", + "description": "Write ONBOARDING.md (Hebrew, lawyer-facing) and IT-SETUP.md (admin), record demo video, coordinate GPO cert push with IT, generate per-user API keys in EspoCRM, distribute install links + credentials, and provide 48h office hours.", + "details": "1. **docs/ONBOARDING.md** (Hebrew):\n - Target audience: lawyers (non-technical)\n - Sections:\n - מה זה התוסף? (what is the add-in)\n - דרישות מערכת (system requirements: Win10/11, Office 2019/2021/M365, .NET 4.8)\n - התקנה:\n 1. לחץ על הלינק: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n 2. אשר את ההתקנה (ClickOnce)\n 3. הפעל מחדש את Outlook\n 4. בהפעלה הראשונה: הזן username + API key (שתקבל במייל)\n - שימוש:\n - **תיוק מייל קיים**: בחר מייל → לחץ \"תייק ל-EspoCRM\" ברצועת הכלים → חפש תיק/לקוח → אשר\n - **פרטי תיק בסרגל צד**: בחר מייל → הסרגל מימין מציג את התיק/לקוח המקושר\n - **כתיבת מייל מתיק**: חלון חדש של מייל → \"כתוב מתיק\" → בחר תיק → המייל יתויק אוטומטית בשליחה\n - **סנכרון תיקיות**: הגדרות → תיקיות → סמן תיקיות לניטור → בחר מצב (אוטומטי/ידני)\n - פתרון בעיות:\n - שגיאת 401: הזן מחדש API key (הגדרות → חיבור)\n - המייל לא התויק: בדוק לוגים ב-%LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\logs\\\\ + פנה לתמיכה\n - התוסף לא נטען: בדוק File → Options → Add-ins → Manage COM Add-ins → ודא ש-MarcusLaw.OutlookAddin מסומן\n\n2. **docs/IT-SETUP.md** (admin, English + Hebrew notes):\n - Target audience: IT admin\n - Sections:\n - Code-signing certificate deployment:\n 1. Export public cert (.cer) from codesign.pfx: `openssl pkcs12 -in codesign.pfx -nokeys -out codesign.cer`\n 2. Copy codesign.cer to \\\\\\domain.local\\SYSVOL\\domain.local\\Scripts\\certs\\\\\n 3. Group Policy: Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies:\n - Trusted Publishers: Import codesign.cer\n - Trusted Root Certification Authorities: Import codesign.cer (if self-signed)\n 4. Run `gpupdate /force` on all 10 lawyer machines\n 5. Verify: `certutil -store -user TrustedPublisher` should list \"Marcus-Law OutlookAddin\"\n - EspoCRM API key provisioning:\n 1. Admin → API Users → Create user for each lawyer (username = email prefix, role = Regular User + API User)\n 2. Generate API key per user: User Profile → API Key → Generate\n 3. Send per-user email with:\n - Install link: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n - Username: \n - API Key: \n - Monitoring:\n - Check Mattermost #תשתיות-ופריסה for deployment notifications\n - Logs on client machines: %LOCALAPPDATA%\\MarcusLaw\\OutlookAddin\\logs\\\\\n - Server logs: EspoCRM data/logs/ (MailRouter endpoint calls)\n\n3. **Demo video** (5 minutes, screen capture):\n - Tools: OBS Studio or Windows Game Bar\n - Script:\n 0:00-0:30 — Intro: \"שלום, זה תוסף EspoCRM ל-Outlook. נראה ארבעת הפיצ'רים העיקריים.\"\n 0:30-1:30 — Feature 1: תיוק מייל קיים (select email, click ribbon, search, file)\n 1:30-2:30 — Feature 2: סרגל צד (select email, show sidebar with case info, click \"File to this\")\n 2:30-3:30 — Feature 3: כתיבת מייל מתיק (compose, \"Compose from Case\", send, auto-file)\n 3:30-4:30 — Feature 4: סנכרון תיקיות (Settings → Folders, check folder, demo auto-file)\n 4:30-5:00 — Outro: \"שאלות? office-hours או תמיכה@marcus-law.co.il\"\n - Upload to internal file server or Mattermost\n\n4. **Coordinate with IT**:\n - Email IT admin (or Mattermost DM) 1 week before rollout:\n - Request GPO cert push to all 10 lawyer machines\n - Provide codesign.cer + docs/IT-SETUP.md\n - Verify cert installed on all machines before sending install links\n\n5. **Generate API keys**:\n - EspoCRM Admin panel:\n - For each lawyer: Administration → Users → create API User (if not exists)\n - Or: per-user API key via User Profile → API → Generate Key\n - Store keys in Infisical `/outlook-addin/users/` (or send via secure 1-time link)\n\n6. **Distribute install link + credentials**:\n - After GPO cert confirmed on all machines:\n - Send individual emails to 10 lawyers:\n Subject: התקנת תוסף EspoCRM ל-Outlook\n Body:\n שלום [שם],\n \n נא להתקין את תוסף EspoCRM ל-Outlook:\n 1. לחץ כאן: https://platform.dev.marcus-law.co.il/outlook-addin/setup.exe\n 2. בהפעלה הראשונה הזן:\n Username: [email-prefix]\n API Key: [key]\n \n מדריך מלא: [link to ONBOARDING.md]\n וידאו הדגמה: [link to demo video]\n \n שעות פתוחות ליום ראשון + שני: 9:00-17:00 ב-#תמיכה-טכנית\n\n7. **Office hours**:\n - Block 48 hours (e.g., Sunday + Monday) for support\n - Announce in Mattermost #תמיכה-טכנית: \"התוסף זמין להתקנה! זמינים לשאלות היום ומחר.\"\n - Prepare FAQ doc with common issues (401 error, add-in not loading, etc.)\n - Monitor logs via FileBrowser or SSH to dev server\n\n8. **Post-rollout**:\n - Collect feedback via Mattermost poll or Google Form\n - Log any bugs as Task Master tasks\n - Plan follow-up release (v1.1.0) with fixes + UX improvements", + "testStrategy": "1. Peer review ONBOARDING.md + IT-SETUP.md with 1-2 lawyers and IT admin, incorporate feedback. 2. Record demo video, send to 2 lawyers for feedback on clarity. 3. Dry-run: install on 1 test machine (not dev machine) using the exact email + install link flow, verify no issues. 4. After GPO cert push, verify on 2 lawyer machines that cert is in TrustedPublisher store. 5. During office hours: track # of installs, # of support requests, # of successful filings (via EspoCRM logs or Mattermost bot). 6. Post-48h: survey lawyers for feedback, measure adoption rate (# active users / 10).", + "priority": "high", + "dependencies": [ + 8 + ], + "status": "pending", + "subtasks": [] + } + ], + "metadata": { + "created": "2026-05-11T09:13:47.068Z", + "updated": "2026-05-11T09:13:47.068Z", + "description": "Tasks for master context" + } + } +} \ No newline at end of file