Commit Graph

19 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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
PointStar e527aa80af feat(filing): scaffold Task #3 Feature 1 — file mail to Case/Contact
Core layer:
- FilingService: orchestrates batch filing with 401-aborts-batch, 409-treats-as-success, transient errors fall to DiskRetryQueue
- DiskRetryQueue: atomic write (tmp + rename), Hebrew UTF-8 round-trip, idempotent remove
- Models: MailEnvelope (STA-extracted primitives), FilingTarget, FilingOutcome enum, FilingItemResult, FilingBatchSummary, RetryQueueItem
- EspoCrmClient.CreateContactAsync for "create contact from sender" flow

UI layer:
- FileToCaseDialog (WPF, RTL, Hebrew labels) with grouped search results + recent sidebar
- FileToCaseViewModel: 250ms debounced GlobalSearch, create-contact-from-sender command, DialogResult signaling

Tests (38 passing):
- FilingServiceTests: happy path, 409, 401 aborts batch, transient enqueues, queue failure → Failed, target propagation, null guards
- DiskRetryQueueTests: id assignment, Hebrew round-trip, corrupt-file skip, idempotent remove, atomic overwrite

VSTO host project (OutlookAddin.csproj) regenerated; ThisAddIn wiring in follow-up commits.
gitignore: exclude .claude/settings.local.json (per-machine).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:04:10 +03:00