58 Commits

Author SHA1 Message Date
PointStar 137929b4ac publish: 1.2.4 — smoke-test the new manifest-fetch updater
No code change. Confirms that the v1.2.3 updater (HTTP GET the manifest
+ Process.Start the .vsto URL) reaches VSTOInstaller cleanly without
the TrustNotGrantedException that the old ApplicationDeployment.Update
path always threw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:01:17 +03:00
PointStar 95d7599cb1 fix(updater): replace broken ApplicationDeployment API with manifest GET
Log shows TrustNotGrantedException thrown inside CheckForDetailedUpdate
itself (ApplicationTrust.RequestTrust → DetermineTrustCore →
DetermineTrust). The misleading "המשתמש סירב להעניק את ההרשאות"
fires even when the user accepted the trust dialog on install and the
publisher cert is unchanged: ApplicationDeployment.CheckForDetailedUpdate
re-runs the TrustManager evaluation, which can't show a UAC-style
prompt from inside a hosted Outlook process and falls through to a
denial exception.

Bypass the ClickOnce update API entirely:
1. Read CurrentVersion from ApplicationDeployment (this part is fine)
2. HTTP GET the .vsto manifest at UpdateLocation, parse the top-level
   <assemblyIdentity> version
3. Compare versions
4. If newer, Process.Start the .vsto URL with UseShellExecute=true so
   Windows hands it to VSTOInstaller.exe — which is the only thing that
   actually knows how to update a running VSTO add-in correctly

ApplicationDeployment.Update() is also dropped because it has the same
in-process limitation as the trust check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:57:02 +03:00
PointStar 62254fcd8c publish: 1.2.2 — verify auto-update path
No code change. Bumping to confirm a fresh install from platform.dev
picks up the next tag-driven release cleanly via "בדוק עדכונים".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:49:56 +03:00
PointStar 7aa96c99a3 feat(about): logs + settings paths open the folder in Explorer
The two %APPDATA% / %LOCALAPPDATA% paths in Settings → אודות are now
hyperlinks. Click to open the folder (creates it lazily if it's not
there yet, so the link works before the first log is written).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:43:24 +03:00
PointStar 025dab9b20 feat(branding): Klear visual rebrand + drop orphaned Publish/ dir
User-facing strings only — ClickOnce identity, AssemblyName, namespaces,
DPAPI entropy and on-disk paths all stay MarcusLaw so the 10 lawyers
auto-update without any manual uninstall/reinstall.

What lawyers see change:
- About panel: dark-navy header card (#121d2e from KlearBranding) with
  the Klear logo (32KB PNG mirrored from
  espocrm-extensions/KlearBranding/files/client/custom/img/logo-klear.png)
  and the title "Klear" instead of "Marcus-Law OutlookAddin"
- All MessageBox titles ("Klear" instead of "Marcus-Law OutlookAddin")
- Ribbon group label ("Klear" instead of "Marcus-Law") in both the
  Explorer and the new-message Inspector ribbons
- Startup-failure dialog ("תוסף Klear נכשל" instead of "תוסף Marcus-Law")

What stays MarcusLaw (deliberate — Layer 1 only):
- ClickOnce app identity → preserves auto-update across this release
- %APPDATA%\MarcusLaw\…\settings.json + creds.dat → no migration needed
- DpapiCredentialStore EntropySeed → API keys remain decryptable
- Internal namespaces / pipe name / AssemblyCompany attribute

Cleanup:
- Removed the entire Publish/ directory from git. It was the orphaned
  manual-release channel under gitea raw/branch/main/Publish/ that no
  one actually uses — production installs are served from
  platform.dev.marcus-law.co.il via the Gitea Actions pipeline. The
  pipeline writes to a local publish/ workspace and uploads a tar.gz
  to the generic registry; it never reads or writes the repo's Publish/.
- Wiped the local LegalCRM_1_0_*_0 leftovers from the failed rename
  experiment.

Bumped to 1.2.0 — MINOR because the rebrand is a user-visible change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:32:40 +03:00
PointStar b1dc57324e fix(save-attachments): authoritative disk-verify + hide extension in rename UI
Two bugs reported after v1.1.2:

1. False-negative "נכשלו" toast even though the file was on disk. Root
   cause from the log: Polly's 15s timeout on /NetworkStorage/action/upload
   fires while the server is still writing larger PDFs, so the client sees
   TimeoutRejectedException → Outcome.Failed, while the server quietly
   finishes the write. The outcome is now decided by listing the target
   folder, not by the upload response. If the upload exception fires, the
   listing is retried up to 4 times with 2s delays to let the server
   finish. Saved if the file appears, Failed only if it never does.

2. The filename TextBox in the attachments list exposed the extension
   (.pdf / .docx). Users have to delete it manually when renaming and
   risk dropping it. AttachmentRowViewModel now splits FileName into
   BaseName (editable) and Extension (frozen at construction). The XAML
   binds the TextBox to BaseName and shows the Extension as a muted
   adjacent chip. The on-disk filename = BaseName + Extension always.

Bump to 1.1.3.0 for the patch release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:21:28 +03:00
PointStar 18b6640e57 feat(save-attachments): editable filenames, Hebrew status, smoother subfolder load
- DataGrid case picker now shows status via CaseStatusToHebrewConverter
  (added PendingHearing / PendingDecision mappings)
- Attachment row replaces the read-only filename TextBlock with a TwoWay
  TextBox so users can rename a file before saving (flows through
  EspoAttachment.Name into the upload + the on-disk path)
- SaveAttachmentsViewModel.IsLoadingSubfolders gates the folder ComboBox
  while EnsureCaseSubfolders / GetCase / ListNetworkStorageFolder run,
  so users can't pick a stale "(שורש התיק)" before the real list arrives
- About tab: ResolveDisplayVersion now reads from the host VSTO assembly
  (*.OutlookAddin) instead of the UI assembly. The UI csproj is SDK-style
  with no <Version> so it always reported 1.0.0.0, which leaked into the
  About line on dev installs
- Bump to 1.1.2.0 (csproj + AssemblyInfo) to match the v1.1.2 tag that
  will trigger the Gitea Actions auto-publish to platform.dev

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:27:49 +03:00
PointStar dbd7f3068b publish: 1.0.7 — switch to 3-part SemVer, manual bumps only
Versioning policy: MAJOR.MINOR.PATCH stored as MAJOR.MINOR.PATCH.0
in the ClickOnce manifest (it requires 4 components). The trailing
.0 is hidden by SettingsViewModel.ResolveDisplayVersion so the About
tab shows "1.0.7" not "1.0.7.0".

AutoIncrementApplicationRevision is now false; every published change
must bump <ApplicationVersion> and AssemblyVersion/AssemblyFileVersion
together by hand. MAJOR bumps need explicit owner approval.

Previously AssemblyVersion was pinned at 1.0.0.0 while ClickOnce
auto-bumped only ApplicationVersion, so the deployed DLL's
FileVersion never matched the manifest. Now they're locked at 1.0.7.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:51:40 +03:00
PointStar b6624a924f feat(save-attachments): show case#, court#, status in candidate picker
Replace the single-line ListBox with a DataGrid (case number, name,
court case number, status) so users disambiguate same-named cases
(e.g. two "שלומוב זויה" cases). Hydrates each search hit via
GET /Case/{id}?select=id,name,number,status,cCourtCaseNumber in
parallel so columns populate without an extra click. CourtCaseNumber
is mapped to the EspoCRM custom field cCourtCaseNumber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:51:15 +03:00
PointStar 9e7a64045f publish: 1.0.0.5 — host ClickOnce bundle on Gitea raw URL
- InstallUrl switched from \192.168.10.97 LAN share to
  https://gitea.dev.marcus-law.co.il/.../raw/branch/main/Publish/
  so end-users (including remote ones over VPN) can install and
  auto-update from the Gitea repo over HTTPS.
- IsWebBootstrapper set to True now that setup.exe fetches over HTTP.
- .gitignore: re-include Publish/** despite the *.vsto/*.application/
  *.deploy/*.manifest wildcards so the ClickOnce distribution root
  can be tracked.
- First publish of the bundle (version 1.0.0.5); older 1_0_0_2..4
  folders deleted before commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:58:10 +03:00
PointStar 017f10e60b feat(sidebar): inline candidate picker, per-case file action, back navigation
- Ambiguous lookups (2+ contacts on one email) now dedupe by Contact Id
  and fetch each candidate's full record so the picker shows
  name + account + email instead of just the bare search payload.
- Per-row "תייק" button on each linked case lets the user pick the
  correct case to file to when a contact has multiple cases; bottom
  "תייק לתיק הזה" button is hidden in that scenario.
- "→ חזור לרשימת אנשי קשר" link restores the candidate list after
  drilling into one of them.
- Contact name + case rows in the sidebar are now hyperlinks that
  open the entity in Klear.
- AddInHost.LaunchFileToCaseAsync accepts an optional MatchResult and
  auto-files when the sidebar resolved a single linked case, skipping
  the manual search dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:57:51 +03:00
PointStar be5edc203c feat: in-app updater button + correct case-folder resolution
Two related improvements:

1. About tab → "בדוק עדכונים" button. Calls ClickOnce's
   ApplicationDeployment.CurrentDeployment.CheckForDetailedUpdate +
   Update on background threads, so the user can pull a new release
   immediately without leaving Outlook — no PowerShell, no
   CleanOnlineAppCache dance.

   Status messages cover the four real outcomes:
     • not deployed via ClickOnce (F5 / dev install) → explicit error
     • on the latest version → green "already up to date"
     • update available → progress text, then "installed — restart
       Outlook"
     • download / manifest / generic exception → red, with reason

2. Case folder resolution now matches the server. The previous client
   logic used Case.name (the lawsuit title, e.g. "מונאס אריאל נ' ביטוח
   לאומי") which produced "47-מונאס אריאל נ ביטוח לאומי". But
   NetworkStorageIntegration's buildEntityFolderName uses the primary
   contact's name from Case.contactsIds[0] → server actually creates
   "47-אריאל מונאס" for the same case, so the two diverged and our
   uploads went to a phantom folder.

   New flow on case selection:
     a) EnsureCaseSubfoldersAsync (creates server's canonical folders)
     b) GetCase with select=contactsIds + networkStorageFolderPath
     c) GetContactAsync on contactsIds[0] → grab Name
     d) ResolveCaseFolderPath(case, contactName) → exact match
     e) Cache result on vm.ResolvedCaseFolderPath; AddInHost reads it
        instead of recomputing.

   ResolveCaseFolderPath fallback order is now (1) the integration's
   stored path, (2) <number>-<primary contact name>, (3) <number>-<case
   name>, (4) the contact / case name alone — matching the server's
   buildEntityFolderName logic step by step.

Tests: 39 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:08:20 +03:00
PointStar 1788b5bbee fix(about): show real ClickOnce deployment version in Settings → About
The About tab was always showing "1.0.0" because it read the DLL's
AssemblyVersion, which we never bumped in source — even when the
ClickOnce manifest had been rolled to 1.1.0 by the CI tag.

Read System.Deployment.Application.ApplicationDeployment.CurrentDeployment
.CurrentVersion at runtime when the add-in is running under ClickOnce
(IsNetworkDeployed=true). That's the version the user actually
installed. Fall back to AssemblyVersion when dev-running under F5 (no
ClickOnce context). Adds System.Deployment GAC reference to the UI
project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:59:19 +03:00
PointStar 8011d4e34c diag(attachments): post-upload "did the file actually arrive" check
The server reported successful uploads for the two PDFs into case 6a03166d…
("מונאס אריאל") but the user couldn't find the files on the share. Both
LocalFilesystemClient::uploadFile and WebDavClient::uploadFile return
true only on a real write, so something downstream is silently dropping
the payload — most likely a WebDAV proxy or a basePath mismatch
between what NetworkStorageIntegration thinks the storage is vs. what
the user is browsing.

Add a verification step right after the upload:

  1. POST /Attachment   →  attachmentId
  2. POST /NetworkStorage/action/upload  →  201 from server
  3. GET  /NetworkStorage/folderContents?path=<folder>
     • If our filename is in the listing → INF "verify present"
     • Else → WRN "expected X but NOT in listing. Currently in folder: […]"

The diagnostic line surfaces both halves of the puzzle in a single
log entry, so the next save attempt will tell us in plain text whether
the storage layer is the liar or the path is being normalised
somewhere unexpected.

Tests: 39 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:58:12 +03:00
PointStar 5242abb585 feat(attachments): rewire to Marcus-Law NetworkStorageIntegration
After reading the NetworkStorageIntegration repo I realized the previous
"save attachments" flow was wrong on every level — Marcus-Law doesn't
use EspoCRM's stock Document/DocumentFolder model. Folders are real
filesystem paths under the network share, named after the Case (format
"<number>-<primary contact name>") with four standard subfolders:
מסמכים, אסמכתאות, התכתבויות, כללי.

Rewire the save pipeline against the integration's real endpoints
(documented in routes.json on the server repo):

Core — three new IEspoCrmClient methods:
- EnsureCaseSubfoldersAsync(caseId)
    POST /NetworkStorage/action/createSubfolders
    Idempotent — creates case folder + defaults if missing.
- ListNetworkStorageFolderAsync(path)
    GET /NetworkStorage/folderContents?path=…
    Returns folders + files; we filter to type=folder for the dropdown.
- UploadAttachmentToNetworkStorageAsync(attachmentId, targetPath)
    POST /NetworkStorage/action/upload
    Moves the staged Attachment binary onto the share at <path>/<name>.

CaseEntity gets a new `networkStorageFolderPath` field (set by the
integration's hook on first Document save; empty until then).

NetworkStorageItem DTO captures the folder-listing rows.

AttachmentSaveService rewritten — no more Document creation, no more
DocumentFolder picker. The new contract is:
    SaveAsync(attachments, targetFolderPath)
Where targetFolderPath = "<case folder>[/subfolder]". For each
attachment: upload binary via standard /Attachment, then move via
NetworkStorage upload.

UI — SaveAttachmentsViewModel:
- Subfolder dropdown switched from DocumentFolder objects to strings.
- Static defaults loaded up-front:
    "(שורש התיק)" + 4 server-side defaults.
- When the user picks a case:
    1) hydrate Case (number + networkStorageFolderPath) via GetCase
    2) call EnsureCaseSubfoldersAsync to materialise standard folders
    3) listFolder at the case path to merge any custom subfolders into
       the dropdown.
- ResolveCaseFolderPath: use Case.networkStorageFolderPath if set,
  otherwise compute "<number>-<contact-name>" with a client-side
  SanitizeFolderSegment that mirrors LocalFilesystemClient::sanitizeFileName
  on the server (geresh/gershayim/quotes removed, illegal chars → hyphen).

AddInHost.LaunchSaveAttachmentsAsync:
- Composes <caseFolder>[/subfolder] from the VM choice.
- Pre-flight ensureCaseSubfolders as a safety net (in case the user
  submitted before the dialog's lazy refresh completed).
- Summary MessageBox now shows the resolved storage path instead of a
  bare entity name.

Tests: 39 passing. The build is clean across Core / UI / Tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:42:22 +03:00
PointStar 037d573e6e log: ListDocumentFoldersAsync prints folder count on success
When the case-scoped query fails (e.g. tenant rejects parentId in
where), the ViewModel falls back to ListDocumentFoldersAsync — but a
success path with N=0 was silent, leaving the user wondering why the
dropdown stayed empty. Add an INF line so the log shows whether
N folders came back, so 0-folder tenants are obvious from a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:22:10 +03:00
PointStar cb7d530b4c fix(attachments): map MIME from extension + don't treat 403 as auth
Two bugs found by reading the log carefully — both shipped in the
initial attachments feature:

1. MailItemExtractor sent every attachment with ContentType = "" and
   UploadAttachmentAsync defaulted that to "application/octet-stream".
   EspoCRM's attachment upload whitelist (config
   attachmentUpload.fileAcceptOptions) does not include octet-stream,
   so the server replied:
     403 (X-Status-Reason=Not allowed file type.)
   Add a GuessMimeType(fileName) helper to MailItemExtractor that maps
   common extensions (pdf, doc/docx, xls/xlsx, ppt/pptx, txt, csv,
   images, archives, email, audio/video) to canonical MIMEs. The
   octet-stream is now only used for truly unknown extensions.

2. 403 was being conflated with 401 in THREE places — EnsureSuccessAsync,
   the inline UploadAttachmentAsync handler, and the inline
   CreateDocumentAsync handler. Result: a single "Not allowed file
   type" rejection threw EspoCrmAuthorizationException, which
   AttachmentSaveService treats as a batch-level auth failure and
   stops processing remaining files. (Same bug would have killed
   FilingService and MatchingService for any non-auth 403 — e.g. an
   ACL miss on one record stopping a multi-mail file.)

   Only 401 is unambiguously an auth issue. 403 means "you ARE who
   you say you are, but this specific operation isn't allowed" —
   content rejected / ACL miss / role missing. Treat 403 as a normal
   HTTP error: log the body + X-Status-Reason and surface the message
   to the user without aborting the batch.

Also include the X-Status-Reason header in the generic
EspoCrmHttpException message everywhere, so any future "fileSizeTooBig"
or similar shows up in the user-visible error and not just the log.

Tests: 39 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:12:08 +03:00
PointStar 7924cda520 fix(documents): set publishDate on Document create (Marcus-Law requires it)
The server returned:
  400 {"messageTranslation":{"label":"validationFailure",
       "data":{"field":"publishDate","type":"required"}}}

Marcus-Law's EspoCRM install marks Document.publishDate as required.
By default the field is optional, but this tenant tightened it. Send
today's date (UTC, YYYY-MM-DD form) so the validation passes — the
user can still edit the published date in EspoCRM later if needed.

Also add the same verbose-error pattern to CreateDocumentAsync that we
already use on UploadAttachmentAsync: when the create returns non-2xx,
log the response body + X-Status-Reason header so any future required-
field mismatch shows up as a single readable line instead of
"שמירת הקבצים נכשלה".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:03:16 +03:00
PointStar 7ee9a00fc4 fix(attachments): match official EspoCRM API + folder fallback
Per the EspoCRM Attachment API doc (documentation.espocrm.com/development/api/attachment/),
the binary is sent in the "file" field as a data URI. The previous
extra "contents" alias was a legacy convention that strict tenants
reject with HTTP 400 (with an empty body, so the user only ever saw
"שמירת הקבצים נכשלה"). Drop "contents" — match the documented payload:

  {
    "name": "report.pdf",
    "type": "application/pdf",
    "role": "Attachment",
    "relatedType": "Document",
    "field": "file",
    "file": "data:application/pdf;base64,..."
  }

Folder dropdown: previously case-scoped only. The /Document filter by
parentId returned 400 on this tenant and case-scoped queries can be
fragile across versions. Now: try case-scoped first (so the most-
relevant folders surface); if it comes back empty or errors, fall back
to ListDocumentFoldersAsync (the full tenant list). The user always has
something to choose from, and the "(בלי תיקייה)" sentinel still works
as a "no folder" pick.

Tests: 39 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:51:46 +03:00
PointStar af8cf13443 fix(attachments): folder query + verbose upload diagnostics
Two fixes for the save-attachments flow:

1. ListFoldersUsedInCaseAsync no longer hits /Case/{id}/documents — that
   related-records endpoint requires a Case-to-Document link that
   EspoCRM doesn't ship by default, so it returned an empty list on
   every install. Switch to a direct Document list filtered by the
   polymorphic parent fields:
     GET /Document?where[0]=parentType=Case&where[1]=parentId=<id>
   That returns all documents linked to the case regardless of how the
   parent relationship is wired.

2. UploadAttachmentAsync now:
   - Emits BOTH "contents" and "file" payload fields with the
     data-URI base64 string. Different EspoCRM versions read from
     different keys; including both makes the call portable.
   - Logs the request shape (name, type, base64 length) before sending
     so server-side rejects are easier to attribute.
   - On a non-2xx response, logs the response body AND any
     X-Status-Reason header (EspoCRM convention for short error codes
     like "fileSizeIsTooLarge") so the user-visible exception finally
     includes a useful reason instead of an empty body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:42:38 +03:00
PointStar 26322c92ff feat(save-attachments): scope folder list to the selected case
The folder dropdown was showing every DocumentFolder in EspoCRM (global
list). The user expected "folders that exist in THIS case" — only the
folders already in use for the chosen case's documents.

Change: when the user picks a case in the SaveAttachments dialog, the
folder list refreshes by walking
  GET /Case/{id}/documents?select=id,folderId,folderName&maxSize=200
and deduping the folderId values it finds. EspoCRM denormalizes
folderName onto Document, so no second lookup is needed.

If a case has no documents yet, the dropdown shows just the sentinel
"(בלי תיקייה)" — same default behavior as before, but no more
unrelated folders from other cases.

Files:
- Core: DocumentEntity gets FolderName; IEspoCrmClient and EspoCrmClient
  get ListFoldersUsedInCaseAsync(caseId)
- UI: SaveAttachmentsViewModel re-fetches on SelectedCase change
  (LoadFoldersForCaseAsync); AddInHost no longer calls the old
  LoadFoldersAsync.

Tests: 39 still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:32:55 +03:00
PointStar c98fee7b5b fix(csproj): re-add klear-attach.png embed entry (VS save stripped it)
Visual Studio rewrote the legacy VSTO csproj on its next save and
dropped the EmbeddedResource entry for the paperclip icon — without
it the new SaveAttachments ribbon button has no image at runtime and
the file isn't shipped in the assembly. Add it back next to the four
other Klear icons.
2026-05-12 20:28:19 +03:00
PointStar 1a9ca2d2ad fix(ribbon): invalidate "שמור קבצים" on SelectionChange (button stays disabled)
The new SaveAttachments button rendered but stayed greyed out. Cause:
getEnabled callbacks are evaluated once when the ribbon loads and the
result is cached — Office only re-asks if you explicitly invalidate
the control. The previous SelectionChange handler invalidated only
FileToEspoCrm.

Collected the selection-dependent button IDs into a static array and
made the SelectionChange handler invalidate all of them. Adding more
context-sensitive buttons in the future is now a one-line edit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:27:33 +03:00
PointStar b1f8e3e50b feat(attachments): "save attachments only" ribbon action
New workflow alongside "תייק ל-Klear": save just the attachments of a
mail to a Case in EspoCRM (no Email entity created), with an optional
DocumentFolder.

Core:
- AttachmentEntity, DocumentEntity, DocumentFolder DTOs
- IEspoCrmClient gets three new endpoints:
    UploadAttachmentAsync (POST /Attachment, data-URI base64, role=Attachment)
    CreateDocumentAsync   (POST /Document, fileId + parent + folderId)
    ListDocumentFoldersAsync (GET /DocumentFolder, returns envelope)
- AttachmentSaveService orchestrates per-attachment upload + Document
  create, short-circuits on EspoCrmAuthorizationException, aggregates
  per-file Saved/Failed/AuthRequired outcomes into AttachmentSaveSummary

UI:
- SaveAttachmentsDialog (RTL, card layout matching FileToCaseDialog):
  Case search + folder dropdown + per-attachment checkbox list
- SaveAttachmentsViewModel: debounced GlobalSearch filtered to Case,
  optional folder selector (with a "(no folder)" sentinel row),
  Attachments collection of AttachmentRowViewModel rows
- AttachmentRowViewModel: IsSelected checkbox + display name + size
  computed from base64 length

Host:
- AddInHost.LaunchSaveAttachmentsAsync(MailItem) — extracts on the STA
  via MailItemExtractor, fetches folders, shows the dialog (anchored to
  Outlook via AttachOwnerToOutlook), runs AttachmentSaveService on OK,
  reports a Hebrew MessageBox summary
- AttachmentSaveService field on AddInHost + dispose hook

Ribbon:
- New "שמור קבצים" button in the Marcus-Law group, between Klear and
  פרטי תיק. Enabled only when exactly one MailItem is selected and it
  has at least one attachment
- klear-attach.png paperclip icon (32x32, blue rounded square),
  embedded as a resource and resolved by the existing OnGetImage
  callback (new case in the switch)

Tests: 39 still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:12:46 +03:00
PointStar 137cb99fba feat(sidebar): Hebrew case status labels + show all linked cases (not just 5)
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>
2026-05-11 19:13:12 +03:00
PointStar a1e25af86b fix(json): tolerate EspoCRM's naive datetime format (YYYY-MM-DD HH:mm:ss)
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>
2026-05-11 19:05:32 +03:00
PointStar 414f71706a fix: restore PNG embed entries + handle numeric Case.number from EspoCRM
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>
2026-05-11 18:57:15 +03:00
PointStar f371fc83ad fix(ribbon): switch icons from loadImage to per-button getImage callbacks
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>
2026-05-11 18:44:17 +03:00
PointStar 732c5641a8 feat(ribbon): custom Klear icons for all four ribbon buttons
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>
2026-05-11 18:15:04 +03:00
PointStar 037b66c683 fix(sidebar): fetch contact's cases via /Contact/{id}/cases instead of Case?where[]
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>
2026-05-11 17:13:29 +03:00
PointStar dc04a96bdd fix(ux): owner-anchor dialogs + sidebar reopen + Klear pane title
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>
2026-05-11 17:10:32 +03:00
PointStar 3bd8b6dfbb ui(design): Hebrew entity-type labels + modern dialog styling
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>
2026-05-11 17:06:50 +03:00
PointStar d175d4bfbb ui: move "חיפוש" to visual right + use clearer ribbon imageMso
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>
2026-05-11 17:03:19 +03:00
PointStar 6b14e5a6a6 feat(brand): shorten ribbon button label to just "Klear"
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>
2026-05-11 16:52:45 +03:00
PointStar eb081942e5 feat(brand): rename user-facing "EspoCRM" → "Klear"
User-facing surface only — display labels, button content, dialog
titles, screentips/supertips, resource strings (he + en), and the one
MessageBox referencing the brand. Internal class names (EspoCrmClient,
EspoEntityRef, EspoCrmAuthorizationException, EspoCrmHttpException,
EspoCRM-marked log messages and code comments) stay untouched — they
describe the upstream HTTP API, not the product the lawyers see.

Files updated:
- src/OutlookAddin/Ribbon/ExplorerRibbon.xml  (button label + tooltips)
- src/OutlookAddin/Ribbon/InspectorRibbon.xml (compose tooltips)
- src/OutlookAddin.UI/Dialogs/FileToCaseDialog.xaml (window title)
- src/OutlookAddin.UI/Dialogs/SettingsDialog.xaml (titles, field labels,
  About text)
- src/OutlookAddin.UI/Views/ReadingPaneView.xaml (button labels)
- src/OutlookAddin.UI/Resources/Strings.resx       (he)
- src/OutlookAddin.UI/Resources/Strings.en-US.resx (en)
- src/OutlookAddin/Services/AddInHost.cs (entity-type error MessageBox)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:52:09 +03:00
PointStar 633449548a fix(filing): accept entityName / _scope aliases + log silent target rejection
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>
2026-05-11 16:42:48 +03:00
PointStar 41a39ce2ac fix(matching): /EmailAddress/search returns a bare array, not {total,list}
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>
2026-05-11 16:37:09 +03:00
PointStar 484e908026 fix(addin-host): treat API key alone as configured (don't require username)
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>
2026-05-11 16:31:47 +03:00
PointStar b6e4e38833 fix(creds): store empty username when not supplied (was saving "(api-key)" placeholder)
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>
2026-05-11 16:28:37 +03:00
PointStar 891b9d01ae fix(auth): send X-Api-Key header — Espo-Authorization alone was wrong for API users
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>
2026-05-11 16:22:23 +03:00
PointStar 661e0cb598 fix(tls): enable TLS 1.2/1.3 on AddIn startup — fix SecureChannelFailure
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>
2026-05-11 16:15:42 +03:00
PointStar 40c3839278 fix(settings): surface real reason behind "Test connection" failures
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>
2026-05-11 16:06:43 +03:00
PointStar 3bedb2cb06 fix(sidebar): wrap ElementHost in WinForms UserControl + cast Explorer.Close event
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>
2026-05-11 16:00:18 +03:00
PointStar 69af5b6ac3 feat(i18n): Task #7 — Hebrew/English localization infrastructure
- 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>
2026-05-11 15:52:59 +03:00
PointStar a05b8114d4 feat(folders): Task #6 — per-folder auto-sync with confidence gate
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>
2026-05-11 15:51:03 +03:00
PointStar 0964439441 feat(compose): Task #5 — Compose from Case + URL protocol + auto-file
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>
2026-05-11 15:47:36 +03:00
PointStar 2368764251 feat(sidebar): Task #4 — reading-pane sidebar with sender matching
Core:
- IEspoCrmClient.ListCasesForContactAsync (paged where[]=linkedWith query)
- EspoListResponse<T> generic envelope for /list endpoints
- IMatchingService + MatchingService: EmailAddress/search → Contact →
  Account → recent 5 Cases. MemoryCache 10-min TTL; caches null and
  ambiguous results too. Returns null for 0 matches, IsAmbiguous=true
  for 2+, populated MatchResult for exactly 1.
- MatchResult DTO with Contact / Account / RecentCases / IsAmbiguous

UI:
- ReadingPaneView (WPF UserControl, RTL): 6 states — Empty / Loading /
  Match / NoMatch / Ambiguous / Error — switched via Visibility bindings
- ReadingPaneViewModel: state machine, FileRequested + OpenInBrowserRequested
  events, Reset()/LoadAsync() with cancellation chain (every selection
  cancels the previous lookup)

Host:
- ExplorerSelectionHandler: subscribes to Outlook.Explorer.SelectionChange,
  200ms debounce via Timer, dedupes by last sender, marshals to WPF
  dispatcher before reading COM state
- SidebarController: attaches a CustomTaskPane per Explorer
  (ElementHost wrapping ReadingPaneView), tracks bindings by COM identity,
  unhooks on Explorer.Close. Sidebar defers attach until CRM client is
  configured (subscribes to CrmClientChanged)
- AddInHost: MatchingService field, MemoryCache shared across lookups,
  InitializeSidebar(taskPanes) invoked from ThisAddIn_Startup

Core/UI/Tests green (38 tests passing); VSTO host still needs VS
Installer Repair locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:42:40 +03:00
PointStar aa65546fb1 feat(settings): add first-run Settings dialog and ribbon entry
- 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>
2026-05-11 15:37:39 +03:00
chaim 923a0b657f fix(host): enable AutoGenerateBindingRedirects for System.Text.Json deps
VSTO csproj template disables binding redirects, but System.Text.Json's
transitive deps (System.Text.Encodings.Web, System.Runtime.CompilerServices.Unsafe,
System.Memory) ship as out-of-band NuGet packs whose versions diverge
from .NET Framework 4.8's facade assemblies. Without redirects, the
JIT loads the wrong Unsafe.dll for DefaultJavaScriptEncoder's static
init, throwing FileLoadException wrapped in TypeInitializationException
when SettingsManager.JsonOptions is touched.

GenerateBindingRedirectsOutputType=true ensures the generated
{MarcusLaw.OutlookAddin.dll}.config goes alongside the assembly so
the VSTO loader actually sees it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:36:54 +00:00
chaim e8265400f8 fix(host): copy Core's NuGet refs (System.Text.Json, ProtectedData, Polly, Caching)
Legacy VSTO csproj doesn't follow PackageReferences transitively from
ProjectReferences — at runtime the host's bin\ ends up without the
satellite DLLs that Core depends on. JIT'ing SettingsManager throws
a type-initializer exception because JavaScriptEncoder.UnsafeRelaxedJsonEscaping
(in System.Text.Encodings.Web, transitive of System.Text.Json) is missing.

Pull all four runtime NuGet packs into the host directly. Versions
mirror Core.csproj exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:31:40 +00:00