Repository: kopia/kopia
Pull request #4233 (2024-11-05 06:34:40)

feat(cli): send error notifications and snapshot reports
Notifications will be sent to all configured notification profiles according to their severity levels.

The following events will trigger notifications:

- Snapshot is created (CLI only, severity >= report)
- Server Maintenance error occurs (CLI, server and UI, severity >= error)
- Any other CLI error occurs (CLI only, severity >= error).

A flag `--no-error-notifications` can be used to disable error notifications.

For #1958 

# Examples:

## Pushover error notification:

```
Kopia has encountered an error during snapshot create on MacBookPro.home.lan

Operation: snapshot create
Started:   2024-11-04 22:37:14 -0800 PST
Finished:  2024-11-04 22:38:40 -0800 PST (1m26s)

example error
github.com/kopia/kopia/cli.(*App).repositoryWriterAction.(*App).maybeRepositoryAction.func2
	/Users/jarek/Projects/Kopia/cli/app.go:586
<stack trace>
```

## Pushover snapshot report

```
Kopia created 11 snapshots on MacBookPro.home.lan

Path: /Users/jarek/Desktop
Status: ok
Start: 2024-11-04 22:38:14 -0800 PST Duration: 12.939ms
Size: 2 GB, 118 files, 14 directories.


Path: /Users/jarek/Documents
Status: ok
Start: 2024-11-04 22:38:38 -0800 PST Duration: 24.95ms
Size: 1.4 GB, 257 files, 42 directories.

<more sources follow>
```

## Email error notification

![image](https://github.com/user-attachments/assets/e3abe620-2cdd-4a3f-870b-3b8cbd64d026)

## Email snapshot report

![image](https://github.com/user-attachments/assets/c0d31bda-219a-4168-8abb-15d4feb282be)




codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kopia/kopia/pull/4233?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Attention: Patch coverage is `68.85246%` with `19 lines` in your changes missing coverage. Please review.
> Project coverage is 76.05%. Comparing base [(`cb455c6`)](https://app.codecov.io/gh/kopia/kopia/commit/cb455c699a04e54cb47e25a676d7d65063ce883e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) to head [(`e942537`)](https://app.codecov.io/gh/kopia/kopia/commit/e942537fa5ad2813ad935ebd72b1e33bb16610fa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Report is 336 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/kopia/kopia/pull/4233?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Patch % | Lines |
|---|---|---|
| [cli/command\_snapshot\_create.go](https://app.codecov.io/gh/kopia/kopia/pull/4233?src=pr&el=tree&filepath=cli%2Fcommand_snapshot_create.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfc25hcHNob3RfY3JlYXRlLmdv) | 70.58% | [5 Missing and 5 partials :warning: ](https://app.codecov.io/gh/kopia/kopia/pull/4233?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) |
| [internal/server/server\_maintenance.go](https://app.codecov.io/gh/kopia/kopia/pull/4233?src=pr&el=tree&filepath=internal%2Fserver%2Fserver_maintenance.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvc2VydmVyL3NlcnZlcl9tYWludGVuYW5jZS5nbw==) | 30.00% | [6 Missing and 1 partial :warning: ](https://app.codecov.io/gh/kopia/kopia/pull/4233?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) |
| [internal/server/server.go](https://app.codecov.io/gh/kopia/kopia/pull/4233?src=pr&el=tree&filepath=internal%2Fserver%2Fserver.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvc2VydmVyL3NlcnZlci5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/kopia/kopia/pull/4233?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4233      +/-   ##
==========================================
+ Coverage   75.86%   76.05%   +0.19%     
==========================================
  Files         470      506      +36     
  Lines       37301    38873    +1572     
==========================================
+ Hits        28299    29566    +1267     
- Misses       7071     7343     +272     
- Partials     1931     1964      +33     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/kopia/kopia/pull/4233?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).


Patch:
diff --git a/app/package-lock.json b/app/package-lock.json
index 028ab4ec..f2250e8f 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -12,19 +12,19 @@
         "auto-launch": "^5.0.6",
         "electron-log": "^5.2.0",
         "electron-store": "^10.0.0",
-        "electron-updater": "^6.3.4",
+        "electron-updater": "^6.3.9",
         "minimist": "^1.2.8",
         "semver": "^7.6.3",
-        "uuid": "^10.0.0"
+        "uuid": "^11.0.2"
       },
       "devDependencies": {
         "@electron/notarize": "^2.5.0",
-        "@playwright/test": "^1.47.2",
+        "@playwright/test": "^1.48.2",
         "asar": "^3.2.0",
         "concurrently": "^9.0.1",
         "dotenv": "^16.4.5",
-        "electron": "^32.1.2",
-        "electron-builder": "^25.0.5",
+        "electron": "^33.0.2",
+        "electron-builder": "^25.1.8",
         "electron-store": "^10.0.0",
         "playwright": "^1.37.1",
         "playwright-core": "^1.35.1"
@@ -48,12 +48,11 @@
       }
     },
     "node_modules/@electron/asar": {
-      "version": "3.2.13",
-      "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.13.tgz",
-      "integrity": "sha512-pY5z2qQSwbFzJsBdgfJIzXf5ElHTVMutC2dxh0FD60njknMu3n1NnTABOcQwbb5/v5soqE79m9UjaJryBf3epg==",
+      "version": "3.2.15",
+      "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.15.tgz",
+      "integrity": "sha512-AerUbRZpkDVRs58WP32t4U2bx85sfwRkQI8RMIEi6s2NBE++sgjsgAAMtXvnfTISKUkXo386pxFW7sa7WtMCrw==",
       "dev": true,
       "dependencies": {
-        "@types/glob": "^7.1.0",
         "commander": "^5.0.0",
         "glob": "^7.1.6",
         "minimatch": "^3.0.4"
@@ -214,9 +213,9 @@
       }
     },
     "node_modules/@electron/rebuild": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.0.tgz",
-      "integrity": "sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==",
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz",
+      "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==",
       "dev": true,
       "dependencies": {
         "@malept/cross-spawn-promise": "^2.0.0",
@@ -578,12 +577,12 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.47.2",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
-      "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
+      "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
       "dev": true,
       "dependencies": {
-        "playwright": "1.47.2"
+        "playwright": "1.48.2"
       },
       "bin": {
         "playwright": "cli.js"
@@ -650,7 +649,8 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
       "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "node_modules/@types/fs-extra": {
       "version": "9.0.13",
@@ -666,6 +666,7 @@
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
       "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
       "dev": true,
+      "optional": true,
       "dependencies": {
         "@types/events": "*",
         "@types/minimatch": "*",
@@ -691,7 +692,8 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
       "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "node_modules/@types/ms": {
       "version": "0.7.34",
@@ -767,15 +769,15 @@
       "dev": true
     },
     "node_modules/agent-base": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
-      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+      "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
       "dev": true,
       "dependencies": {
-        "debug": "4"
+        "debug": "^4.3.4"
       },
       "engines": {
-        "node": ">= 6.0.0"
+        "node": ">= 14"
       }
     },
     "node_modules/agentkeepalive": {
@@ -892,41 +894,44 @@
       }
     },
     "node_modules/app-builder-bin": {
-      "version": "5.0.0-alpha.7",
-      "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.7.tgz",
-      "integrity": "sha512-ww2mK4ITUvqisnqOuUWAeHzokpPidyZ7a0ZkwW+V7sF5/Pdi2OldkRjAWqEzn6Xtmj3SLVT84as4wB59A6jJ4g==",
+      "version": "5.0.0-alpha.10",
+      "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
+      "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==",
       "dev": true
     },
     "node_modules/app-builder-lib": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.0.5.tgz",
-      "integrity": "sha512-rxgxMx1f7I4ZAP0jA5+5iB7X6x6MJvGF7GauRzQBnIVihwXX2HOiAE7yenyY9Ry5YAiH47MnCxdq413Wq6XOcQ==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz",
+      "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==",
       "dev": true,
       "dependencies": {
         "@develar/schema-utils": "~2.6.5",
-        "@electron/notarize": "2.3.2",
+        "@electron/notarize": "2.5.0",
         "@electron/osx-sign": "1.3.1",
-        "@electron/rebuild": "3.6.0",
+        "@electron/rebuild": "3.6.1",
         "@electron/universal": "2.0.1",
         "@malept/flatpak-bundler": "^0.4.0",
         "@types/fs-extra": "9.0.13",
         "async-exit-hook": "^2.0.1",
         "bluebird-lst": "^1.0.9",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "chromium-pickle-js": "^0.2.0",
+        "config-file-ts": "0.2.8-rc1",
         "debug": "^4.3.4",
+        "dotenv": "^16.4.5",
+        "dotenv-expand": "^11.0.6",
         "ejs": "^3.1.8",
-        "electron-publish": "25.0.3",
+        "electron-publish": "25.1.7",
         "form-data": "^4.0.0",
         "fs-extra": "^10.1.0",
         "hosted-git-info": "^4.1.0",
         "is-ci": "^3.0.0",
         "isbinaryfile": "^5.0.0",
         "js-yaml": "^4.1.0",
+        "json5": "^2.2.3",
         "lazy-val": "^1.0.5",
         "minimatch": "^10.0.0",
-        "read-config-file": "6.4.0",
         "resedit": "^1.7.0",
         "sanitize-filename": "^1.6.3",
         "semver": "^7.3.8",
@@ -937,37 +942,8 @@
         "node": ">=14.0.0"
       },
       "peerDependencies": {
-        "dmg-builder": "25.0.5",
-        "electron-builder-squirrel-windows": "25.0.5"
-      }
-    },
-    "node_modules/app-builder-lib/node_modules/@electron/notarize": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.2.tgz",
-      "integrity": "sha512-zfayxCe19euNwRycCty1C7lF7snk9YwfRpB5M8GLr1a4ICH63znxaPNAubrMvj0yDvVozqfgsdYpXVUnpWBDpg==",
-      "dev": true,
-      "dependencies": {
-        "debug": "^4.1.1",
-        "fs-extra": "^9.0.1",
-        "promise-retry": "^2.0.1"
-      },
-      "engines": {
-        "node": ">= 10.0.0"
-      }
-    },
-    "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-      "dev": true,
-      "dependencies": {
-        "at-least-node": "^1.0.0",
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=10"
+        "dmg-builder": "25.1.8",
+        "electron-builder-squirrel-windows": "25.1.8"
       }
     },
     "node_modules/app-builder-lib/node_modules/brace-expansion": {
@@ -1338,22 +1314,22 @@
       "dev": true
     },
     "node_modules/builder-util": {
-      "version": "25.0.3",
-      "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.0.3.tgz",
-      "integrity": "sha512-eH5c1ukdY2xjtFQWQ6jlzEuXuqcuAVc3UQ6V6fdYu9Kg3CkDbCR82Mox42uaJDmee9WXSbP/88cOworFdOHPhw==",
+      "version": "25.1.7",
+      "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz",
+      "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==",
       "dev": true,
       "dependencies": {
         "@types/debug": "^4.1.6",
         "7zip-bin": "~5.2.0",
-        "app-builder-bin": "5.0.0-alpha.7",
+        "app-builder-bin": "5.0.0-alpha.10",
         "bluebird-lst": "^1.0.9",
-        "builder-util-runtime": "9.2.5",
+        "builder-util-runtime": "9.2.10",
         "chalk": "^4.1.2",
         "cross-spawn": "^7.0.3",
         "debug": "^4.3.4",
         "fs-extra": "^10.1.0",
-        "http-proxy-agent": "^5.0.0",
-        "https-proxy-agent": "^5.0.1",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.0",
         "is-ci": "^3.0.0",
         "js-yaml": "^4.1.0",
         "source-map-support": "^0.5.19",
@@ -1362,9 +1338,9 @@
       }
     },
     "node_modules/builder-util-runtime": {
-      "version": "9.2.5",
-      "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
-      "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
+      "version": "9.2.10",
+      "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz",
+      "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==",
       "dependencies": {
         "debug": "^4.3.4",
         "sax": "^1.2.4"
@@ -2106,14 +2082,14 @@
       }
     },
     "node_modules/dmg-builder": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.5.tgz",
-      "integrity": "sha512-ocnZV44ZqInoSFaY54fF7BlCtw+WtbrjyPrkBhaB+Ztn7GPKjmFgRbIKytifJ8h9Cib8jdFRMgjCUtkU45Y6DA==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz",
+      "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==",
       "dev": true,
       "dependencies": {
-        "app-builder-lib": "25.0.5",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "app-builder-lib": "25.1.8",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "fs-extra": "^10.1.0",
         "iconv-lite": "^0.6.2",
         "js-yaml": "^4.1.0"
@@ -2259,9 +2235,9 @@
       }
     },
     "node_modules/electron": {
-      "version": "32.1.2",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-32.1.2.tgz",
-      "integrity": "sha512-CXe6doFzhmh1U7daOvUzmF6Cj8hssdYWMeEPRnRO6rB9/bbwMlWctcQ7P8NJXhLQ88/vYUJQrJvlJPh8qM0BRQ==",
+      "version": "33.0.2",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-33.0.2.tgz",
+      "integrity": "sha512-C2WksfP0COsMHbYXSJG68j6S3TjuGDrw/YT42B526yXalIlNQZ2GeAYKryg6AEMkIp3p8TUfDRD0+HyiyCt/nw==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
@@ -2277,20 +2253,19 @@
       }
     },
     "node_modules/electron-builder": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.0.5.tgz",
-      "integrity": "sha512-Uj5LFRbUqNiVajsgqcwlKe+CHtwubK3hcoJsW5C2YiWodej2mmxM+LrTqga0rrWWHVMNmrcmGcS/WHpKwy6KEw==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz",
+      "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==",
       "dev": true,
       "dependencies": {
-        "app-builder-lib": "25.0.5",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "app-builder-lib": "25.1.8",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "chalk": "^4.1.2",
-        "dmg-builder": "25.0.5",
+        "dmg-builder": "25.1.8",
         "fs-extra": "^10.1.0",
         "is-ci": "^3.0.0",
         "lazy-val": "^1.0.5",
-        "read-config-file": "6.4.0",
         "simple-update-notifier": "2.0.0",
         "yargs": "^17.6.2"
       },
@@ -2303,15 +2278,15 @@
       }
     },
     "node_modules/electron-builder-squirrel-windows": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.5.tgz",
-      "integrity": "sha512-N2U7LGSdt4hmEhjEeIV2XJbjj2YIrTL6enfsGKfOhGTpL6GEejUmT3gjdKUqKBS5+NBx0GWhnEwD3MpO2P6Nfg==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz",
+      "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
       "dev": true,
       "peer": true,
       "dependencies": {
-        "app-builder-lib": "25.0.5",
+        "app-builder-lib": "25.1.8",
         "archiver": "^5.3.1",
-        "builder-util": "25.0.3",
+        "builder-util": "25.1.7",
         "fs-extra": "^10.1.0"
       }
     },
@@ -2397,14 +2372,14 @@
       }
     },
     "node_modules/electron-publish": {
-      "version": "25.0.3",
-      "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.0.3.tgz",
-      "integrity": "sha512-wSGm+TFK2lArswIFBPLuIRHbo945s3MCvG5y1xVC57zL/PsrElUkaGH2ERtRrcKNpaDNq77rDA9JnMJhAFJjUg==",
+      "version": "25.1.7",
+      "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
+      "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==",
       "dev": true,
       "dependencies": {
         "@types/fs-extra": "^9.0.11",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "chalk": "^4.1.2",
         "fs-extra": "^10.1.0",
         "lazy-val": "^1.0.5",
@@ -2475,11 +2450,11 @@
       }
     },
     "node_modules/electron-updater": {
-      "version": "6.3.4",
-      "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz",
-      "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==",
+      "version": "6.3.9",
+      "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz",
+      "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==",
       "dependencies": {
-        "builder-util-runtime": "9.2.5",
+        "builder-util-runtime": "9.2.10",
         "fs-extra": "^10.1.0",
         "js-yaml": "^4.1.0",
         "lazy-val": "^1.0.5",
@@ -2706,9 +2681,9 @@
       }
     },
     "node_modules/form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
       "dev": true,
       "dependencies": {
         "asynckit": "^0.4.0",
@@ -2986,17 +2961,16 @@
       "dev": true
     },
     "node_modules/http-proxy-agent": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
-      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
       "dev": true,
       "dependencies": {
-        "@tootallnate/once": "2",
-        "agent-base": "6",
-        "debug": "4"
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
       },
       "engines": {
-        "node": ">= 6"
+        "node": ">= 14"
       }
     },
     "node_modules/http2-wrapper": {
@@ -3013,16 +2987,16 @@
       }
     },
     "node_modules/https-proxy-agent": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
-      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+      "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
       "dev": true,
       "dependencies": {
-        "agent-base": "6",
+        "agent-base": "^7.0.2",
         "debug": "4"
       },
       "engines": {
-        "node": ">= 6"
+        "node": ">= 14"
       }
     },
     "node_modules/humanize-ms": {
@@ -3192,9 +3166,9 @@
       "peer": true
     },
     "node_modules/isbinaryfile": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz",
-      "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==",
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz",
+      "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==",
       "dev": true,
       "engines": {
         "node": ">= 18.0.0"
@@ -3480,6 +3454,45 @@
         "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
       }
     },
+    "node_modules/make-fetch-happen/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dev": true,
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/make-fetch-happen/node_modules/http-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "dev": true,
+      "dependencies": {
+        "@tootallnate/once": "2",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/make-fetch-happen/node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dev": true,
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/make-fetch-happen/node_modules/lru-cache": {
       "version": "7.18.3",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -3692,18 +3705,18 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "node_modules/negotiator": {
-      "version": "0.6.3",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+      "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
       "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
     },
     "node_modules/node-abi": {
-      "version": "3.68.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz",
-      "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==",
+      "version": "3.71.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz",
+      "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==",
       "dev": true,
       "dependencies": {
         "semver": "^7.3.5"
@@ -3977,12 +3990,12 @@
       "dev": true
     },
     "node_modules/playwright": {
-      "version": "1.47.2",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
-      "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
+      "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
       "dev": true,
       "dependencies": {
-        "playwright-core": "1.47.2"
+        "playwright-core": "1.48.2"
       },
       "bin": {
         "playwright": "cli.js"
@@ -3995,9 +4008,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.47.2",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
-      "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
+      "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -4098,23 +4111,6 @@
         "read-binary-file-arch": "cli.js"
       }
     },
-    "node_modules/read-config-file": {
-      "version": "6.4.0",
-      "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.4.0.tgz",
-      "integrity": "sha512-uB5QOBeF84PT61GlV11OTV4jUGHAO3iDEOP6v9ygxhG6Bs9PLg7WsjNT6mtIX2G+x8lJTr4ZWNeG6LDTKkNf2Q==",
-      "dev": true,
-      "dependencies": {
-        "config-file-ts": "0.2.8-rc1",
-        "dotenv": "^16.4.5",
-        "dotenv-expand": "^11.0.6",
-        "js-yaml": "^4.1.0",
-        "json5": "^2.2.3",
-        "lazy-val": "^1.0.5"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
     "node_modules/readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -4181,9 +4177,9 @@
       }
     },
     "node_modules/resedit": {
-      "version": "1.7.1",
-      "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.1.tgz",
-      "integrity": "sha512-/FJ6/gKAXbcHtivannhecWsa43kGVFK3aHHv9Jm3x0eFiM31MoGihkAOWbm3UsvjYLRVw0zTkfARy2dI96JL1Q==",
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
+      "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==",
       "dev": true,
       "dependencies": {
         "pe-library": "^0.4.1"
@@ -4467,6 +4463,18 @@
         "node": ">= 10"
       }
     },
+    "node_modules/socks-proxy-agent/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dev": true,
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -4770,9 +4778,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.6.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
-      "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "dev": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -4863,15 +4871,15 @@
       "dev": true
     },
     "node_modules/uuid": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
-      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "version": "11.0.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
+      "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
       "funding": [
         "https://github.com/sponsors/broofa",
         "https://github.com/sponsors/ctavan"
       ],
       "bin": {
-        "uuid": "dist/bin/uuid"
+        "uuid": "dist/esm/bin/uuid"
       }
     },
     "node_modules/verror": {
@@ -5097,12 +5105,11 @@
       }
     },
     "@electron/asar": {
-      "version": "3.2.13",
-      "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.13.tgz",
-      "integrity": "sha512-pY5z2qQSwbFzJsBdgfJIzXf5ElHTVMutC2dxh0FD60njknMu3n1NnTABOcQwbb5/v5soqE79m9UjaJryBf3epg==",
+      "version": "3.2.15",
+      "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.15.tgz",
+      "integrity": "sha512-AerUbRZpkDVRs58WP32t4U2bx85sfwRkQI8RMIEi6s2NBE++sgjsgAAMtXvnfTISKUkXo386pxFW7sa7WtMCrw==",
       "dev": true,
       "requires": {
-        "@types/glob": "^7.1.0",
         "commander": "^5.0.0",
         "glob": "^7.1.6",
         "minimatch": "^3.0.4"
@@ -5223,9 +5230,9 @@
       }
     },
     "@electron/rebuild": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.0.tgz",
-      "integrity": "sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==",
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz",
+      "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==",
       "dev": true,
       "requires": {
         "@malept/cross-spawn-promise": "^2.0.0",
@@ -5493,12 +5500,12 @@
       "optional": true
     },
     "@playwright/test": {
-      "version": "1.47.2",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
-      "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
+      "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
       "dev": true,
       "requires": {
-        "playwright": "1.47.2"
+        "playwright": "1.48.2"
       }
     },
     "@sindresorhus/is": {
@@ -5547,7 +5554,8 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
       "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "@types/fs-extra": {
       "version": "9.0.13",
@@ -5563,6 +5571,7 @@
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
       "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
       "dev": true,
+      "optional": true,
       "requires": {
         "@types/events": "*",
         "@types/minimatch": "*",
@@ -5588,7 +5597,8 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
       "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "@types/ms": {
       "version": "0.7.34",
@@ -5661,12 +5671,12 @@
       "dev": true
     },
     "agent-base": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
-      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+      "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
       "dev": true,
       "requires": {
-        "debug": "4"
+        "debug": "^4.3.4"
       }
     },
     "agentkeepalive": {
@@ -5752,41 +5762,44 @@
       }
     },
     "app-builder-bin": {
-      "version": "5.0.0-alpha.7",
-      "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.7.tgz",
-      "integrity": "sha512-ww2mK4ITUvqisnqOuUWAeHzokpPidyZ7a0ZkwW+V7sF5/Pdi2OldkRjAWqEzn6Xtmj3SLVT84as4wB59A6jJ4g==",
+      "version": "5.0.0-alpha.10",
+      "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
+      "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==",
       "dev": true
     },
     "app-builder-lib": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.0.5.tgz",
-      "integrity": "sha512-rxgxMx1f7I4ZAP0jA5+5iB7X6x6MJvGF7GauRzQBnIVihwXX2HOiAE7yenyY9Ry5YAiH47MnCxdq413Wq6XOcQ==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz",
+      "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==",
       "dev": true,
       "requires": {
         "@develar/schema-utils": "~2.6.5",
-        "@electron/notarize": "2.3.2",
+        "@electron/notarize": "2.5.0",
         "@electron/osx-sign": "1.3.1",
-        "@electron/rebuild": "3.6.0",
+        "@electron/rebuild": "3.6.1",
         "@electron/universal": "2.0.1",
         "@malept/flatpak-bundler": "^0.4.0",
         "@types/fs-extra": "9.0.13",
         "async-exit-hook": "^2.0.1",
         "bluebird-lst": "^1.0.9",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "chromium-pickle-js": "^0.2.0",
+        "config-file-ts": "0.2.8-rc1",
         "debug": "^4.3.4",
+        "dotenv": "^16.4.5",
+        "dotenv-expand": "^11.0.6",
         "ejs": "^3.1.8",
-        "electron-publish": "25.0.3",
+        "electron-publish": "25.1.7",
         "form-data": "^4.0.0",
         "fs-extra": "^10.1.0",
         "hosted-git-info": "^4.1.0",
         "is-ci": "^3.0.0",
         "isbinaryfile": "^5.0.0",
         "js-yaml": "^4.1.0",
+        "json5": "^2.2.3",
         "lazy-val": "^1.0.5",
         "minimatch": "^10.0.0",
-        "read-config-file": "6.4.0",
         "resedit": "^1.7.0",
         "sanitize-filename": "^1.6.3",
         "semver": "^7.3.8",
@@ -5794,31 +5807,6 @@
         "temp-file": "^3.4.0"
       },
       "dependencies": {
-        "@electron/notarize": {
-          "version": "2.3.2",
-          "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.2.tgz",
-          "integrity": "sha512-zfayxCe19euNwRycCty1C7lF7snk9YwfRpB5M8GLr1a4ICH63znxaPNAubrMvj0yDvVozqfgsdYpXVUnpWBDpg==",
-          "dev": true,
-          "requires": {
-            "debug": "^4.1.1",
-            "fs-extra": "^9.0.1",
-            "promise-retry": "^2.0.1"
-          },
-          "dependencies": {
-            "fs-extra": {
-              "version": "9.1.0",
-              "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-              "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-              "dev": true,
-              "requires": {
-                "at-least-node": "^1.0.0",
-                "graceful-fs": "^4.2.0",
-                "jsonfile": "^6.0.1",
-                "universalify": "^2.0.0"
-              }
-            }
-          }
-        },
         "brace-expansion": {
           "version": "2.0.1",
           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -6113,22 +6101,22 @@
       "dev": true
     },
     "builder-util": {
-      "version": "25.0.3",
-      "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.0.3.tgz",
-      "integrity": "sha512-eH5c1ukdY2xjtFQWQ6jlzEuXuqcuAVc3UQ6V6fdYu9Kg3CkDbCR82Mox42uaJDmee9WXSbP/88cOworFdOHPhw==",
+      "version": "25.1.7",
+      "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz",
+      "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==",
       "dev": true,
       "requires": {
         "@types/debug": "^4.1.6",
         "7zip-bin": "~5.2.0",
-        "app-builder-bin": "5.0.0-alpha.7",
+        "app-builder-bin": "5.0.0-alpha.10",
         "bluebird-lst": "^1.0.9",
-        "builder-util-runtime": "9.2.5",
+        "builder-util-runtime": "9.2.10",
         "chalk": "^4.1.2",
         "cross-spawn": "^7.0.3",
         "debug": "^4.3.4",
         "fs-extra": "^10.1.0",
-        "http-proxy-agent": "^5.0.0",
-        "https-proxy-agent": "^5.0.1",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.0",
         "is-ci": "^3.0.0",
         "js-yaml": "^4.1.0",
         "source-map-support": "^0.5.19",
@@ -6166,9 +6154,9 @@
       }
     },
     "builder-util-runtime": {
-      "version": "9.2.5",
-      "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
-      "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
+      "version": "9.2.10",
+      "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz",
+      "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==",
       "requires": {
         "debug": "^4.3.4",
         "sax": "^1.2.4"
@@ -6685,14 +6673,14 @@
       }
     },
     "dmg-builder": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.5.tgz",
-      "integrity": "sha512-ocnZV44ZqInoSFaY54fF7BlCtw+WtbrjyPrkBhaB+Ztn7GPKjmFgRbIKytifJ8h9Cib8jdFRMgjCUtkU45Y6DA==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz",
+      "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==",
       "dev": true,
       "requires": {
-        "app-builder-lib": "25.0.5",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "app-builder-lib": "25.1.8",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "dmg-license": "^1.0.11",
         "fs-extra": "^10.1.0",
         "iconv-lite": "^0.6.2",
@@ -6793,9 +6781,9 @@
       }
     },
     "electron": {
-      "version": "32.1.2",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-32.1.2.tgz",
-      "integrity": "sha512-CXe6doFzhmh1U7daOvUzmF6Cj8hssdYWMeEPRnRO6rB9/bbwMlWctcQ7P8NJXhLQ88/vYUJQrJvlJPh8qM0BRQ==",
+      "version": "33.0.2",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-33.0.2.tgz",
+      "integrity": "sha512-C2WksfP0COsMHbYXSJG68j6S3TjuGDrw/YT42B526yXalIlNQZ2GeAYKryg6AEMkIp3p8TUfDRD0+HyiyCt/nw==",
       "dev": true,
       "requires": {
         "@electron/get": "^2.0.0",
@@ -6804,20 +6792,19 @@
       }
     },
     "electron-builder": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.0.5.tgz",
-      "integrity": "sha512-Uj5LFRbUqNiVajsgqcwlKe+CHtwubK3hcoJsW5C2YiWodej2mmxM+LrTqga0rrWWHVMNmrcmGcS/WHpKwy6KEw==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz",
+      "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==",
       "dev": true,
       "requires": {
-        "app-builder-lib": "25.0.5",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "app-builder-lib": "25.1.8",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "chalk": "^4.1.2",
-        "dmg-builder": "25.0.5",
+        "dmg-builder": "25.1.8",
         "fs-extra": "^10.1.0",
         "is-ci": "^3.0.0",
         "lazy-val": "^1.0.5",
-        "read-config-file": "6.4.0",
         "simple-update-notifier": "2.0.0",
         "yargs": "^17.6.2"
       },
@@ -6852,15 +6839,15 @@
       }
     },
     "electron-builder-squirrel-windows": {
-      "version": "25.0.5",
-      "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.5.tgz",
-      "integrity": "sha512-N2U7LGSdt4hmEhjEeIV2XJbjj2YIrTL6enfsGKfOhGTpL6GEejUmT3gjdKUqKBS5+NBx0GWhnEwD3MpO2P6Nfg==",
+      "version": "25.1.8",
+      "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz",
+      "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
       "dev": true,
       "peer": true,
       "requires": {
-        "app-builder-lib": "25.0.5",
+        "app-builder-lib": "25.1.8",
         "archiver": "^5.3.1",
-        "builder-util": "25.0.3",
+        "builder-util": "25.1.7",
         "fs-extra": "^10.1.0"
       },
       "dependencies": {
@@ -6902,14 +6889,14 @@
       "integrity": "sha512-VjLkvaLmbP3AOGOh5Fob9M8bFU0mmeSAb5G2EoTBx+kQLf2XA/0byzjsVGBTHhikbT+m1AB27NEQUv9wX9nM8w=="
     },
     "electron-publish": {
-      "version": "25.0.3",
-      "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.0.3.tgz",
-      "integrity": "sha512-wSGm+TFK2lArswIFBPLuIRHbo945s3MCvG5y1xVC57zL/PsrElUkaGH2ERtRrcKNpaDNq77rDA9JnMJhAFJjUg==",
+      "version": "25.1.7",
+      "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
+      "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==",
       "dev": true,
       "requires": {
         "@types/fs-extra": "^9.0.11",
-        "builder-util": "25.0.3",
-        "builder-util-runtime": "9.2.5",
+        "builder-util": "25.1.7",
+        "builder-util-runtime": "9.2.10",
         "chalk": "^4.1.2",
         "fs-extra": "^10.1.0",
         "lazy-val": "^1.0.5",
@@ -6964,11 +6951,11 @@
       }
     },
     "electron-updater": {
-      "version": "6.3.4",
-      "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz",
-      "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==",
+      "version": "6.3.9",
+      "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz",
+      "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==",
       "requires": {
-        "builder-util-runtime": "9.2.5",
+        "builder-util-runtime": "9.2.10",
         "fs-extra": "^10.1.0",
         "js-yaml": "^4.1.0",
         "lazy-val": "^1.0.5",
@@ -7155,9 +7142,9 @@
       }
     },
     "form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
       "dev": true,
       "requires": {
         "asynckit": "^0.4.0",
@@ -7367,14 +7354,13 @@
       "dev": true
     },
     "http-proxy-agent": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
-      "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
       "dev": true,
       "requires": {
-        "@tootallnate/once": "2",
-        "agent-base": "6",
-        "debug": "4"
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
       }
     },
     "http2-wrapper": {
@@ -7388,12 +7374,12 @@
       }
     },
     "https-proxy-agent": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
-      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+      "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
       "dev": true,
       "requires": {
-        "agent-base": "6",
+        "agent-base": "^7.0.2",
         "debug": "4"
       }
     },
@@ -7517,9 +7503,9 @@
       "peer": true
     },
     "isbinaryfile": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz",
-      "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==",
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz",
+      "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==",
       "dev": true
     },
     "isexe": {
@@ -7763,6 +7749,36 @@
         "ssri": "^9.0.0"
       },
       "dependencies": {
+        "agent-base": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+          "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+          "dev": true,
+          "requires": {
+            "debug": "4"
+          }
+        },
+        "http-proxy-agent": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+          "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+          "dev": true,
+          "requires": {
+            "@tootallnate/once": "2",
+            "agent-base": "6",
+            "debug": "4"
+          }
+        },
+        "https-proxy-agent": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+          "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+          "dev": true,
+          "requires": {
+            "agent-base": "6",
+            "debug": "4"
+          }
+        },
         "lru-cache": {
           "version": "7.18.3",
           "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -7915,15 +7931,15 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "negotiator": {
-      "version": "0.6.3",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+      "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
       "dev": true
     },
     "node-abi": {
-      "version": "3.68.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz",
-      "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==",
+      "version": "3.71.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz",
+      "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==",
       "dev": true,
       "requires": {
         "semver": "^7.3.5"
@@ -8118,19 +8134,19 @@
       "dev": true
     },
     "playwright": {
-      "version": "1.47.2",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
-      "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
+      "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
       "dev": true,
       "requires": {
         "fsevents": "2.3.2",
-        "playwright-core": "1.47.2"
+        "playwright-core": "1.48.2"
       }
     },
     "playwright-core": {
-      "version": "1.47.2",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
-      "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
+      "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
       "dev": true
     },
     "plist": {
@@ -8204,20 +8220,6 @@
         "debug": "^4.3.4"
       }
     },
-    "read-config-file": {
-      "version": "6.4.0",
-      "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.4.0.tgz",
-      "integrity": "sha512-uB5QOBeF84PT61GlV11OTV4jUGHAO3iDEOP6v9ygxhG6Bs9PLg7WsjNT6mtIX2G+x8lJTr4ZWNeG6LDTKkNf2Q==",
-      "dev": true,
-      "requires": {
-        "config-file-ts": "0.2.8-rc1",
-        "dotenv": "^16.4.5",
-        "dotenv-expand": "^11.0.6",
-        "js-yaml": "^4.1.0",
-        "json5": "^2.2.3",
-        "lazy-val": "^1.0.5"
-      }
-    },
     "readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -8274,9 +8276,9 @@
       "dev": true
     },
     "resedit": {
-      "version": "1.7.1",
-      "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.1.tgz",
-      "integrity": "sha512-/FJ6/gKAXbcHtivannhecWsa43kGVFK3aHHv9Jm3x0eFiM31MoGihkAOWbm3UsvjYLRVw0zTkfARy2dI96JL1Q==",
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
+      "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==",
       "dev": true,
       "requires": {
         "pe-library": "^0.4.1"
@@ -8481,6 +8483,17 @@
         "agent-base": "^6.0.2",
         "debug": "^4.3.3",
         "socks": "^2.6.2"
+      },
+      "dependencies": {
+        "agent-base": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+          "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+          "dev": true,
+          "requires": {
+            "debug": "4"
+          }
+        }
       }
     },
     "source-map": {
@@ -8725,9 +8738,9 @@
       "optional": true
     },
     "typescript": {
-      "version": "5.6.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
-      "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
       "dev": true
     },
     "uint8array-extras": {
@@ -8793,9 +8806,9 @@
       "dev": true
     },
     "uuid": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
-      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
+      "version": "11.0.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
+      "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ=="
     },
     "verror": {
       "version": "1.10.1",
diff --git a/app/package.json b/app/package.json
index 7a5a1535..c0ccdd34 100644
--- a/app/package.json
+++ b/app/package.json
@@ -6,10 +6,10 @@
     "auto-launch": "^5.0.6",
     "electron-log": "^5.2.0",
     "electron-store": "^10.0.0",
-    "electron-updater": "^6.3.4",
+    "electron-updater": "^6.3.9",
     "minimist": "^1.2.8",
     "semver": "^7.6.3",
-    "uuid": "^10.0.0"
+    "uuid": "^11.0.2"
   },
   "type": "module",
   "author": {
@@ -112,12 +112,12 @@
   },
   "devDependencies": {
     "@electron/notarize": "^2.5.0",
-    "@playwright/test": "^1.47.2",
+    "@playwright/test": "^1.48.2",
     "asar": "^3.2.0",
     "concurrently": "^9.0.1",
     "dotenv": "^16.4.5",
-    "electron": "^32.1.2",
-    "electron-builder": "^25.0.5",
+    "electron": "^33.0.2",
+    "electron-builder": "^25.1.8",
     "electron-store": "^10.0.0",
     "playwright": "^1.37.1",
     "playwright-core": "^1.35.1"
diff --git a/cli/app.go b/cli/app.go
index 43083575..b9bf1fe1 100644
--- a/cli/app.go
+++ b/cli/app.go
@@ -16,9 +16,13 @@
 	"go.opentelemetry.io/otel/trace"
 
 	"github.com/kopia/kopia/internal/apiclient"
+	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/passwordpersist"
 	"github.com/kopia/kopia/internal/releasable"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/logging"
@@ -88,6 +92,7 @@ type appServices interface {
 	repositoryConfigFileName() string
 	getProgress() *cliProgress
 	getRestoreProgress() RestoreProgress
+	notificationTemplateOptions() notifytemplate.Options
 
 	stdout() io.Writer
 	Stderr() io.Writer
@@ -112,6 +117,7 @@ type advancedAppServices interface {
 	getPasswordFromFlags(ctx context.Context, isCreate, allowPersistent bool) (string, error)
 	optionsFromFlags(ctx context.Context) *repo.Options
 	runAppWithContext(command *kingpin.CmdClause, callback func(ctx context.Context) error) error
+	enableErrorNotifications() bool
 }
 
 // App contains per-invocation flags and state of Kopia CLI.
@@ -139,6 +145,8 @@ type App struct {
 	upgradeOwnerID      string
 	doNotWaitForUpgrade bool
 
+	errorNotifications bool
+
 	currentAction         string
 	onExitCallbacks       []func()
 	onFatalErrorCallbacks []func(err error)
@@ -240,6 +248,10 @@ func (c *App) passwordPersistenceStrategy() passwordpersist.Strategy {
 	return passwordpersist.File()
 }
 
+func (c *App) enableErrorNotifications() bool {
+	return c.errorNotifications
+}
+
 func (c *App) setup(app *kingpin.Application) {
 	app.PreAction(func(pc *kingpin.ParseContext) error {
 		if sc := pc.SelectedCommand; sc != nil {
@@ -274,6 +286,7 @@ func (c *App) setup(app *kingpin.Application) {
 	app.Flag("dump-allocator-stats", "Dump allocator stats at the end of execution.").Hidden().Envar(c.EnvName("KOPIA_DUMP_ALLOCATOR_STATS")).BoolVar(&c.dumpAllocatorStats)
 	app.Flag("upgrade-owner-id", "Repository format upgrade owner-id.").Hidden().Envar(c.EnvName("KOPIA_REPO_UPGRADE_OWNER_ID")).StringVar(&c.upgradeOwnerID)
 	app.Flag("upgrade-no-block", "Do not block when repository format upgrade is in progress, instead exit with a message.").Hidden().Default("false").Envar(c.EnvName("KOPIA_REPO_UPGRADE_NO_BLOCK")).BoolVar(&c.doNotWaitForUpgrade)
+	app.Flag("error-notifications", "Send notification on errors").Hidden().Envar(c.EnvName("KOPIA_SEND_ERROR_NOTIFICATIONS")).Default("true").BoolVar(&c.errorNotifications)
 
 	if c.enableTestOnlyFlags() {
 		app.Flag("ignore-missing-required-features", "Open repository despite missing features (VERY DANGEROUS, ONLY FOR TESTING)").Hidden().BoolVar(&c.testonlyIgnoreMissingRequiredFeatures)
@@ -562,6 +575,8 @@ func (c *App) maybeRepositoryAction(act func(ctx context.Context, rep repo.Repos
 			return errors.Wrap(err, "open repository")
 		}
 
+		t0 := clock.Now()
+
 		err = act(ctx, rep)
 
 		if rep != nil && err == nil && !mode.disableMaintenance {
@@ -570,6 +585,17 @@ func (c *App) maybeRepositoryAction(act func(ctx context.Context, rep repo.Repos
 			}
 		}
 
+		if err != nil && rep != nil && c.errorNotifications {
+			notification.Send(ctx, rep, "generic-error", notifydata.NewErrorInfo(
+				c.currentActionName(),
+				c.currentActionName(),
+				t0,
+				clock.Now(),
+				err), notification.SeverityError,
+				c.notificationTemplateOptions(),
+			)
+		}
+
 		if rep != nil && mode.mustBeConnected {
 			if cerr := rep.Close(ctx); cerr != nil {
 				return errors.Wrap(cerr, "unable to close repository")
@@ -647,6 +673,11 @@ func (c *App) advancedCommand(ctx context.Context) {
 	}
 }
 
+func (c *App) notificationTemplateOptions() notifytemplate.Options {
+	// perhaps make this configurable in the future
+	return notifytemplate.DefaultOptions
+}
+
 func init() {
 	kingpin.EnableFileExpansion = false
 }
diff --git a/cli/command_notification_template_set.go b/cli/command_notification_template_set.go
index 82d5b6b1..5c1ba85b 100644
--- a/cli/command_notification_template_set.go
+++ b/cli/command_notification_template_set.go
@@ -77,7 +77,7 @@ func (c *commandNotificationTemplateSet) launchEditor(ctx context.Context, rep r
 	var lastUpdated string
 
 	if err := editor.EditLoop(ctx, "template.md", s, false, func(updated string) error {
-		_, err := notifytemplate.ParseTemplate(updated)
+		_, err := notifytemplate.ParseTemplate(updated, notifytemplate.DefaultOptions)
 		if err == nil {
 			lastUpdated = updated
 			return nil
diff --git a/cli/command_server_start.go b/cli/command_server_start.go
index 98ff726a..cb509490 100644
--- a/cli/command_server_start.go
+++ b/cli/command_server_start.go
@@ -161,6 +161,9 @@ func (c *commandServerStart) serverStartOptions(ctx context.Context) (*server.Op
 		DebugScheduler:         c.debugScheduler,
 		MinMaintenanceInterval: c.minMaintenanceInterval,
 		DisableCSRFTokenChecks: c.disableCSRFTokenChecks,
+
+		EnableErrorNotifications: c.svc.enableErrorNotifications(),
+		NotifyTemplateOptions:    c.svc.notificationTemplateOptions(),
 	}, nil
 }
 
diff --git a/cli/command_snapshot_create.go b/cli/command_snapshot_create.go
index 26700cbe..7f34127e 100644
--- a/cli/command_snapshot_create.go
+++ b/cli/command_snapshot_create.go
@@ -12,6 +12,8 @@
 
 	"github.com/kopia/kopia/fs"
 	"github.com/kopia/kopia/fs/virtualfs"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/snapshot"
 	"github.com/kopia/kopia/snapshot/policy"
@@ -127,6 +129,8 @@ func (c *commandSnapshotCreate) run(ctx context.Context, rep repo.RepositoryWrit
 		return err
 	}
 
+	var st notifydata.MultiSnapshotStatus
+
 	for _, snapshotDir := range sources {
 		if u.IsCanceled() {
 			log(ctx).Info("Upload canceled")
@@ -138,11 +142,13 @@ func (c *commandSnapshotCreate) run(ctx context.Context, rep repo.RepositoryWrit
 			finalErrors = append(finalErrors, fmt.Sprintf("failed to prepare source: %s", err))
 		}
 
-		if err := c.snapshotSingleSource(ctx, fsEntry, setManual, rep, u, sourceInfo, tags); err != nil {
+		if err := c.snapshotSingleSource(ctx, fsEntry, setManual, rep, u, sourceInfo, tags, &st); err != nil {
 			finalErrors = append(finalErrors, err.Error())
 		}
 	}
 
+	notification.Send(ctx, rep, "snapshot-report", st, notification.SeverityReport, c.svc.notificationTemplateOptions())
+
 	// ensure we flush at least once in the session to properly close all pending buffers,
 	// otherwise the session will be reported as memory leak.
 	// by default the wrapper function does not flush on errors, which is what we want to do always.
@@ -259,27 +265,48 @@ func startTimeAfterEndTime(startTime, endTime time.Time) bool {
 		startTime.After(endTime)
 }
 
-//nolint:gocyclo
-func (c *commandSnapshotCreate) snapshotSingleSource(ctx context.Context, fsEntry fs.Entry, setManual bool, rep repo.RepositoryWriter, u *snapshotfs.Uploader, sourceInfo snapshot.SourceInfo, tags map[string]string) error {
+//nolint:gocyclo,funlen
+func (c *commandSnapshotCreate) snapshotSingleSource(
+	ctx context.Context,
+	fsEntry fs.Entry,
+	setManual bool,
+	rep repo.RepositoryWriter,
+	u *snapshotfs.Uploader,
+	sourceInfo snapshot.SourceInfo,
+	tags map[string]string,
+	st *notifydata.MultiSnapshotStatus,
+) (finalErr error) {
 	log(ctx).Infof("Snapshotting %v ...", sourceInfo)
 
-	var err error
+	var mwe notifydata.ManifestWithError
 
-	previous, err := findPreviousSnapshotManifest(ctx, rep, sourceInfo, nil)
-	if err != nil {
-		return err
+	mwe.Source = sourceInfo
+
+	st.Snapshots = append(st.Snapshots, &mwe)
+
+	defer func() {
+		if finalErr != nil {
+			mwe.Error = finalErr.Error()
+		}
+	}()
+
+	var previous []*snapshot.Manifest
+
+	previous, finalErr = findPreviousSnapshotManifest(ctx, rep, sourceInfo, nil)
+	if finalErr != nil {
+		return finalErr
 	}
 
-	policyTree, err := policy.TreeForSource(ctx, rep, sourceInfo)
-	if err != nil {
-		return errors.Wrap(err, "unable to get policy tree")
+	policyTree, finalErr := policy.TreeForSource(ctx, rep, sourceInfo)
+	if finalErr != nil {
+		return errors.Wrap(finalErr, "unable to get policy tree")
 	}
 
-	manifest, err := u.Upload(ctx, fsEntry, policyTree, sourceInfo, previous...)
-	if err != nil {
+	manifest, finalErr := u.Upload(ctx, fsEntry, policyTree, sourceInfo, previous...)
+	if finalErr != nil {
 		// fail-fast uploads will fail here without recording a manifest, other uploads will
 		// possibly fail later.
-		return errors.Wrap(err, "upload error")
+		return errors.Wrap(finalErr, "upload error")
 	}
 
 	manifest.Description = c.snapshotCreateDescription
@@ -308,6 +335,8 @@ func (c *commandSnapshotCreate) snapshotSingleSource(ctx context.Context, fsEntr
 		manifest.EndTime = fs.UTCTimestampFromTime(endTimeOverride)
 	}
 
+	mwe.Manifest = *manifest
+
 	ignoreIdenticalSnapshot := policyTree.EffectivePolicy().RetentionPolicy.IgnoreIdenticalSnapshots.OrDefault(false)
 	if ignoreIdenticalSnapshot && len(previous) > 0 {
 		if previous[0].RootObjectID() == manifest.RootObjectID() {
@@ -316,17 +345,17 @@ func (c *commandSnapshotCreate) snapshotSingleSource(ctx context.Context, fsEntr
 		}
 	}
 
-	if _, err = snapshot.SaveSnapshot(ctx, rep, manifest); err != nil {
-		return errors.Wrap(err, "cannot save manifest")
+	if _, finalErr = snapshot.SaveSnapshot(ctx, rep, manifest); finalErr != nil {
+		return errors.Wrap(finalErr, "cannot save manifest")
 	}
 
-	if _, err = policy.ApplyRetentionPolicy(ctx, rep, sourceInfo, true); err != nil {
-		return errors.Wrap(err, "unable to apply retention policy")
+	if _, finalErr = policy.ApplyRetentionPolicy(ctx, rep, sourceInfo, true); finalErr != nil {
+		return errors.Wrap(finalErr, "unable to apply retention policy")
 	}
 
 	if setManual {
-		if err = policy.SetManual(ctx, rep, sourceInfo); err != nil {
-			return errors.Wrap(err, "unable to set manual field in scheduling policy for source")
+		if finalErr = policy.SetManual(ctx, rep, sourceInfo); finalErr != nil {
+			return errors.Wrap(finalErr, "unable to set manual field in scheduling policy for source")
 		}
 	}
 
diff --git a/go.mod b/go.mod
index 9a5d9745..e8df41ba 100644
--- a/go.mod
+++ b/go.mod
@@ -56,17 +56,17 @@ require (
 	go.opentelemetry.io/otel/trace v1.31.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.28.0
+	golang.org/x/crypto v0.29.0
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
-	golang.org/x/mod v0.21.0
-	golang.org/x/net v0.30.0
-	golang.org/x/oauth2 v0.23.0
-	golang.org/x/sync v0.8.0
-	golang.org/x/sys v0.26.0
-	golang.org/x/term v0.25.0
-	golang.org/x/text v0.19.0
-	google.golang.org/api v0.204.0
-	google.golang.org/grpc v1.67.1
+	golang.org/x/mod v0.22.0
+	golang.org/x/net v0.31.0
+	golang.org/x/oauth2 v0.24.0
+	golang.org/x/sync v0.9.0
+	golang.org/x/sys v0.27.0
+	golang.org/x/term v0.26.0
+	golang.org/x/text v0.20.0
+	google.golang.org/api v0.205.0
+	google.golang.org/grpc v1.68.0
 	google.golang.org/protobuf v1.35.1
 	gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216
 )
@@ -75,7 +75,7 @@ require (
 	al.essio.dev/pkg/shellescape v1.5.1 // indirect
 	cel.dev/expr v0.16.1 // indirect
 	cloud.google.com/go v0.116.0 // indirect
-	cloud.google.com/go/auth v0.10.0 // indirect
+	cloud.google.com/go/auth v0.10.1 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect
 	cloud.google.com/go/compute/metadata v0.5.2 // indirect
 	cloud.google.com/go/iam v1.2.1 // indirect
diff --git a/go.sum b/go.sum
index 93263d73..2f90b847 100644
--- a/go.sum
+++ b/go.sum
@@ -5,8 +5,8 @@ cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
 cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
-cloud.google.com/go/auth v0.10.0 h1:tWlkvFAh+wwTOzXIjrwM64karR1iTBZ/GRr0S/DULYo=
-cloud.google.com/go/auth v0.10.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
+cloud.google.com/go/auth v0.10.1 h1:TnK46qldSfHWt2a0b/hciaiVJsmDXWy9FqyUan0uYiI=
+cloud.google.com/go/auth v0.10.1/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
 cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk=
 cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
 cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
@@ -350,8 +350,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
-golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
@@ -360,8 +360,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -373,18 +373,18 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -400,23 +400,24 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
-golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
+golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
 golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
 golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -429,8 +430,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.204.0 h1:3PjmQQEDkR/ENVZZwIYB4W/KzYtN8OrqnNcHWpeR8E4=
-google.golang.org/api v0.204.0/go.mod h1:69y8QSoKIbL9F94bWgWAq6wGqGwyjBgi2y8rAK8zLag=
+google.golang.org/api v0.205.0 h1:LFaxkAIpDb/GsrWV20dMMo5MR0h8UARTbn24LmD+0Pg=
+google.golang.org/api v0.205.0/go.mod h1:NrK1EMqO8Xk6l6QwRAmrXXg2v6dzukhlOyvkYtnvUuc=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -447,8 +448,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
-google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
+google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
 google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a h1:UIpYSuWdWHSzjwcAFRLjKcPXFZVVLXGEM23W+NWqipw=
 google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a/go.mod h1:9i1T9n4ZinTUZGgzENMi8MDDgbGC5mqTS75JAv6xN3A=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
diff --git a/internal/server/grpc_session.go b/internal/server/grpc_session.go
index 3779ea26..e532b288 100644
--- a/internal/server/grpc_session.go
+++ b/internal/server/grpc_session.go
@@ -132,7 +132,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 			go func() {
 				defer s.grpcServerState.sem.Release(1)
 
-				handleSessionRequest(ctx, dw, authz, usernameAtHostname, req, func(resp *grpcapi.SessionResponse) {
+				s.handleSessionRequest(ctx, dw, authz, usernameAtHostname, req, func(resp *grpcapi.SessionResponse) {
 					if err := s.send(srv, req.GetRequestId(), resp); err != nil {
 						select {
 						case lastErr <- err:
@@ -149,7 +149,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 
 var tracer = otel.Tracer("kopia/grpc")
 
-func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) {
+func (s *Server) handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) {
 	if req.GetTraceContext() != nil {
 		var tc propagation.TraceContext
 		ctx = tc.Extract(ctx, propagation.MapCarrier(req.GetTraceContext()))
@@ -187,7 +187,7 @@ func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, a
 		respond(handleApplyRetentionPolicyRequest(ctx, dw, authz, usernameAtHostname, inner.ApplyRetentionPolicy))
 
 	case *grpcapi.SessionRequest_SendNotification:
-		respond(handleSendNotificationRequest(ctx, dw, authz, inner.SendNotification))
+		respond(s.handleSendNotificationRequest(ctx, dw, authz, inner.SendNotification))
 
 	case *grpcapi.SessionRequest_InitializeSession:
 		respond(errorResponse(errors.New("InitializeSession must be the first request in a session")))
@@ -488,7 +488,7 @@ func handleApplyRetentionPolicyRequest(ctx context.Context, rep repo.RepositoryW
 	}
 }
 
-func handleSendNotificationRequest(ctx context.Context, rep repo.RepositoryWriter, authz auth.AuthorizationInfo, req *grpcapi.SendNotificationRequest) *grpcapi.SessionResponse {
+func (s *Server) handleSendNotificationRequest(ctx context.Context, rep repo.RepositoryWriter, authz auth.AuthorizationInfo, req *grpcapi.SendNotificationRequest) *grpcapi.SessionResponse {
 	ctx, span := tracer.Start(ctx, "GRPCSession.SendNotification")
 	defer span.End()
 
@@ -499,7 +499,8 @@ func handleSendNotificationRequest(ctx context.Context, rep repo.RepositoryWrite
 	if err := notification.SendInternal(ctx, rep,
 		req.GetTemplateName(),
 		json.RawMessage(req.GetEventArgs()),
-		notification.Severity(req.GetSeverity())); err != nil {
+		notification.Severity(req.GetSeverity()),
+		s.options.NotifyTemplateOptions); err != nil {
 		return errorResponse(err)
 	}
 
diff --git a/internal/server/server.go b/internal/server/server.go
index a0e4e516..8a6423b8 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -26,6 +26,7 @@
 	"github.com/kopia/kopia/internal/scheduler"
 	"github.com/kopia/kopia/internal/serverapi"
 	"github.com/kopia/kopia/internal/uitask"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/logging"
 	"github.com/kopia/kopia/repo/maintenance"
@@ -535,6 +536,14 @@ func (s *Server) endUpload(ctx context.Context, src snapshot.SourceInfo) {
 	s.parallelSnapshotsChanged.Signal()
 }
 
+func (s *Server) enableErrorNotifications() bool {
+	return s.options.EnableErrorNotifications
+}
+
+func (s *Server) notificationTemplateOptions() notifytemplate.Options {
+	return s.options.NotifyTemplateOptions
+}
+
 // SetRepository sets the repository (nil is allowed and indicates server that is not
 // connected to the repository).
 func (s *Server) SetRepository(ctx context.Context, rep repo.Repository) error {
@@ -785,23 +794,25 @@ func (s *Server) ServeStaticFiles(m *mux.Router, fs http.FileSystem) {
 
 // Options encompasses all API server options.
 type Options struct {
-	ConfigFile             string
-	ConnectOptions         *repo.ConnectOptions
-	RefreshInterval        time.Duration
-	MaxConcurrency         int
-	Authenticator          auth.Authenticator
-	Authorizer             auth.Authorizer
-	PasswordPersist        passwordpersist.Strategy
-	AuthCookieSigningKey   string
-	LogRequests            bool
-	UIUser                 string // name of the user allowed to access the UI API
-	UIPreferencesFile      string // name of the JSON file storing UI preferences
-	ServerControlUser      string // name of the user allowed to access the server control API
-	DisableCSRFTokenChecks bool
-	PersistentLogs         bool
-	UITitlePrefix          string
-	DebugScheduler         bool
-	MinMaintenanceInterval time.Duration
+	ConfigFile               string
+	ConnectOptions           *repo.ConnectOptions
+	RefreshInterval          time.Duration
+	MaxConcurrency           int
+	Authenticator            auth.Authenticator
+	Authorizer               auth.Authorizer
+	PasswordPersist          passwordpersist.Strategy
+	AuthCookieSigningKey     string
+	LogRequests              bool
+	UIUser                   string // name of the user allowed to access the UI API
+	UIPreferencesFile        string // name of the JSON file storing UI preferences
+	ServerControlUser        string // name of the user allowed to access the server control API
+	DisableCSRFTokenChecks   bool
+	PersistentLogs           bool
+	UITitlePrefix            string
+	DebugScheduler           bool
+	MinMaintenanceInterval   time.Duration
+	EnableErrorNotifications bool
+	NotifyTemplateOptions    notifytemplate.Options
 }
 
 // InitRepositoryFunc is a function that attempts to connect to/open repository.
diff --git a/internal/server/server_maintenance.go b/internal/server/server_maintenance.go
index 8faedbcd..1703d1e8 100644
--- a/internal/server/server_maintenance.go
+++ b/internal/server/server_maintenance.go
@@ -8,6 +8,9 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/internal/clock"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/maintenance"
 )
@@ -32,6 +35,8 @@ type srvMaintenance struct {
 type maintenanceManagerServerInterface interface {
 	runMaintenanceTask(ctx context.Context, dr repo.DirectRepository) error
 	refreshScheduler(reason string)
+	enableErrorNotifications() bool
+	notificationTemplateOptions() notifytemplate.Options
 }
 
 func (s *srvMaintenance) trigger() {
@@ -138,9 +143,21 @@ func startMaintenanceManager(
 
 				m.beforeRun()
 
+				t0 := clock.Now()
+
 				if err := srv.runMaintenanceTask(mctx, rep); err != nil {
 					log(ctx).Debugw("maintenance task failed", "err", err)
 					m.afterFailedRun()
+
+					if srv.enableErrorNotifications() {
+						notification.Send(ctx,
+							rep,
+							"generic-error",
+							notifydata.NewErrorInfo("Maintenance", "Scheduled Maintenance", t0, clock.Now(), err),
+							notification.SeverityError,
+							srv.notificationTemplateOptions(),
+						)
+					}
 				}
 
 				m.refresh(mctx, true)
diff --git a/notification/notification_send.go b/notification/notification_send.go
index 1a4c2b92..200f34d1 100644
--- a/notification/notification_send.go
+++ b/notification/notification_send.go
@@ -105,7 +105,7 @@ func notificationSendersFromRepo(ctx context.Context, rep repo.Repository, sever
 
 // Send sends a notification for the given event.
 // Any errors encountered during the process are logged.
-func Send(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity) {
+func Send(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity, opt notifytemplate.Options) {
 	// if we're connected to a repository server, send the notification there.
 	if rem, ok := rep.(repo.RemoteNotifications); ok {
 		jsonData, err := json.Marshal(eventArgs)
@@ -122,13 +122,13 @@ func Send(ctx context.Context, rep repo.Repository, templateName string, eventAr
 		return
 	}
 
-	if err := SendInternal(ctx, rep, templateName, eventArgs, sev); err != nil {
+	if err := SendInternal(ctx, rep, templateName, eventArgs, sev, opt); err != nil {
 		log(ctx).Warnw("unable to send notification", "err", err)
 	}
 }
 
 // SendInternal sends a notification for the given event and returns an error.
-func SendInternal(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity) error {
+func SendInternal(ctx context.Context, rep repo.Repository, templateName string, eventArgs any, sev Severity, opt notifytemplate.Options) error {
 	senders, err := notificationSendersFromRepo(ctx, rep, sev)
 	if err != nil {
 		return errors.Wrap(err, "unable to get notification senders")
@@ -137,7 +137,7 @@ func SendInternal(ctx context.Context, rep repo.Repository, templateName string,
 	var resultErr error
 
 	for _, s := range senders {
-		if err := SendTo(ctx, rep, s, templateName, eventArgs, sev); err != nil {
+		if err := SendTo(ctx, rep, s, templateName, eventArgs, sev, opt); err != nil {
 			resultErr = multierr.Append(resultErr, err)
 		}
 	}
@@ -166,7 +166,7 @@ func MakeTemplateArgs(eventArgs any) TemplateArgs {
 }
 
 // SendTo sends a notification to the given sender.
-func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateName string, eventArgs any, sev Severity) error {
+func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateName string, eventArgs any, sev Severity, opt notifytemplate.Options) error {
 	// execute template
 	var bodyBuf bytes.Buffer
 
@@ -175,7 +175,7 @@ func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateN
 		return errors.Wrap(err, "unable to resolve notification template")
 	}
 
-	t, err := notifytemplate.ParseTemplate(tmpl)
+	t, err := notifytemplate.ParseTemplate(tmpl, opt)
 	if err != nil {
 		return errors.Wrap(err, "unable to parse notification template")
 	}
@@ -205,5 +205,5 @@ func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateN
 func SendTestNotification(ctx context.Context, rep repo.Repository, s sender.Sender) error {
 	log(ctx).Infof("Sending test notification to %v", s.Summary())
 
-	return SendTo(ctx, rep, s, notifytemplate.TestNotification, struct{}{}, SeveritySuccess)
+	return SendTo(ctx, rep, s, notifytemplate.TestNotification, struct{}{}, SeveritySuccess, notifytemplate.DefaultOptions)
 }
diff --git a/notification/notifydata/doc.go b/notification/notifydata/doc.go
new file mode 100644
index 00000000..26449422
--- /dev/null
+++ b/notification/notifydata/doc.go
@@ -0,0 +1,2 @@
+// Package notifydata contains the data structures used by the notification package.
+package notifydata
diff --git a/notification/notifydata/error_info.go b/notification/notifydata/error_info.go
new file mode 100644
index 00000000..4002a061
--- /dev/null
+++ b/notification/notifydata/error_info.go
@@ -0,0 +1,43 @@
+package notifydata
+
+import (
+	"fmt"
+	"time"
+)
+
+// ErrorInfo represents information about errors.
+type ErrorInfo struct {
+	Operation        string    `json:"operation"`
+	OperationDetails string    `json:"operationDetails"`
+	StartTime        time.Time `json:"start"`
+	EndTime          time.Time `json:"end"`
+	ErrorMessage     string    `json:"error"`
+	ErrorDetails     string    `json:"errorDetails"`
+}
+
+// StartTimestamp returns the start time of the operation that caused the error.
+func (e *ErrorInfo) StartTimestamp() time.Time {
+	return e.StartTime.Truncate(time.Second)
+}
+
+// EndTimestamp returns the end time of the operation that caused the error.
+func (e *ErrorInfo) EndTimestamp() time.Time {
+	return e.EndTime.Truncate(time.Second)
+}
+
+// Duration returns the duration of the operation.
+func (e *ErrorInfo) Duration() time.Duration {
+	return e.EndTimestamp().Sub(e.StartTimestamp())
+}
+
+// NewErrorInfo creates a new ErrorInfo.
+func NewErrorInfo(operation, operationDetails string, startTime, endTime time.Time, err error) *ErrorInfo {
+	return &ErrorInfo{
+		Operation:        operation,
+		OperationDetails: operationDetails,
+		StartTime:        startTime,
+		EndTime:          endTime,
+		ErrorMessage:     fmt.Sprintf("%v", err),
+		ErrorDetails:     fmt.Sprintf("%+v", err),
+	}
+}
diff --git a/notification/notifydata/multi_snapshot_status.go b/notification/notifydata/multi_snapshot_status.go
new file mode 100644
index 00000000..4543ea89
--- /dev/null
+++ b/notification/notifydata/multi_snapshot_status.go
@@ -0,0 +1,96 @@
+package notifydata
+
+import (
+	"time"
+
+	"github.com/kopia/kopia/snapshot"
+)
+
+// ManifestWithError represents information about the snapshot manifest with optional error.
+type ManifestWithError struct {
+	snapshot.Manifest `json:"manifest"` // may not be filled out if there was an error, Manifst.Source is always set.
+
+	Error string `json:"error"` // will be present if there was an error
+}
+
+// StartTimestamp returns the start time of the snapshot.
+func (m *ManifestWithError) StartTimestamp() time.Time {
+	return m.StartTime.ToTime().UTC().Truncate(time.Second)
+}
+
+// EndTimestamp returns the end time of the snapshot.
+func (m *ManifestWithError) EndTimestamp() time.Time {
+	return m.EndTime.ToTime().UTC().Truncate(time.Second)
+}
+
+// TotalSize returns the total size of the snapshot in bytes.
+func (m *ManifestWithError) TotalSize() int64 {
+	if m.RootEntry == nil {
+		return 0
+	}
+
+	if m.RootEntry.DirSummary != nil {
+		return m.RootEntry.DirSummary.TotalFileSize
+	}
+
+	return m.RootEntry.FileSize
+}
+
+// TotalFiles returns the total number of files in the snapshot.
+func (m *ManifestWithError) TotalFiles() int64 {
+	if m.RootEntry == nil {
+		return 0
+	}
+
+	if m.RootEntry.DirSummary != nil {
+		return m.RootEntry.DirSummary.TotalFileCount
+	}
+
+	return 1
+}
+
+// TotalDirs returns the total number of directories in the snapshot.
+func (m *ManifestWithError) TotalDirs() int64 {
+	if m.RootEntry == nil {
+		return 0
+	}
+
+	if m.RootEntry.DirSummary != nil {
+		return m.RootEntry.DirSummary.TotalDirCount
+	}
+
+	return 0
+}
+
+// Duration returns the duration of the snapshot.
+func (m *ManifestWithError) Duration() time.Duration {
+	return time.Duration(m.EndTime - m.StartTime)
+}
+
+// StatusCode returns the status code of the manifest.
+func (m *ManifestWithError) StatusCode() string {
+	if m.Error != "" {
+		return "fatal"
+	}
+
+	if m.Manifest.IncompleteReason != "" {
+		return "incomplete"
+	}
+
+	if m.Manifest.RootEntry != nil && m.Manifest.RootEntry.DirSummary != nil {
+		if m.Manifest.RootEntry.DirSummary.FatalErrorCount > 0 {
+			return "fatal"
+		}
+
+		if m.Manifest.RootEntry.DirSummary.IgnoredErrorCount > 0 {
+			return "error"
+		}
+	}
+
+	return "ok"
+}
+
+// MultiSnapshotStatus represents the status of multiple snapshots.
+type MultiSnapshotStatus struct {
+	Snapshots []*ManifestWithError `json:"snapshots"`
+}
diff --git a/notification/notifytemplate/embeddedtemplate.go b/notification/notifytemplate/embeddedtemplate.go
index 9e3841f8..0c24cb12 100644
--- a/notification/notifytemplate/embeddedtemplate.go
+++ b/notification/notifytemplate/embeddedtemplate.go
@@ -3,12 +3,15 @@
 
 import (
 	"embed"
+	"slices"
+	"sort"
 	"text/template"
 	"time"
 
 	"github.com/pkg/errors"
 
-	"github.com/kopia/kopia/fs"
+	"github.com/kopia/kopia/internal/units"
+	"github.com/kopia/kopia/notification/notifydata"
 )
 
 //go:embed "*.html"
@@ -20,22 +23,41 @@
 	TestNotification = "test-notification"
 )
 
-// Functions is a map of functions that can be used in templates.
+// Options provides options for template rendering.
+type Options struct {
+	Timezone   *time.Location
+	TimeFormat string
+}
+
+// functions is a map of functions that can be used in templates.
+func functions(opt Options) template.FuncMap {
+	if opt.Timezone == nil {
+		opt.Timezone = time.Local
+	}
+
+	if opt.TimeFormat == "" {
+		opt.TimeFormat = time.RFC1123Z
+	}
+
+	return template.FuncMap{
+		"bytes": units.BytesString[int64],
+		"sortSnapshotManifestsByName": func(man []*notifydata.ManifestWithError) []*notifydata.ManifestWithError {
+			res := slices.Clone(man)
+			sort.Slice(res, func(i, j int) bool {
+				return res[i].Source.String() < res[j].Source.String()
+			})
+			return res
+		},
+		"formatTime": func(t time.Time) string {
+			return t.In(opt.Timezone).Format(opt.TimeFormat)
+		},
+	}
+}
+
+// DefaultOptions is the default set of options.
 //
 //nolint:gochecknoglobals
-var Functions = template.FuncMap{
-	"toTime": func(t any) time.Time {
-		if t, ok := t.(time.Time); ok {
-			return t
-		}
-
-		if t, ok := t.(fs.UTCTimestamp); ok {
-			return t.ToTime()
-		}
-
-		return time.Time{}
-	},
-}
+var DefaultOptions = Options{}
 
 // GetEmbeddedTemplate returns embedded template by name.
 func GetEmbeddedTemplate(templateName string) (string, error) {
@@ -61,7 +83,7 @@ func SupportedTemplates() []string {
 }
 
 // ParseTemplate parses a named template.
-func ParseTemplate(tmpl string) (*template.Template, error) {
+func ParseTemplate(tmpl string, opt Options) (*template.Template, error) {
 	//nolint:wrapcheck
-	return template.New("template").Funcs(Functions).Parse(tmpl)
+	return template.New("template").Funcs(functions(opt)).Parse(tmpl)
 }
diff --git a/notification/notifytemplate/generic-error.html b/notification/notifytemplate/generic-error.html
new file mode 100644
index 00000000..7287c11a
--- /dev/null
+++ b/notification/notifytemplate/generic-error.html
@@ -0,0 +1,20 @@
+Subject: Kopia has encountered an error during {{ .EventArgs.Operation }} on {{.Hostname}}
+
+<!doctype html>
+<html>
+<head>
+</head>
+<body>
+
+<p><b>Operation:</b> {{ .EventArgs.OperationDetails }}</p>
+<p><b>Started:</b> {{ .EventArgs.StartTimestamp | formatTime }}</p>
+<p><b>Finished:</b> {{ .EventArgs.EndTimestamp | formatTime }} ({{ .EventArgs.Duration }})</p>
+
+<p><b>Message:</b> {{ .EventArgs.ErrorMessage }}</p>
+
+<pre>{{ .EventArgs.ErrorDetails }}</pre>
+
+<p>Generated at {{ .EventTime | formatTime }} by <a href="https://kopia.io">Kopia {{ .KopiaBuildVersion }}</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/generic-error.txt b/notification/notifytemplate/generic-error.txt
new file mode 100644
index 00000000..eea8ace7
--- /dev/null
+++ b/notification/notifytemplate/generic-error.txt
@@ -0,0 +1,11 @@
+Subject: Kopia has encountered an error during {{ .EventArgs.Operation }} on {{.Hostname}}
+
+Operation: {{ .EventArgs.OperationDetails }}
+Started:   {{ .EventArgs.StartTimestamp | formatTime }}
+Finished:  {{ .EventArgs.EndTimestamp | formatTime }} ({{ .EventArgs.Duration }})
+
+{{ .EventArgs.ErrorDetails }}
+
+Generated at {{ .EventTime | formatTime }} by Kopia {{ .KopiaBuildVersion }}.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/snapshot-report.html b/notification/notifytemplate/snapshot-report.html
new file mode 100644
index 00000000..272d4330
--- /dev/null
+++ b/notification/notifytemplate/snapshot-report.html
@@ -0,0 +1,103 @@
+Subject: Kopia created {{ len .EventArgs.Snapshots }} snapshot{{ if gt (len .EventArgs.Snapshots) 1 }}s{{end}} on {{.Hostname}}
+
+<!doctype html>
+<html>
+<head>
+<style type="text/css">
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    thead tr {
+        background-color: #f2f2f2;
+    }
+
+    th, td {
+        border: 1px solid black;
+        padding: 2px;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f2f2f2;
+    }
+
+    code {
+        background-color: #f2f2f2;
+        padding: 2px;
+    }
+
+    span.path {
+        font-family: monospace;
+        color: #344652;
+        font-weight: bold;
+    }
+
+    tr.snapshotstatus-fatal {
+        background-color: #fde9e4;
+    }
+
+    tr.snapshotstatus-error {
+        background-color: #fcffba;
+    }
+
+    tr.snapshotstatus-incomplete {
+        background-color: #8a8c7e;
+    }
+</style>
+</head>
+<body>
+<table border="1">
+<thead>
+    <tr>
+        <th>Source</th>
+        <th>Started</th>
+        <th>Duration</th>
+        <th>Total Size</th>
+        <th>Total Files</th>
+        <th>Total Directories</th>
+    </tr>
+</thead>
+{{ range .EventArgs.Snapshots | sortSnapshotManifestsByName}}
+<tr class="snapshotstatus-{{ .StatusCode }}">
+<td><span class="path">{{ .Source.Path }}</span></td>
+<td>{{ .StartTimestamp | formatTime }}</td>
+<td>{{ .Duration }}</td>
+<td>{{ .TotalSize | bytes }}</td>
+<td>{{ .TotalFiles }}</td>
+<td>{{ .TotalDirs }}</td>
+</tr>
+
+{{ if .Error }}
+<tr class="snapshotstatus-{{ .StatusCode }}">
+    <td colspan="6">
+        <b style="color:red">Error:</b> {{ .Error }}
+    </td>
+</tr>
+{{ end }}
+
+{{ if .RootEntry }}
+{{ if .RootEntry.DirSummary }}
+{{ if .RootEntry.DirSummary.FailedEntries }}
+<tr class="snapshotstatus-{{ .StatusCode }}">
+    <td colspan="6">
+        <b style="color:red">Failed Entries:</b>
+        <ul>
+        {{ range .RootEntry.DirSummary.FailedEntries }}
+        <li><span class="path">{{.EntryPath}}</span>: {{.Error}}</li>
+        {{ end }}
+        </ul>
+    </td>
+</tr>
+{{ end }}
+{{ end }}
+{{ end }}
+
+{{ end }}
+</table>
+
+<p>Generated at {{ .EventTime | formatTime }} by <a href="https://kopia.io">Kopia {{ .KopiaBuildVersion }}</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/snapshot-report.txt b/notification/notifytemplate/snapshot-report.txt
new file mode 100644
index 00000000..212c85d0
--- /dev/null
+++ b/notification/notifytemplate/snapshot-report.txt
@@ -0,0 +1,16 @@
+Subject: Kopia created {{ len .EventArgs.Snapshots }} snapshot{{ if gt (len .EventArgs.Snapshots) 1 }}s{{end}} on {{.Hostname}}
+
+{{ range .EventArgs.Snapshots | sortSnapshotManifestsByName}}Path: {{ .Source.Path }}
+
+  Status: {{ .StatusCode }}
+  Start: {{ .StartTimestamp | formatTime  }} Duration: {{ .Duration }}
+  Size: {{ .TotalSize | bytes }}, {{ .TotalFiles }} files, {{ .TotalDirs }} directories.
+{{ if .Error }}  Error: {{ .Error }}
+{{ end }}{{ if .RootEntry }}{{ if .RootEntry.DirSummary }}{{ if .RootEntry.DirSummary.FailedEntries }}
+  Failed Entries:
+{{ range .RootEntry.DirSummary.FailedEntries }}
+  - {{.EntryPath}}: {{.Error}}{{ end }}{{ end }}{{ end }}
+{{ end }}
+{{ end }}Generated at {{ .EventTime | formatTime }} by Kopia {{ .KopiaBuildVersion }}.
+
+https://kopia.io/
\ No newline at end of file


Test patch:
diff --git a/internal/server/server_maintenance_test.go b/internal/server/server_maintenance_test.go
index cade0639..1d478549 100644
--- a/internal/server/server_maintenance_test.go
+++ b/internal/server/server_maintenance_test.go
@@ -12,6 +12,7 @@
 
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/repotesting"
+	"github.com/kopia/kopia/notification/notifytemplate"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/maintenance"
 )
@@ -47,6 +48,14 @@ func (s *testServer) refreshScheduler(reason string) {
 	s.refreshSchedulerCount.Add(1)
 }
 
+func (s *testServer) enableErrorNotifications() bool {
+	return false
+}
+
+func (s *testServer) notificationTemplateOptions() notifytemplate.Options {
+	return notifytemplate.DefaultOptions
+}
+
 func TestServerMaintenance(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
 
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index 36182e99..4a6a8402 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -290,7 +290,7 @@ func remoteRepositoryNotificationTest(t *testing.T, ctx context.Context, rep rep
 	}))
 	require.NoError(t, rw.Flush(ctx))
 
-	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError)
+	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError, notifytemplate.DefaultOptions)
 	require.Equal(t, int32(1), numRequestsReceived.Load())
 
 	// another webhook which fails
@@ -307,7 +307,7 @@ func remoteRepositoryNotificationTest(t *testing.T, ctx context.Context, rep rep
 	}))
 
 	require.NoError(t, rw.Flush(ctx))
-	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError)
+	notification.Send(ctx, rep, notifytemplate.TestNotification, nil, notification.SeverityError, notifytemplate.DefaultOptions)
 	require.Equal(t, int32(1), numRequestsReceived.Load())
 }
 
diff --git a/notification/notifytemplate/notifytemplate_test.go b/notification/notifytemplate/notifytemplate_test.go
new file mode 100644
index 00000000..182c98fb
--- /dev/null
+++ b/notification/notifytemplate/notifytemplate_test.go
@@ -0,0 +1,118 @@
+package notifytemplate_test
+
+import (
+	"bytes"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/fs"
+	"github.com/kopia/kopia/notification"
+	"github.com/kopia/kopia/notification/notifydata"
+	"github.com/kopia/kopia/notification/notifytemplate"
+	"github.com/kopia/kopia/snapshot"
+)
+
+var defaultTestOptions = notifytemplate.Options{
+	Timezone: time.UTC,
+}
+
+var altTestOptions = notifytemplate.Options{
+	Timezone:   time.FixedZone("PST", -8*60*60),
+	TimeFormat: time.RFC1123,
+}
+
+func TestNotifyTemplate_generic_error(t *testing.T) {
+	args := notification.MakeTemplateArgs(&notifydata.ErrorInfo{
+		Operation:        "Some Operation",
+		OperationDetails: "Some Operation Details",
+		ErrorMessage:     "error message",
+		ErrorDetails:     "error details",
+		StartTime:        time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
+		EndTime:          time.Date(2020, 1, 2, 3, 4, 6, 7, time.UTC),
+	})
+
+	args.EventTime = time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
+	args.Hostname = "some-host"
+
+	verifyTemplate(t, "generic-error.txt", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "generic-error.html", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "generic-error.txt", ".alt", args, altTestOptions)
+	verifyTemplate(t, "generic-error.html", ".alt", args, altTestOptions)
+}
+
+func TestNotifyTemplate_snapshot_report(t *testing.T) {
+	args := notification.MakeTemplateArgs(&notifydata.MultiSnapshotStatus{
+		Snapshots: []*notifydata.ManifestWithError{
+			{
+				Manifest: snapshot.Manifest{
+					Source:    snapshot.SourceInfo{Host: "some-host", UserName: "some-user", Path: "/some/path"},
+					StartTime: fs.UTCTimestamp(time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC).UnixNano()),
+					EndTime:   fs.UTCTimestamp(time.Date(2020, 1, 2, 3, 4, 6, 7, time.UTC).UnixNano()),
+					RootEntry: &snapshot.DirEntry{
+						DirSummary: &fs.DirectorySummary{
+							TotalFileCount: 123,
+							TotalFileSize:  456,
+							TotalDirCount:  33,
+							FailedEntries: []*fs.EntryWithError{
+								{
+									EntryPath: "/some/path",
+									Error:     "some error",
+								},
+								{
+									EntryPath: "/some/path2",
+									Error:     "some error",
+								},
+							},
+						},
+					},
+				},
+			},
+			{
+				Error: "some top-level error",
+				Manifest: snapshot.Manifest{
+					Source: snapshot.SourceInfo{Host: "some-host", UserName: "some-user", Path: "/some/other/path"},
+				},
+			},
+		},
+	})
+
+	args.EventTime = time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
+	args.Hostname = "some-host"
+
+	verifyTemplate(t, "snapshot-report.txt", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "snapshot-report.html", ".default", args, defaultTestOptions)
+	verifyTemplate(t, "snapshot-report.txt", ".alt", args, altTestOptions)
+	verifyTemplate(t, "snapshot-report.html", ".alt", args, altTestOptions)
+}
+
+func verifyTemplate(t *testing.T, embeddedTemplateName, expectedSuffix string, args interface{}, opt notifytemplate.Options) {
+	t.Helper()
+
+	tmpl, err := notifytemplate.GetEmbeddedTemplate(embeddedTemplateName)
+	require.NoError(t, err)
+
+	tt, err := notifytemplate.ParseTemplate(tmpl, opt)
+	require.NoError(t, err)
+
+	var buf bytes.Buffer
+
+	require.NoError(t, tt.Execute(&buf, args))
+
+	actualFileName := filepath.Join("testdata", embeddedTemplateName+expectedSuffix+".actual")
+	require.NoError(t, os.WriteFile(actualFileName, buf.Bytes(), 0o644))
+
+	expectedFileName := filepath.Join("testdata", embeddedTemplateName+expectedSuffix+".expected")
+
+	wantBytes, err := os.ReadFile(expectedFileName)
+	require.NoError(t, err)
+
+	want := string(wantBytes)
+
+	require.Equal(t, want, buf.String())
+
+	require.NoError(t, os.Remove(actualFileName))
+}
diff --git a/notification/notifytemplate/test-notification.html b/notification/notifytemplate/test-notification.html
index d8823c65..a9aaf6ea 100644
--- a/notification/notifytemplate/test-notification.html
+++ b/notification/notifytemplate/test-notification.html
@@ -1,4 +1,4 @@
-Subject: Test notification from Kopia at {{ .EventTime }}
+Subject: Test notification from Kopia at {{ .EventTime | formatTime }}
 
 <p>This is a test notification from Kopia.</p>
 
diff --git a/notification/notifytemplate/test-notification.txt b/notification/notifytemplate/test-notification.txt
index e364e269..6624b03e 100644
--- a/notification/notifytemplate/test-notification.txt
+++ b/notification/notifytemplate/test-notification.txt
@@ -1,4 +1,4 @@
-Subject: Test notification from Kopia at {{ .EventTime }}
+Subject: Test notification from Kopia at {{ .EventTime | formatTime }}
 
 This is a test notification from Kopia.
 
diff --git a/notification/notifytemplate/testdata/.gitignore b/notification/notifytemplate/testdata/.gitignore
new file mode 100644
index 00000000..e69de29b
diff --git a/notification/notifytemplate/testdata/generic-error.html.alt.expected b/notification/notifytemplate/testdata/generic-error.html.alt.expected
new file mode 100644
index 00000000..0d8f0013
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.html.alt.expected
@@ -0,0 +1,20 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+<!doctype html>
+<html>
+<head>
+</head>
+<body>
+
+<p><b>Operation:</b> Some Operation Details</p>
+<p><b>Started:</b> Wed, 01 Jan 2020 19:04:05 PST</p>
+<p><b>Finished:</b> Wed, 01 Jan 2020 19:04:06 PST (1s)</p>
+
+<p><b>Message:</b> error message</p>
+
+<pre>error details</pre>
+
+<p>Generated at Wed, 01 Jan 2020 19:04:05 PST by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/generic-error.html.default.expected b/notification/notifytemplate/testdata/generic-error.html.default.expected
new file mode 100644
index 00000000..9faf8d9d
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.html.default.expected
@@ -0,0 +1,20 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+<!doctype html>
+<html>
+<head>
+</head>
+<body>
+
+<p><b>Operation:</b> Some Operation Details</p>
+<p><b>Started:</b> Thu, 02 Jan 2020 03:04:05 +0000</p>
+<p><b>Finished:</b> Thu, 02 Jan 2020 03:04:06 +0000 (1s)</p>
+
+<p><b>Message:</b> error message</p>
+
+<pre>error details</pre>
+
+<p>Generated at Thu, 02 Jan 2020 03:04:05 +0000 by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/generic-error.txt.alt.expected b/notification/notifytemplate/testdata/generic-error.txt.alt.expected
new file mode 100644
index 00000000..170cdcdc
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.txt.alt.expected
@@ -0,0 +1,11 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+Operation: Some Operation Details
+Started:   Wed, 01 Jan 2020 19:04:05 PST
+Finished:  Wed, 01 Jan 2020 19:04:06 PST (1s)
+
+error details
+
+Generated at Wed, 01 Jan 2020 19:04:05 PST by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/testdata/generic-error.txt.default.expected b/notification/notifytemplate/testdata/generic-error.txt.default.expected
new file mode 100644
index 00000000..4f03e75b
--- /dev/null
+++ b/notification/notifytemplate/testdata/generic-error.txt.default.expected
@@ -0,0 +1,11 @@
+Subject: Kopia has encountered an error during Some Operation on some-host
+
+Operation: Some Operation Details
+Started:   Thu, 02 Jan 2020 03:04:05 +0000
+Finished:  Thu, 02 Jan 2020 03:04:06 +0000 (1s)
+
+error details
+
+Generated at Thu, 02 Jan 2020 03:04:05 +0000 by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/testdata/snapshot-report.html.alt.expected b/notification/notifytemplate/testdata/snapshot-report.html.alt.expected
new file mode 100644
index 00000000..11647e19
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.html.alt.expected
@@ -0,0 +1,119 @@
+Subject: Kopia created 2 snapshots on some-host
+
+<!doctype html>
+<html>
+<head>
+<style type="text/css">
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    thead tr {
+        background-color: #f2f2f2;
+    }
+
+    th, td {
+        border: 1px solid black;
+        padding: 2px;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f2f2f2;
+    }
+
+    code {
+        background-color: #f2f2f2;
+        padding: 2px;
+    }
+
+    span.path {
+        font-family: monospace;
+        color: #344652;
+        font-weight: bold;
+    }
+
+    tr.snapshotstatus-fatal {
+        background-color: #fde9e4;
+    }
+
+    tr.snapshotstatus-error {
+        background-color: #fcffba;
+    }
+
+    tr.snapshotstatus-incomplete {
+        background-color: #8a8c7e;
+    }
+</style>
+</head>
+<body>
+<table border="1">
+<thead>
+    <tr>
+        <th>Source</th>
+        <th>Started</th>
+        <th>Duration</th>
+        <th>Total Size</th>
+        <th>Total Files</th>
+        <th>Total Directories</th>
+    </tr>
+</thead>
+
+<tr class="snapshotstatus-fatal">
+<td><span class="path">/some/other/path</span></td>
+<td>Wed, 31 Dec 1969 16:00:00 PST</td>
+<td>0s</td>
+<td>0 B</td>
+<td>0</td>
+<td>0</td>
+</tr>
+
+
+<tr class="snapshotstatus-fatal">
+    <td colspan="6">
+        <b style="color:red">Error:</b> some top-level error
+    </td>
+</tr>
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+<td><span class="path">/some/path</span></td>
+<td>Wed, 01 Jan 2020 19:04:05 PST</td>
+<td>1.000000001s</td>
+<td>456 B</td>
+<td>123</td>
+<td>33</td>
+</tr>
+
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+    <td colspan="6">
+        <b style="color:red">Failed Entries:</b>
+        <ul>
+        
+        <li><span class="path">/some/path</span>: some error</li>
+        
+        <li><span class="path">/some/path2</span>: some error</li>
+        
+        </ul>
+    </td>
+</tr>
+
+
+
+
+
+</table>
+
+<p>Generated at Wed, 01 Jan 2020 19:04:05 PST by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/snapshot-report.html.default.expected b/notification/notifytemplate/testdata/snapshot-report.html.default.expected
new file mode 100644
index 00000000..5cf75fb2
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.html.default.expected
@@ -0,0 +1,119 @@
+Subject: Kopia created 2 snapshots on some-host
+
+<!doctype html>
+<html>
+<head>
+<style type="text/css">
+    table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    thead tr {
+        background-color: #f2f2f2;
+    }
+
+    th, td {
+        border: 1px solid black;
+        padding: 2px;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f2f2f2;
+    }
+
+    code {
+        background-color: #f2f2f2;
+        padding: 2px;
+    }
+
+    span.path {
+        font-family: monospace;
+        color: #344652;
+        font-weight: bold;
+    }
+
+    tr.snapshotstatus-fatal {
+        background-color: #fde9e4;
+    }
+
+    tr.snapshotstatus-error {
+        background-color: #fcffba;
+    }
+
+    tr.snapshotstatus-incomplete {
+        background-color: #8a8c7e;
+    }
+</style>
+</head>
+<body>
+<table border="1">
+<thead>
+    <tr>
+        <th>Source</th>
+        <th>Started</th>
+        <th>Duration</th>
+        <th>Total Size</th>
+        <th>Total Files</th>
+        <th>Total Directories</th>
+    </tr>
+</thead>
+
+<tr class="snapshotstatus-fatal">
+<td><span class="path">/some/other/path</span></td>
+<td>Thu, 01 Jan 1970 00:00:00 +0000</td>
+<td>0s</td>
+<td>0 B</td>
+<td>0</td>
+<td>0</td>
+</tr>
+
+
+<tr class="snapshotstatus-fatal">
+    <td colspan="6">
+        <b style="color:red">Error:</b> some top-level error
+    </td>
+</tr>
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+<td><span class="path">/some/path</span></td>
+<td>Thu, 02 Jan 2020 03:04:05 +0000</td>
+<td>1.000000001s</td>
+<td>456 B</td>
+<td>123</td>
+<td>33</td>
+</tr>
+
+
+
+
+
+
+<tr class="snapshotstatus-ok">
+    <td colspan="6">
+        <b style="color:red">Failed Entries:</b>
+        <ul>
+        
+        <li><span class="path">/some/path</span>: some error</li>
+        
+        <li><span class="path">/some/path2</span>: some error</li>
+        
+        </ul>
+    </td>
+</tr>
+
+
+
+
+
+</table>
+
+<p>Generated at Thu, 02 Jan 2020 03:04:05 +0000 by <a href="https://kopia.io">Kopia v0-unofficial</a>.</p>
+
+</body>
+</html>
diff --git a/notification/notifytemplate/testdata/snapshot-report.txt.alt.expected b/notification/notifytemplate/testdata/snapshot-report.txt.alt.expected
new file mode 100644
index 00000000..46de1240
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.txt.alt.expected
@@ -0,0 +1,23 @@
+Subject: Kopia created 2 snapshots on some-host
+
+Path: /some/other/path
+
+  Status: fatal
+  Start: Wed, 31 Dec 1969 16:00:00 PST Duration: 0s
+  Size: 0 B, 0 files, 0 directories.
+  Error: some top-level error
+
+Path: /some/path
+
+  Status: ok
+  Start: Wed, 01 Jan 2020 19:04:05 PST Duration: 1.000000001s
+  Size: 456 B, 123 files, 33 directories.
+
+  Failed Entries:
+
+  - /some/path: some error
+  - /some/path2: some error
+
+Generated at Wed, 01 Jan 2020 19:04:05 PST by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file
diff --git a/notification/notifytemplate/testdata/snapshot-report.txt.default.expected b/notification/notifytemplate/testdata/snapshot-report.txt.default.expected
new file mode 100644
index 00000000..b11a4e06
--- /dev/null
+++ b/notification/notifytemplate/testdata/snapshot-report.txt.default.expected
@@ -0,0 +1,23 @@
+Subject: Kopia created 2 snapshots on some-host
+
+Path: /some/other/path
+
+  Status: fatal
+  Start: Thu, 01 Jan 1970 00:00:00 +0000 Duration: 0s
+  Size: 0 B, 0 files, 0 directories.
+  Error: some top-level error
+
+Path: /some/path
+
+  Status: ok
+  Start: Thu, 02 Jan 2020 03:04:05 +0000 Duration: 1.000000001s
+  Size: 456 B, 123 files, 33 directories.
+
+  Failed Entries:
+
+  - /some/path: some error
+  - /some/path2: some error
+
+Generated at Thu, 02 Jan 2020 03:04:05 +0000 by Kopia v0-unofficial.
+
+https://kopia.io/
\ No newline at end of file