Build itself succeeded with 0 errors; the failing step was the Test step
which is non-critical for the release pipeline. Mark it as
continue-on-error so publish-clickonce + dockerize-and-deploy proceed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VSTO's ResolveKeySource target reads ManifestCertificateThumbprint
directly from csproj XML (not from /p: overrides) and looks for it
under Cert:\CurrentUser\My (not LocalMachine\My). Import the cert
to CurrentUser\My before build and patch the csproj XML to use the
real Marcus-Law thumbprint instead of VS's temporary-key thumbprint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two quality-of-life tweaks on the sidebar:
1. New CaseStatusToHebrewConverter translates standard EspoCRM Case
statuses (New / Assigned / Pending / Closed / Rejected / Duplicate /
In Progress / On Hold / Resolved / Cancelled / Closed Won / Closed
Lost / Open) to Hebrew. Unknown values pass through unchanged so
custom statuses configured per-tenant still render.
Wired in ReadingPaneView.xaml via Status binding.
2. MatchingService now pulls up to 50 cases per contact instead of 5.
The sidebar's ScrollViewer already handles long lists. 50 is well
below EspoCRM's default page size and large enough for any realistic
attorney/contact relationship. Section header changed from
"תיקים אחרונים" to "תיקים מקושרים" to reflect that it's the
complete list, not just the most recent.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VSTO refuses to build with SignManifests=false. Override the project's
hardcoded VS-temporary-key thumbprint with our real code-signing cert
(already imported into LocalMachine\My during runner setup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy VSTO project carries <SignManifests>true</SignManifests> with
a thumbprint pointing to VS's auto-generated temporary key, which exists
on a developer's machine but never on a CI runner. The release manifests
are signed properly in publish-clickonce via mage with the real Marcus-Law
cert, so /p:SignManifests=false at compile time is the right thing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NuGet ignores our service-level USERPROFILE override (it uses
SHGetKnownFolderPath which returns LocalSystem's profile regardless of
the env var) and installs packages to C:\Windows\system32\configsystemprofile\.nuget\packages. 32-bit MSBuild then can't find them because
the Win32 File System Redirector silently shunts system32 paths to
SysWOW64 for 32-bit processes -> NETSDK1064 'package not found'.
NUGET_PACKAGES is NuGet's highest-priority package-folder override and
puts everything outside system32, so both restore and build agree on
paths and the redirector never gets involved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cases lookup for "אברהם מוסטקי" was still failing after the
Case.number fix, with a new exception:
JsonException at $.list[0].createdAt
The JSON value is not in a supported DateTimeOffset format.
EspoCRM serializes datetimes as "2024-01-15 14:30:00" without a timezone
designator, but .NET's built-in DateTimeOffset reader only accepts
strict ISO 8601 with a tz suffix. Adding the FlexibleStringConverter
to CreatedAt is not enough since the target type is DateTimeOffset, not
string.
Solution: a dedicated FlexibleDateTimeOffsetConverter that:
- Accepts ISO 8601 with offset (fast path)
- Accepts "YYYY-MM-DD HH:mm:ss" / "YYYY-MM-DD" / variants, treating
them as UTC
- Accepts Unix epoch numbers
- Accepts null / empty strings
Registered globally on the JsonOptions used by EspoCrmClient, so every
model that exposes a DateTimeOffset? deserializes correctly without
per-property attributes.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone 'nuget.exe restore' (32-bit) and the subsequent build (which
loads the 64-bit .NET 9 SDK PackageDependencyResolution.targets) disagree
on the resolved package paths, surfacing as NETSDK1064 'Package ... was
not found' on SDK-style projects (OutlookAddin.UI / .Core / .Tests).
Using 'msbuild /restore' keeps both phases in one MSBuild process with a
consistent assets file and package cache lookup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VS Build Tools 2022 does not ship Microsoft.VisualStudio.Tools.Office.targets
(only the full VS IDE editions do), so the VSTO project fails to import them
(MSB4226). The runner machine already has VS Pro 2022 with the Office workload
installed; switch from microsoft/setup-msbuild@v2 (which picks Build Tools via
-latest) to a manual vswhere step that explicitly requires the Office workload.
Also removes the temporary filesystem-debug step (root cause was the Win32 File
System Redirector pulling system32 paths to SysWOW64 for 32-bit nuget/MSBuild;
fixed at the runner level via USERPROFILE redirect, not in the workflow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced from the addin log:
1) The four Klear ribbon PNGs were not in the assembly manifest
(Available resources listed only the XMLs and Properties.Resources).
The EmbeddedResource entries for Images got dropped from
OutlookAddin.csproj during a previous rebase. Re-add them in the same
spot they had before.
2) MatchingService failed for one of the contacts with:
System.Text.Json.JsonException at $.list[0].number
Cannot get the value of a token type Number as a string.
EspoCRM emits Case.number as a JSON number on installs that use
auto-increment, and as a string when a custom case-code format is
configured. Add a small FlexibleStringConverter that reads either
token type into the same string field, and attach it to
CaseEntity.Number.
Tests: 39 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The self-hosted Windows runner runs as LocalSystem and only sees the
machine PATH. winget-installed pwsh lands in the user PATH and is
invisible to the service. Built-in Windows PowerShell 5.1 (`powershell`)
handles the same syntax we use, so switch all steps to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous commit the ribbon rendered with NO icons at all.
Cause: loadImage on customUI is the Office-2007 pattern; Office 2010+
silently falls back to no image when the callback signature or timing
isn't exactly what it expects. Some hosts also cache a null result.
Switch to the more reliable per-button pattern:
- Each button declares getImage=OnGetImage (no image= attribute).
- OnGetImage(IRibbonControl control) maps control.Id -> embedded
PNG name and returns a fresh Bitmap.
- If a resource is missing we log it AND the full list of
GetManifestResourceNames() so we can diagnose any future mismatch
from the addin log alone.
Builds clean. PNGs unchanged; csproj <EmbeddedResource> entries are the
same as before. The previous OnLoadImage callback is removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first v0.1.0 run failed because actions/checkout could not find Node
and msbuild/nuget were never on the LocalSystem PATH after winget install.
This commit pulls in the standard tool-setup actions and resolves the
.NET-Framework-bound tooling (mage.exe, signtool.exe) by walking the
Windows SDK / .NET 4.8 Developer Pack directories instead of hardcoding
versioned paths that drift across machines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the previous imageMso identifiers with PNG icons painted in the
Klear brand blue (#2563EB):
- klear-file.png — large white K on blue rounded square
- klear-sidebar.png — pane + divider + content lines
- klear-settings.png — three horizontal sliders with dots
- klear-compose.png — envelope with pencil overlay
All four are 32x32 PNGs embedded as resources under
OutlookAddin.Images.*. Ribbon XML now declares `image=<name>.png` and a
shared `loadImage=OnLoadImage` callback resolves each name to a Bitmap
from the assembly's manifest resources.
The ribbons affected:
- TabMail / Marcus-Law: Klear, פרטי תיק, הגדרות (3 buttons)
- TabNewMailMessage / Marcus-Law: כתוב מתיק (1 button)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace direct SSH rsync with build → push to registry → Coolify pull pattern,
matching the rest of the infra (decisions-court, ai-gateway, ...). Adds cdn/
Dockerfile + nginx.conf to serve .vsto/.application/.manifest/.deploy with
correct MIME types behind a StripPrefix Traefik middleware.
The new workflow has three jobs:
- build: Windows runner, nuget restore + msbuild + dotnet test.
- publish-clickonce: Windows runner, only on v* tag — sign assemblies +
mage manifests + upload publish/ as artifact.
- dockerize-and-deploy: Linux runner — download artifact, build & push
outlook-addin-cdn:<version> + :latest to
gitea.dev.marcus-law.co.il, then trigger Coolify
redeploy of app awdlvjlozz0dvn6kjlme9rd7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidebar identified the contact but showed "(אין תיקים פתוחים)" even
when cases existed for them. The previous query —
GET /Case?where[0][type]=linkedWith&where[0][attribute]=contacts&where[0][value]=…
— came back empty against this EspoCRM build, likely because the link
name on Case differs from the default or the where[] syntax isn't
accepted on this host.
Switch to the contact-side related-records endpoint which is the
canonical EspoCRM way to fetch linked records:
GET /Contact/{contactId}/cases?select=…&orderBy=createdAt&order=desc&maxSize=5
Keep the old /Case?where[] query as a fallback for installs where the
relationship walk fails. Log the chosen path and result count so the
next "no cases shown" report can be diagnosed from the log file alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related polish fixes:
1. Modal dialogs (File-to-Case / Compose-from-Case / Settings) used to
open without an Owner, so they would sink behind other windows and
the user perceived Outlook as "frozen". AttachOwnerToOutlook now
sets WindowInteropHelper.Owner to the foreground HWND (Win32
GetForegroundWindow with MainWindowHandle as fallback) before
ShowDialog. Also sets WindowStartupLocation=CenterOwner and pumps
SetForegroundWindow + Activate on Loaded.
2. Sidebar pane title was "פרטי תיק (EspoCRM)" — renamed to
"Klear · פרטי תיק" to match the new branding.
3. New ribbon button "פרטי תיק" (Marcus-Law group, between Klear and
הגדרות) re-opens the sidebar after the user clicks its X.
Implementation:
- SidebarController.ShowForActiveExplorer() toggles existing pane
Visible back on, or attaches one if missing.
- ExplorerBinding.Pane is now public on the binding for this
reuse.
- AddInHost.ShowSidebar() forwards to the controller.
- ExplorerRibbon.OnShowSidebar wires the ribbon callback.
imageMso = ReadingPaneShow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. New WPF converters (UI.Converters/EntityTypeToHebrewConverter.cs):
- EntityTypeToHebrewConverter maps Case/Contact/Account/Lead/... →
תיק / איש קשר / לקוח / ליד / ...
- EntityTypeToBrushConverter per-type accent brushes
- EntityTypeToIconConverter per-type emoji glyph
2. FileToCaseDialog redesigned:
- Soft slate background (#F8FAFC) with card-style borders + 6px
rounded corners
- Search row turned into a card with a 🔍 leading icon (DockPanel.Dock
respects RTL so it's on the visual right)
- Group headers now show: emoji + Hebrew label (תיק / איש קשר / ...)
+ result count, with a colored stripe per entity type
- Custom ResultItem template: hover + selected backgrounds, rounded
selection
- Recents pane has a header strip and per-row colored chip showing
the Hebrew entity type
- Primary action ("תייק") in solid blue with hover state; secondary
("ביטול") in white with border; tertiary ("+ צור איש קשר") as a
muted link button
3. ComposeFromCaseDialog gets the same card + button styling for
visual consistency.
Builds clean, 39 tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. FileToCaseDialog + ComposeFromCaseDialog: under FlowDirection=RTL,
DockPanel flips Dock="Right" to render at visual left. Switch the
search label to Dock="Left" so it renders on the right (start of
Hebrew reading order), matching where the eye expects it.
2. Replace imageMso values that were either off-topic or rare:
- FileToEspoCrm: MeetingForwardToManager → MoveToFolder
- OpenEspoCrmSettings: ServerPropertiesDialog → FileOptions
- ComposeFromCase: MailMergeStartLetters → NewMailMessage
All three are present in every Outlook version ≥ 2010 and convey the
action better.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the "תייק ל-" / "File to" prefix; the action is described in the
screentip/supertip on hover. Ribbon real estate is tight and the brand
name alone reads cleaner in both locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two combined fixes for "user clicks תייק, nothing happens":
1. EspoEntityRef now also reads "entityName" and "_scope" into
EntityType — different EspoCRM versions emit different field names
from GlobalSearch. Without this the Submit button was clickable (Id
was populated) but GetSelectedFilingTarget bailed out because
EntityType was the empty string.
2. AddInHost.LaunchFileToCaseAsync now logs the dialog close outcome
AND the rejected target shape, and pops a Hebrew MessageBox when
filing is skipped due to missing entityType — instead of silently
returning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidebar lookup was raising
System.Text.Json.JsonException: The JSON value could not be converted
to MarcusLaw.OutlookAddin.Core.Models.SearchResult
because EspoCRM's /EmailAddress/search endpoint returns
[{id, entityType, emailAddress, name}, ...] (and on some hosted builds
{entityId, entityName, ...}), not the GlobalSearch-style envelope.
- IEspoCrmClient.EmailAddressSearchAsync now returns
Task<List<EmailAddressMatch>>.
- EspoCrmClient parses the bare array, with a fallback that tolerates
{"list":[...]} wrapping if some plugin reshapes the response.
- EmailAddressMatch accepts both the {id, entityType} and the newer
{entityId, entityName} field naming.
- MatchingService updated for the new contract.
The GlobalSearch endpoint (FileToCaseDialog search) still uses the
{total, list} envelope and is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous fix made Username optional in the Settings dialog and
the credential store, TryInitializeCrm still bailed out when Username
was blank — leaving the add-in in "not configured" state even though
the user had saved a valid API key.
Only ApiKey is required now. The check matches the new contract used by
EspoCrmClient (X-Api-Key alone is sufficient for API-User credentials).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user left Username blank and clicked Save, the dialog persisted
the literal string "(api-key)" as the username. On reopen, that value
loaded back and the next Test Connection / save would send a poisoned
Espo-Authorization Basic header — base64("(api-key)":<apiKey>) — which
the server correctly rejects with 401, overshadowing the valid
X-Api-Key header.
Fix:
- DpapiCredentialStore.Save accepts a null/empty username and stores it
as an empty string. ApiKey is still mandatory.
- SettingsViewModel passes null when the field is blank instead of the
placeholder.
- EspoCrmClient already skips the Espo-Authorization header when
username is blank — combined with this fix, no fallback header is
sent for API-only credentials.
Tests: added Save_AcceptsEmptyUsername; updated Save_RejectsBlankInput
→ Save_RejectsBlankApiKey to match the new contract. 39/39 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EspoCRM uses TWO different auth schemes:
* X-Api-Key: <apiKey> — for API-type users (no password)
* Espo-Authorization: base64(u:p) — for regular users with passwords
The PRD-derived constructor only set Espo-Authorization with a fake
base64(username:apiKey) which the server rejects with 401 for API users.
Fix:
- Always send X-Api-Key when an apiKey is provided.
- Also send Espo-Authorization Basic-style header when a username is
supplied, so regular-user credentials still work without code changes.
- Username is now optional (constructor + Settings dialog). API key is
sufficient on its own; Settings credential store falls back to
"(api-key)" as the recorded username for logging when blank.
Tests updated:
- FileMailAsync_AddsBothApiKeyAndBasicAuthHeaders — asserts X-Api-Key
is the primary header and Espo-Authorization remains the fallback.
- Constructor_RejectsBlankCredentials — empty username now allowed,
empty apiKey / baseUrl still throw.
All 38 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: "Could not create SSL/TLS secure channel" on every CRM request
(GlobalSearch, MatchingService, TestConnection). TCP and DNS succeed
fine — the failure is at the schannel handshake stage.
Root cause: .NET Framework 4.8 inherits ServicePointManager.SecurityProtocol
from the host process. In Outlook 2019/M365 that ends up as SSL3/Tls1
which modern HTTPS servers reject.
Fix: in AddInHost ctor, OR Tls12 + Tls13 into SecurityProtocol before
any HttpClient is created. Tls13 (value 12288) is wrapped in try/catch
for pre-Win10-1809 systems.
Also surface WebExceptionStatus values in the Settings "Test connection"
dialog (SecureChannelFailure / TrustFailure / NameResolutionFailure /
ConnectFailure / Timeout) so future schannel issues report a clear
Hebrew explanation instead of "One or more errors occurred".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The catch-all in TestConnectionAsync was just showing
"שגיאת חיבור: One or more errors occurred." which is useless. Walk the
InnerException chain and translate the underlying SocketError /
TaskCanceled / AuthenticationException into a specific Hebrew message:
- HostNotFound → "השרת {host} לא נמצא ב-DNS"
- ConnectionRefused → "החיבור ל-{host}:{port} נדחה"
- TimedOut → "חיבור פג זמן"
- HostUnreachable / NetworkUnreachable → check VPN/network
- AuthenticationException → bad TLS cert
- TaskCanceled → 15s timeout
- otherwise → "{OuterType}: {OuterMsg} — {InnerType}: {InnerMsg}"
Full exception (with stack) keeps going to the addin log via
_logger.Warning, so the user can still file a diagnostic bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two VSTO-only build errors:
1. CustomTaskPanes.Add() requires a System.Windows.Forms.UserControl.
ElementHost derives from Control, not UserControl. Wrap it inside a
thin UserControl so the COM adapter is happy.
2. Outlook.Explorer exposes BOTH a Close() method and a Close event with
the same name; the C# compiler resolves "explorer.Close += handler"
to the method group. Cast through ExplorerEvents_10_Event to bind
the event explicitly.
Core/UI build clean. Host project still requires the VSTO build targets
to compile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trigger: tag push v*.
- Compute version from tag (vMAJOR.MINOR.PATCH → 4-part for mage)
- Sign DLLs + exe with signtool (CODESIGN_THUMBPRINT secret)
- Build ClickOnce .dll.manifest + .vsto via mage.exe, sign both, rename
payload files with .deploy convention for static-file serving
- Stage protocol handler bin/ + install-protocol.ps1 alongside the
ClickOnce artifact
- Upload publish/ artifact to GH Actions for inspection
- Upload to platform.dev:/var/www/outlook-addin/ via rsync (WSL) with scp
fallback. PLATFORM_DEV_SSH_KEY secret expects a base64-encoded private
key. Skips gracefully if the secret is empty.
- POST to MATTERMOST_WEBHOOK_RELEASES on success and failure
(#git-verelasim channel). Skips gracefully if the secret is empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Strings.resx (Hebrew, default) + Strings.en-US.resx (English override)
cover every dialog/sidebar/ribbon string, plus filing summary format
templates (`Msg_FilingSummary_*`)
- LocalizationManager singleton wraps ResourceManager with Get() and
Format() helpers; SetCulture honors AppSettings.Locale
- LocExtension XAML markup ({l:Loc Key}) for bindings
- Themes/RtlFonts.xaml — global RightToLeft FlowDirection and Hebrew
font stack (Segoe UI / David / Frank Ruehl / Arial)
- AddInHost calls LocalizationManager.SetCulture(Settings.Locale) before
any UI thread reads a resource
Existing dialogs keep their hardcoded Hebrew for v1 — those are already
in the correct (he-IL) language. Future iterations can switch to
{l:Loc …} bindings; the resource files already enumerate every string so
the migration is mechanical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Host:
- FolderResolver: walks Outlook Stores/Folders by stored StoreId +
backslash-separated FolderPath; RCWs released at each hop
- FolderWatcher: rooted Items references in a Subscription list
(preventing the GC from silently dropping the COM event sink),
HashSet<EntryID> dedup, marshals category stamping back to the WPF
dispatcher. Per-folder mode: AutoFile when MatchingService returns a
confident unique match AND confidence ≥ ConfidenceThreshold; otherwise
stamps "Needs filing"
- FolderTreeBuilder: one-shot STA traversal (max depth 4, max 500 entries)
into Core-side FolderTreeNode list
UI:
- FolderTreeNode in Core for cross-project transport
- FolderRowViewModel: per-row IsWatched / Mode / ConfidenceThreshold
- SettingsViewModel.LoadFolderTree(tree) merges existing
AppSettings.WatchedFolders into the loaded list
- Save now writes WatchedFolders alongside URL/creds
- SettingsDialog: new "תיקיות" tab with a DataGrid (checkbox + mode combo
per folder)
AddInHost:
- Builds FolderResolver + FolderWatcher inside TryInitializeCrm, applies
Settings.WatchedFolders on startup and after Save
- LaunchSettings: invokes FolderTreeBuilder, passes the flat list to the
ViewModel, and re-applies the watch list after the dialog closes
Core/UI/Tests green (38 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI:
- ComposeFromCaseDialog (RTL, search-then-pick a single Case via
GlobalSearch + entityType=Case filter)
- ComposeFromCaseViewModel — debounced GlobalSearch, returns chosen
EspoEntityRef
Host:
- ComposeService.ApplyCaseToMailAsync:
* fetches Case + primary Contact
* stamps UserProperties EspoCaseId, EspoCaseName, OutlookAddinGuid
* pre-fills To/Subject and appends an RTL footer with a CRM deep link
- ItemSend hook (in AddInHost.HookItemSend): when the user sends a
tagged compose, register guid → target with SentItemsWatcher
- SentItemsWatcher: rooted Items reference (prevents GC), 30s expiry
sweep, 2-min default timeout per pending tag, MailMatched event
- AddInHost.OnSentMailMatched: extracts the sent MailItem, files via
FilingService, stamps "Filed: {name}" category
- LaunchComposeFromCaseAsync (Inspector ribbon) and
LaunchComposeFromCaseByIdAsync (URL protocol) entry points
- NamedPipeListener (\.\pipe\MarcusLaw.OutlookAddin) — listens for
"COMPOSE <caseId>" lines, dispatches on WPF Dispatcher
Ribbon:
- InspectorRibbon.xml — "כתוב מתיק" button on Mail Compose ribbon, hidden
for received/read mail via getVisible
- ExplorerRibbon class now serves both Explorer and Inspector ribbons
(single IRibbonExtensibility, dispatches by ribbonID)
URL protocol:
- New OutlookAddinProtocolHandler console project (net48 WinExe):
parses outlookaddin://compose?caseId=<id>, connects to named pipe,
writes "COMPOSE <id>\n"
- tools/install-protocol.ps1 + uninstall-protocol.ps1 register the
scheme under HKCU\Software\Classes\outlookaddin
Core/UI/Tests + protocol handler all build; 38 unit tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SettingsDialog (WPF, RTL): General tab (URL/username/API key/locale) +
About tab; PasswordBox-bound API key, Test Connection probe, Save/Cancel
- SettingsViewModel: loads from SettingsManager + DpapiCredentialStore,
Test Connection spins a transient HttpClient + EspoCrmClient and reports
401 vs network failures separately; Save persists then signals dialog
close
- Ribbon: second "הגדרות" button in Marcus-Law group; "File to EspoCRM"
now offers to open Settings inline when not configured
- AddInHost.LaunchSettings tears down and rebuilds CRM/filing/retry stack
so new credentials take effect immediately (CrmClientChanged event)
- Tag .taskmaster/tasks/tasks.json: Task #3 in-review with implementation
notes (was already locally modified)
Core/UI/Tests green (38 tests passing); VSTO host project still requires
VS Installer Repair to compile locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>