Compare commits

...

11 Commits

Author SHA1 Message Date
PointStar 32f6facb67 publish: 1.2.10 — verify on-every-startup auto-update from v1.2.9
No code change. After v1.2.9 set UpdatePeriodically=false, the VSTO
runtime should hit the .vsto manifest on every Outlook launch. From a
clean v1.2.9 install: close Outlook, reopen, About tab shows 1.2.10
with no clicks anywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:53:54 +03:00
PointStar 2228353c48 fix(updater): check for updates on every Outlook startup, not every 7 days
Why v1.2.7→v1.2.8 didn't auto-install on Outlook restart: the manifest
was published with <UpdateInterval>7</UpdateInterval> + units=days, which
tells the VSTO runtime "don't even check the manifest URL more often
than once a week." MinimumRequiredVersion (set by CI to the same tag
version) would have forced the install — but only AFTER the check runs,
and the check was throttled to weekly.

Replace UpdateInterval/UpdateIntervalUnits with UpdatePeriodically=false.
This omits the <expiration> element from the deployment manifest, which
ClickOnce/VSTO interprets as "check before every host start." The extra
HTTP GET on the .vsto manifest at Outlook launch is ~6KB, ~100ms — a
non-issue for startup time, vs. the previous behavior where new
releases sat unused on platform.dev for up to a week per user.

From v1.2.9 onward, tag → CI publishes → next Outlook launch on each
lawyer's PC installs it. This is what we wanted the auto-update chain
to look like all along.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:50:31 +03:00
PointStar eea0d92d85 publish: 1.2.8 — verify v1.2.7's restart-to-update flow end-to-end
No code change. Confirms that on a clean v1.2.7 install (correct
simplified updater), pressing "בדוק עדכונים" reports the new version
honestly, and the VSTO runtime auto-installs v1.2.8 on the next
Outlook startup with no errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:40:58 +03:00
PointStar 655fefa822 fix(updater): stop trying to install in-process; tell user to restart Outlook
v1.2.6 fired VSTOInstaller.exe /I directly. It returned 0x8007007E
(ERROR_MOD_NOT_FOUND) — the loaded add-in DLLs are file-locked by the
running Outlook, so even the canonical installer can't overwrite them
while the host is up. This caps a series of failed attempts:

- 1.2.0/1/2: ApplicationDeployment.Update() → TrustNotGrantedException
- 1.2.3/4:   Process.Start(.vsto URL, shell)  → Chrome downloads to
             local-machine zone → InvalidDeploymentException
- 1.2.5/6:   VSTOInstaller.exe /I <url>       → 0x8007007E file lock

There is no API that updates a running VSTO add-in. The VSTO runtime's
own auto-update path (check manifest on Outlook startup, install before
loading) is the only mechanism that works — and it already does, every
time the user restarts Outlook.

So the button now just detects the new version via manifest HTTP GET
and displays "זמינה גרסה X. סגור ופתח את Outlook — הגרסה החדשה תותקן
אוטומטית בעלייה." No install attempt, no spawned process, no zone or
trust dance. The status line is honest about what the user has to do.

Removed the now-unused FindVstoInstaller registry-walk helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:17:32 +03:00
PointStar d2c54840ba publish: 1.2.6 — smoke-test the direct VSTOInstaller updater
No code change. Confirms that the v1.2.5 updater (VSTOInstaller.exe /I)
bypasses the Chrome download trap and pops the install dialog directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:11:38 +03:00
PointStar 870426ec08 fix(updater): invoke VSTOInstaller.exe directly so Chrome doesn't hijack the .vsto
v1.2.3 changed the updater to Process.Start the .vsto URL with
UseShellExecute=true. On any browser-as-default-handler (Chrome / Edge)
that downloads the file to disk instead of handing it to VSTOInstaller,
and re-opening the local copy then fails with InvalidDeploymentException
"לפריסה וליישום אין אזורי אבטחה תואמים" — the downloaded file lives in
the Local Machine zone while the manifest declares the Internet zone.

The reliable path is to invoke VSTOInstaller.exe directly with /I and
the manifest URL. The URL is then processed in the Internet zone,
matching what's declared in the manifest. FindVstoInstaller() looks at
the canonical VSTO 4.0 path and falls back to the HKCR registration of
the .vsto extension. If even that fails, we tell the user to close and
reopen Outlook — the VSTO runtime picks up the new manifest on its own
during startup, so the worst case is just an extra restart, not a stuck
install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:07:58 +03:00
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 35455c2ef6 docs(briefing): document the live release pipeline + Klear rebrand boundary
Replaces the stale "Things deliberately deferred" entries (CI runner,
codesign cert, platform.dev hosting, Mattermost webhook) with a new
"How releases work (current)" section that captures what's actually
running today:

- Production install URL = platform.dev.marcus-law.co.il/outlook-addin/
- Tag-driven Gitea Actions pipeline in .gitea/workflows/build.yml
- Real codesign cert lives in Gitea secrets, dev cert in repo
- Klear visible rebrand boundary: what shipped in v1.2.0 vs what stays
  MarcusLaw to preserve auto-update across the 10 installed lawyer PCs
- The dropped Publish/ folder was never the production channel

The "deferred" table now only mentions the identity-level Layer-2
rebrand, which is the only thing actually still open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:35:51 +03:00
6 changed files with 156 additions and 20 deletions
+37 -4
View File
@@ -268,14 +268,47 @@ If the session is on the Linux dev server (`/home/chaim/espocrm-extensions/Outlo
--- ---
## How releases work (current, 2026-05-24)
**Production install URL:** `https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto`
**Release flow:**
1. Code change merged to `main` (push or PR-merge — no CI fires yet)
2. Bump `<ApplicationVersion>` in `src/OutlookAddin/OutlookAddin.csproj` and `AssemblyVersion` + `AssemblyFileVersion` in `src/OutlookAddin/Properties/AssemblyInfo.cs` to the next 4-part version (e.g. `1.2.1.0`). CI overrides `ApplicationVersion` from the tag at build time, but leaves AssemblyInfo alone — keep them in sync manually so the bundled DLL's FileVersion matches the manifest.
3. `git tag -a v1.2.1 -m "..." && git push --tags`
4. Gitea Actions workflow (`.gitea/workflows/build.yml`) fires:
- Imports the **real** codesign cert from secrets `CODESIGN_CERT_PFX_BASE64` + `CODESIGN_THUMBPRINT` (publicKeyToken `c68d2b4c25051c5b`, `CN=Marcus-Law OutlookAddin`)
- Patches csproj to swap the dev self-signed thumbprint (`2399E9BF…`) for the real one
- `msbuild /t:Publish` with `/p:InstallUrl=https://platform.dev.marcus-law.co.il/outlook-addin/` and `/p:ApplicationVersion=<tag>.0`
- Uploads `publish.tar.gz` to Gitea generic registry (`outlook-addin-publish/<version>/publish.tar.gz`)
- Linux job downloads the tar.gz, builds a Docker image `outlook-addin-cdn:latest`, pushes to Gitea registry, calls Coolify to redeploy the CDN container
- Posts to Mattermost channel `git-verelasim` with the install URL on success / a failure link on failure
5. Lawyers' installed Add-in checks the deployment manifest at the install URL every 7 days (configured in csproj `<UpdateInterval>7</UpdateInterval>`) or whenever they click "בדוק עדכונים" in Settings → אודות.
**Semver policy:** PATCH for bug fixes (unilateral), MINOR for visible features (unilateral when warranted), MAJOR only with Chaim's explicit approval.
**Branding (`KlearBranding` repo):** the product is presented to lawyers as **Klear** — title bars, MessageBox titles, ribbon group label, About header all say "Klear". The logo is mirrored from `espocrm-extensions/KlearBranding/files/client/custom/img/logo-klear.png` into `src/OutlookAddin.UI/Resources/logo-klear.png` and embedded as a WPF Resource. Brand accent color: `#121d2e` (dark navy).
**What stays MarcusLaw under the hood (deliberate — preserves auto-update across the rebrand):**
- ClickOnce app identity (`AssemblyName=MarcusLaw.OutlookAddin`)
- `%APPDATA%\MarcusLaw\OutlookAddin\settings.json` and `creds.dat`
- `%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs\`
- `DpapiCredentialStore.EntropySeed = "MarcusLaw.OutlookAddin.v1"`
- Code namespaces `MarcusLaw.OutlookAddin.*`
- Named pipe `MarcusLaw.OutlookAddin`
Changing any of these would orphan all existing installs and require a coordinated uninstall+reinstall on every lawyer's PC plus an entropy migration for DPAPI-encrypted credentials. See the LegalCRM rebrand misadventure in git history for what *not* to do.
**Known traps when releasing:**
- VS Publish wizard (Project Properties → Publish) **rewrites** csproj at every click — flips `<AutoIncrementApplicationRevision>` back to `true`, resets `<ApplicationVersion>` to whatever's in the UI, sometimes deletes prior `Application Files/<...>/` bundles. **Don't use it.** Tag and let CI do it.
- There's an old `Publish/` directory in git history that was a parallel manual-release channel via `gitea.../raw/branch/main/Publish/`. It's **not the production install URL** — that's platform.dev. The folder was dropped in v1.2.0.
## Things deliberately deferred ## Things deliberately deferred
| What | When | Why deferred | | What | When | Why deferred |
|---|---|---| |---|---|---|
| Windows CI runner on Coolify | Until ClickOnce publish becomes useful | Local builds on dev's VS 2022 suffice through week 5 | | Identity-level rebrand to a pure Klear product (AssemblyName, namespaces, DPAPI entropy, AppData paths) | Until there's another reason to force-reinstall on all 10 PCs | Visible rebrand already done in v1.2.0 without disrupting users; full rename trades aesthetic cleanliness for a day of IT coordination + lost credentials |
| Code signing cert (self-signed) + GPO push to 10 PCs | Week 6 (before release) | IT task; cert isn't useful until we have something to sign |
| ClickOnce hosting setup at `platform.dev.marcus-law.co.il/outlook-addin/` | Week 6 | No build artifacts yet |
| Mattermost release notification webhook in CI | Week 6 | Coupled to CI runner |
--- ---
@@ -123,10 +123,22 @@
Foreground="{Binding UpdateStatusBrush}" Foreground="{Binding UpdateStatusBrush}"
TextWrapping="Wrap" TextWrapping="Wrap"
Margin="0,4,0,0" /> Margin="0,4,0,0" />
<TextBlock Margin="0,16,0,0" TextWrapping="Wrap" <TextBlock Margin="0,16,0,0" TextWrapping="Wrap">
Text="לוגים: %LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs" /> <Run Text="לוגים: " />
<TextBlock TextWrapping="Wrap" <Hyperlink Tag="%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs"
Text="הגדרות: %APPDATA%\MarcusLaw\OutlookAddin\settings.json" /> Click="OnOpenFolderHyperlink"
ToolTip="פתח את התיקייה ב-Explorer">
<Run Text="%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs" />
</Hyperlink>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Text="הגדרות: " />
<Hyperlink Tag="%APPDATA%\MarcusLaw\OutlookAddin"
Click="OnOpenFolderHyperlink"
ToolTip="פתח את התיקייה ב-Explorer">
<Run Text="%APPDATA%\MarcusLaw\OutlookAddin\settings.json" />
</Hyperlink>
</TextBlock>
</StackPanel> </StackPanel>
</TabItem> </TabItem>
</TabControl> </TabControl>
@@ -1,5 +1,8 @@
using System; using System;
using System.Diagnostics;
using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Documents;
using MarcusLaw.OutlookAddin.UI.ViewModels; using MarcusLaw.OutlookAddin.UI.ViewModels;
namespace MarcusLaw.OutlookAddin.UI.Dialogs namespace MarcusLaw.OutlookAddin.UI.Dialogs
@@ -40,5 +43,23 @@ namespace MarcusLaw.OutlookAddin.UI.Dialogs
DialogResult = _viewModel?.DialogResult; DialogResult = _viewModel?.DialogResult;
Close(); Close();
} }
private void OnOpenFolderHyperlink(object sender, RoutedEventArgs e)
{
if (!(sender is Hyperlink h) || !(h.Tag is string raw)) return;
try
{
var path = Environment.ExpandEnvironmentVariables(raw);
// Create the folder lazily so the link works even on the very
// first run, before any log has been written.
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
Process.Start(new ProcessStartInfo("explorer.exe", "\"" + path + "\"") { UseShellExecute = true });
}
catch (Exception ex)
{
MessageBox.Show("פתיחת התיקייה נכשלה: " + ex.Message, "Klear",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
} }
} }
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
@@ -178,18 +179,61 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
} }
var deployment = System.Deployment.Application.ApplicationDeployment.CurrentDeployment; var deployment = System.Deployment.Application.ApplicationDeployment.CurrentDeployment;
var info = await Task.Run(() => deployment.CheckForDetailedUpdate(false)).ConfigureAwait(true); var currentVersion = deployment.CurrentVersion;
if (!info.UpdateAvailable) var manifestUri = deployment.UpdateLocation;
// ApplicationDeployment.CheckForDetailedUpdate triggers a
// TrustManager re-evaluation which throws TrustNotGranted
// inside a hosted VSTO process even when the publisher and
// permissions are unchanged. Bypass it by fetching the
// deployment manifest directly and reading the version.
Version? availableVersion = null;
try
{ {
UpdateStatusBrush = SuccessBrush; availableVersion = await Task.Run(() => FetchManifestVersion(manifestUri)).ConfigureAwait(true);
UpdateStatus = "אתה על הגרסה האחרונה (" + deployment.CurrentVersion + ")."; }
catch (Exception fetchEx)
{
_logger.Warning(fetchEx, "CheckForUpdate: manifest fetch failed");
UpdateStatusBrush = ErrorBrush;
UpdateStatus = "שגיאה בבדיקת השרת: " + fetchEx.Message;
return; return;
} }
UpdateStatus = $"זמינה גרסה {info.AvailableVersion} — מוריד…"; if (availableVersion == null)
var result = await Task.Run(() => deployment.Update()).ConfigureAwait(true); {
UpdateStatusBrush = ErrorBrush;
UpdateStatus = "לא ניתן לקרוא את גרסת השרת מ-" + manifestUri;
return;
}
if (availableVersion <= currentVersion)
{
UpdateStatusBrush = SuccessBrush;
UpdateStatus = "אתה על הגרסה האחרונה (" + currentVersion + ").";
return;
}
// A running Outlook holds file locks on the loaded add-in
// DLLs. Neither ApplicationDeployment.Update (TrustNotGranted)
// nor a direct VSTOInstaller.exe /I call (0x8007007E
// ERROR_MOD_NOT_FOUND) nor Process.Start(.vsto URL) (Chrome
// downloads to local-machine zone — InvalidDeploymentException)
// can install over those locks. The only path that actually
// works is the one VSTO runtime takes automatically on Outlook
// startup: it checks the manifest URL before loading the
// add-in, downloads + installs if there's a newer version,
// then loads it.
//
// So the button's job is just to tell the user: "close and
// reopen Outlook." That's the same flow the user has been
// doing manually all along; we're just confirming the new
// version is there waiting.
UpdateStatusBrush = SuccessBrush; UpdateStatusBrush = SuccessBrush;
UpdateStatus = $"הותקנה גרסה {info.AvailableVersion}. סגור ופתח את Outlook כדי לטעון אותה."; UpdateStatus =
$"זמינה גרסה {availableVersion}. סגור ופתח את Outlook — " +
"הגרסה החדשה תותקן אוטומטית בעלייה.";
return;
} }
catch (System.Deployment.Application.DeploymentDownloadException ex) catch (System.Deployment.Application.DeploymentDownloadException ex)
{ {
@@ -215,6 +259,33 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
} }
} }
/// <summary>
/// Reads the top-level &lt;assemblyIdentity&gt; version from the
/// .vsto deployment manifest at <paramref name="manifestUri"/>.
/// Returns null if the document doesn't look like a deployment
/// manifest. Runs on a worker thread (synchronous HTTP).
/// </summary>
private static Version? FetchManifestVersion(Uri manifestUri)
{
// The .vsto manifest is small (≈6KB). Keep this synchronous —
// no point pulling in HttpClient and its lifecycle just for one
// GET. WebRequest works fine on .NET FW 4.8 and through proxies.
var req = System.Net.WebRequest.Create(manifestUri);
req.Timeout = 10_000;
using (var response = req.GetResponse())
using (var stream = response.GetResponseStream())
{
var doc = System.Xml.Linq.XDocument.Load(stream);
System.Xml.Linq.XNamespace asmv1 = "urn:schemas-microsoft-com:asm.v1";
var identity = doc.Root?
.Elements()
.FirstOrDefault(e => e.Name.LocalName == "assemblyIdentity" && e.Name.Namespace == asmv1);
var versionAttr = identity?.Attribute("version")?.Value;
if (string.IsNullOrWhiteSpace(versionAttr)) return null;
return Version.TryParse(versionAttr, out var v) ? v : null;
}
}
private bool CanCheckForUpdate() => !IsCheckingForUpdate; private bool CanCheckForUpdate() => !IsCheckingForUpdate;
partial void OnIsCheckingForUpdateChanged(bool value) partial void OnIsCheckingForUpdateChanged(bool value)
+2 -3
View File
@@ -36,11 +36,10 @@
<PublishUrl>C:\Users\Chaim\source\repos\OutlookAddin\Publish\</PublishUrl> <PublishUrl>C:\Users\Chaim\source\repos\OutlookAddin\Publish\</PublishUrl>
<InstallUrl>https://gitea.dev.marcus-law.co.il/espocrm-extensions/OutlookAddin/raw/branch/main/Publish/</InstallUrl> <InstallUrl>https://gitea.dev.marcus-law.co.il/espocrm-extensions/OutlookAddin/raw/branch/main/Publish/</InstallUrl>
<TargetCulture>en</TargetCulture> <TargetCulture>en</TargetCulture>
<ApplicationVersion>1.2.0.0</ApplicationVersion> <ApplicationVersion>1.2.10.0</ApplicationVersion>
<AutoIncrementApplicationRevision>false</AutoIncrementApplicationRevision> <AutoIncrementApplicationRevision>false</AutoIncrementApplicationRevision>
<UpdateEnabled>true</UpdateEnabled> <UpdateEnabled>true</UpdateEnabled>
<UpdateInterval>7</UpdateInterval> <UpdatePeriodically>false</UpdatePeriodically>
<UpdateIntervalUnits>days</UpdateIntervalUnits>
<ProductName>MarcusLaw.OutlookAddin</ProductName> <ProductName>MarcusLaw.OutlookAddin</ProductName>
<PublisherName /> <PublisherName />
<SupportUrl /> <SupportUrl />
+2 -2
View File
@@ -33,6 +33,6 @@ using System.Security;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.2.0.0")] [assembly: AssemblyVersion("1.2.10.0")]
[assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyFileVersion("1.2.10.0")]