From 5fefefc50fe1b406bc4ae46b8af21ef52c3b4dfc Mon Sep 17 00:00:00 2001
From: SWZ <simon.lindgren07@icloud.com>
Date: Fri, 11 Nov 2022 16:26:27 +0100
Subject: [PATCH 01/36] Added translation using Weblate (Swedish)

---
 web/public/static/langs/sv.json | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 web/public/static/langs/sv.json

diff --git a/web/public/static/langs/sv.json b/web/public/static/langs/sv.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/web/public/static/langs/sv.json
@@ -0,0 +1 @@
+{}

From fea9d10ed22c77a28de190f75da3455e46825e18 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sat, 12 Nov 2022 06:56:40 -0500
Subject: [PATCH 02/36] Thank you @portothree for your sponsorship

---
 README.md            | 1 +
 docs/integrations.md | 1 +
 2 files changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 7ea7500b..aa41f056 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
 <a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a>
 <a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
 <a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
+<a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
 
 ## License
 Made with ❤️ by [Philipp C. Heckel](https://heckel.io).   
diff --git a/docs/integrations.md b/docs/integrations.md
index 658e6d79..4bce8c49 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -90,6 +90,7 @@ messages until I finally finish implementing end-to-end encryption.
 
 ## Blog + forum posts
 
+- [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) - 11/2022
 - [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - 11/2022
 - [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - 11/2022
 - [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - 11/2022

From 7470ffde4f160431e6513192cf8aac52365eb6bd Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sat, 12 Nov 2022 07:04:55 -0500
Subject: [PATCH 03/36] Bump deps

---
 go.mod                | 12 ++++---
 go.sum                | 14 ++++++++
 web/package-lock.json | 76 +++++++++++++++++++++----------------------
 3 files changed, 59 insertions(+), 43 deletions(-)

diff --git a/go.mod b/go.mod
index c8fb103d..81a20a1f 100644
--- a/go.mod
+++ b/go.mod
@@ -14,8 +14,8 @@ require (
 	github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
 	github.com/stretchr/testify v1.8.1
 	github.com/urfave/cli/v2 v2.23.5
-	golang.org/x/crypto v0.1.0
-	golang.org/x/oauth2 v0.1.0 // indirect
+	golang.org/x/crypto v0.2.0
+	golang.org/x/oauth2 v0.2.0 // indirect
 	golang.org/x/sync v0.1.0
 	golang.org/x/term v0.2.0
 	golang.org/x/time v0.2.0
@@ -25,17 +25,19 @@ require (
 
 require github.com/pkg/errors v0.9.1 // indirect
 
-require firebase.google.com/go/v4 v4.9.0
+require firebase.google.com/go/v4 v4.10.0
 
 require (
-	cloud.google.com/go v0.105.0 // indirect
+	cloud.google.com/go v0.106.0 // indirect
 	cloud.google.com/go/compute v1.12.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
 	cloud.google.com/go/iam v0.7.0 // indirect
 	cloud.google.com/go/longrunning v0.3.0 // indirect
 	github.com/AlekSi/pointer v1.2.0 // indirect
+	github.com/MicahParks/keyfunc v1.5.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
+	github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
@@ -52,7 +54,7 @@ require (
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/appengine/v2 v2.0.2 // indirect
-	google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd // indirect
+	google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e // indirect
 	google.golang.org/grpc v1.50.1 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index ab3dee01..302bdc88 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
 cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
+cloud.google.com/go v0.106.0 h1:AWaMWuZb2oFeiV91OfNHZbmwUhMVuXEaLPm9sqDAOl8=
+cloud.google.com/go v0.106.0/go.mod h1:5NEGxGuIeMQiPaWLwLYZ7kfNWiP6w1+QJK+xqyIT+dw=
 cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
 cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
@@ -21,12 +23,16 @@ cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzz
 cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
 firebase.google.com/go/v4 v4.9.0 h1:VCagv+hYOxUGeuyu7J+o2rKJkDp5JQBbA3Bzlof+LMk=
 firebase.google.com/go/v4 v4.9.0/go.mod h1:bHhRkM3VtGJx19rQdW7GDNLdnA8/T6SsnN5nXk/xdw8=
+firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
+firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
 github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
 github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
 github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/MicahParks/keyfunc v1.5.3 h1:Y+mv+kX3HtL7/dCXXzK4bIDBHg91eunnGGkdndO0RWk=
+github.com/MicahParks/keyfunc v1.5.3/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -46,6 +52,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
 github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
+github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
+github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -122,6 +130,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
+golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -142,6 +152,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
 golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
+golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
+golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
 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=
@@ -197,6 +209,8 @@ google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+C
 google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
 google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd h1:1eV6KuDTxraYYsYGWksp1thEGP+8dtX/TINL9h+ppiI=
 google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M=
+google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/web/package-lock.json b/web/package-lock.json
index d4338d92..2fd105be 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -3370,9 +3370,9 @@
       }
     },
     "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
-      "version": "0.5.8",
-      "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.8.tgz",
-      "integrity": "sha512-wxXRwf+IQ6zvHSJZ+5T2RQNEsq+kx4jKRXfFvdt3nBIUzJUAvXEFsUeoaohDe/Kr84MTjGwcuIUPNcstNJORsA==",
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.9.tgz",
+      "integrity": "sha512-7QV4cqUwhkDIHpMAZ9mestSJ2DMIotVTbOUwbiudhjCRTAWWKIaBecELiEM2LT3AHFeOAaHIcFu4dbXjX+9GBA==",
       "dependencies": {
         "ansi-html-community": "^0.0.8",
         "common-path-prefix": "^3.0.0",
@@ -3380,7 +3380,7 @@
         "error-stack-parser": "^2.0.6",
         "find-up": "^5.0.0",
         "html-entities": "^2.1.0",
-        "loader-utils": "^2.0.0",
+        "loader-utils": "^2.0.3",
         "schema-utils": "^3.0.0",
         "source-map": "^0.7.3"
       },
@@ -11512,9 +11512,9 @@
       }
     },
     "node_modules/loader-utils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
-      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+      "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
       "dependencies": {
         "big.js": "^5.2.2",
         "emojis-list": "^3.0.0",
@@ -12495,9 +12495,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.18",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
-      "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
+      "version": "8.4.19",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
+      "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
       "funding": [
         {
           "type": "opencollective",
@@ -14032,9 +14032,9 @@
       }
     },
     "node_modules/react-dev-utils/node_modules/loader-utils": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
-      "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
+      "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
       "engines": {
         "node": ">= 12.13.0"
       }
@@ -15500,9 +15500,9 @@
       "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
     },
     "node_modules/tailwindcss": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.2.tgz",
-      "integrity": "sha512-c2GtSdqg+harR4QeoTmex0Ngfg8IIHNeLQH5yr2B9uZbZR1Xt1rYbjWOWTcj3YLTZhrmZnPowoQDbSRFyZHQ5Q==",
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
+      "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==",
       "dependencies": {
         "arg": "^5.0.2",
         "chokidar": "^3.5.3",
@@ -16142,9 +16142,9 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.74.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz",
-      "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==",
+      "version": "5.75.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
+      "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^0.0.51",
@@ -19204,9 +19204,9 @@
       }
     },
     "@pmmmwh/react-refresh-webpack-plugin": {
-      "version": "0.5.8",
-      "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.8.tgz",
-      "integrity": "sha512-wxXRwf+IQ6zvHSJZ+5T2RQNEsq+kx4jKRXfFvdt3nBIUzJUAvXEFsUeoaohDe/Kr84MTjGwcuIUPNcstNJORsA==",
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.9.tgz",
+      "integrity": "sha512-7QV4cqUwhkDIHpMAZ9mestSJ2DMIotVTbOUwbiudhjCRTAWWKIaBecELiEM2LT3AHFeOAaHIcFu4dbXjX+9GBA==",
       "requires": {
         "ansi-html-community": "^0.0.8",
         "common-path-prefix": "^3.0.0",
@@ -19214,7 +19214,7 @@
         "error-stack-parser": "^2.0.6",
         "find-up": "^5.0.0",
         "html-entities": "^2.1.0",
-        "loader-utils": "^2.0.0",
+        "loader-utils": "^2.0.3",
         "schema-utils": "^3.0.0",
         "source-map": "^0.7.3"
       },
@@ -25126,9 +25126,9 @@
       "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="
     },
     "loader-utils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
-      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+      "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
       "requires": {
         "big.js": "^5.2.2",
         "emojis-list": "^3.0.0",
@@ -25831,9 +25831,9 @@
       }
     },
     "postcss": {
-      "version": "8.4.18",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
-      "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
+      "version": "8.4.19",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
+      "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
       "requires": {
         "nanoid": "^3.3.4",
         "picocolors": "^1.0.0",
@@ -26762,9 +26762,9 @@
           "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
         },
         "loader-utils": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
-          "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ=="
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
+          "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw=="
         },
         "supports-color": {
           "version": "7.2.0",
@@ -27854,9 +27854,9 @@
       "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
     },
     "tailwindcss": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.2.tgz",
-      "integrity": "sha512-c2GtSdqg+harR4QeoTmex0Ngfg8IIHNeLQH5yr2B9uZbZR1Xt1rYbjWOWTcj3YLTZhrmZnPowoQDbSRFyZHQ5Q==",
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
+      "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==",
       "requires": {
         "arg": "^5.0.2",
         "chokidar": "^3.5.3",
@@ -28319,9 +28319,9 @@
       "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
     },
     "webpack": {
-      "version": "5.74.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz",
-      "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==",
+      "version": "5.75.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
+      "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
       "requires": {
         "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^0.0.51",

From 019e69ec857ac801749c625688294b272db3f985 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sat, 12 Nov 2022 08:36:05 -0500
Subject: [PATCH 04/36] Added more projects

---
 docs/integrations.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/integrations.md b/docs/integrations.md
index 9e6be590..7275553c 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -88,6 +88,8 @@ messages until I finally finish implementing end-to-end encryption.
 - [ntfy-to-slack](https://github.com/ozskywalker/ntfy-to-slack) - Tool to subscribe to a ntfy topic and send the messages to a Slack webhook (Go)
 - [ansible-ntfy](https://github.com/jpmens/ansible-ntfy) - Ansible action plugin to post JSON messages to ntfy (Python)
 - [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP)
+- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
+- [ntfy-sdk](https://gitlab.com/p2kishimoto/ntfy-sdk) - ntfy client library to send notifications (Rust)
 
 ## Blog + forum posts
 

From 651c701b9dd7a961c17af2b896cfc90fe7487570 Mon Sep 17 00:00:00 2001
From: SWZ <simon.lindgren07@icloud.com>
Date: Fri, 11 Nov 2022 15:28:46 +0000
Subject: [PATCH 05/36] Translated using Weblate (Swedish)

Currently translated at 21.6% (41 of 189 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
---
 web/public/static/langs/sv.json | 46 ++++++++++++++++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

diff --git a/web/public/static/langs/sv.json b/web/public/static/langs/sv.json
index 0967ef42..e857ed2b 100644
--- a/web/public/static/langs/sv.json
+++ b/web/public/static/langs/sv.json
@@ -1 +1,45 @@
-{}
+{
+    "action_bar_settings": "Inställningar",
+    "action_bar_send_test_notification": "Skicka test notis",
+    "action_bar_toggle_action_menu": "Öppna/stäng åtgärdsmeny",
+    "message_bar_type_message": "Skriv ett meddelande här",
+    "message_bar_error_publishing": "Fel vid publicering av notis",
+    "message_bar_show_dialog": "Visa publicerings dialog",
+    "message_bar_publish": "Publicera meddelande",
+    "nav_topics_title": "Prenumererade kategorier",
+    "nav_button_all_notifications": "Alla notiser",
+    "nav_button_documentation": "Dokumentation",
+    "nav_button_publish_message": "Publicera notis",
+    "nav_button_subscribe": "Prenumerera på kategori",
+    "alert_grant_title": "Notiser är avstängda",
+    "alert_grant_button": "Bevilja nu",
+    "alert_not_supported_title": "Notiser stöds inte",
+    "notifications_list": "Notis-lista",
+    "notifications_list_item": "Notis",
+    "notifications_delete": "Radera",
+    "notifications_copied_to_clipboard": "Kopierat till urklipp",
+    "notifications_tags": "Taggar",
+    "notifications_new_indicator": "Ny notis",
+    "notifications_attachment_copy_url_title": "Kopiera bifogad URL till urklipp",
+    "notifications_attachment_copy_url_button": "Kopiera URL",
+    "notifications_attachment_open_title": "Gå till {{url}}",
+    "notifications_attachment_open_button": "Öppna bilagan",
+    "notifications_attachment_link_expired": "Nedladdningslänk utgått",
+    "notifications_priority_x": "Prioritet {{priority}}",
+    "action_bar_show_menu": "Visa meny",
+    "action_bar_logo_alt": "ntfy logga",
+    "action_bar_unsubscribe": "Avprenumerera",
+    "action_bar_toggle_mute": "Tysta/aktivera notiser",
+    "action_bar_clear_notifications": "Rensa alla notiser",
+    "nav_button_connecting": "ansluter",
+    "notifications_attachment_image": "Bifogad bild",
+    "nav_button_settings": "Inställningar",
+    "nav_button_muted": "Notiser tystade",
+    "notifications_attachment_link_expires": "länken utgår {{date}}",
+    "notifications_attachment_file_image": "bild fil",
+    "notifications_attachment_file_audio": "ljud fil",
+    "alert_grant_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.",
+    "alert_not_supported_description": "Notiser stöds inte i din webbläsare.",
+    "notifications_mark_read": "Markera som läst",
+    "notifications_attachment_file_video": "video fil"
+}

From 4bcbea32abe127db4a1cc1d3b99c95a0adf2f7e4 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sat, 12 Nov 2022 14:05:56 -0500
Subject: [PATCH 06/36] Bump

---
 docs/install.md       | 60 +++++++++++++++++++++----------------------
 docs/releases.md      | 12 ++++++++-
 go.sum                | 37 --------------------------
 web/package-lock.json | 12 ++++-----
 4 files changed, 47 insertions(+), 74 deletions(-)

diff --git a/docs/install.md b/docs/install.md
index cc1ef8bc..034c88d3 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -26,37 +26,37 @@ deb/rpm packages.
 
 === "x86_64/amd64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_x86_64.tar.gz
-    tar zxvf ntfy_1.28.0_linux_x86_64.tar.gz
-    sudo cp -a ntfy_1.28.0_linux_x86_64/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_x86_64.tar.gz
+    tar zxvf ntfy_1.29.0_linux_x86_64.tar.gz
+    sudo cp -a ntfy_1.29.0_linux_x86_64/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
 === "armv6"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.tar.gz
-    tar zxvf ntfy_1.28.0_linux_armv6.tar.gz
-    sudo cp -a ntfy_1.28.0_linux_armv6/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv6/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.tar.gz
+    tar zxvf ntfy_1.29.0_linux_armv6.tar.gz
+    sudo cp -a ntfy_1.29.0_linux_armv6/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_armv6/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
 === "armv7/armhf"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.tar.gz
-    tar zxvf ntfy_1.28.0_linux_armv7.tar.gz
-    sudo cp -a ntfy_1.28.0_linux_armv7/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv7/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.tar.gz
+    tar zxvf ntfy_1.29.0_linux_armv7.tar.gz
+    sudo cp -a ntfy_1.29.0_linux_armv7/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_armv7/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
 === "arm64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.tar.gz
-    tar zxvf ntfy_1.28.0_linux_arm64.tar.gz
-    sudo cp -a ntfy_1.28.0_linux_arm64/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_arm64/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.tar.gz
+    tar zxvf ntfy_1.29.0_linux_arm64.tar.gz
+    sudo cp -a ntfy_1.29.0_linux_arm64/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_arm64/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
@@ -106,7 +106,7 @@ Manually installing the .deb file:
 
 === "x86_64/amd64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_amd64.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_amd64.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -114,7 +114,7 @@ Manually installing the .deb file:
 
 === "armv6"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -122,7 +122,7 @@ Manually installing the .deb file:
 
 === "armv7/armhf"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -130,7 +130,7 @@ Manually installing the .deb file:
 
 === "arm64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -140,28 +140,28 @@ Manually installing the .deb file:
 
 === "x86_64/amd64"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_amd64.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_amd64.rpm
     sudo systemctl enable ntfy 
     sudo systemctl start ntfy
     ```
 
 === "armv6"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.rpm
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
     ```
 
 === "armv7/armhf"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.rpm
     sudo systemctl enable ntfy 
     sudo systemctl start ntfy
     ```
 
 === "arm64"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.rpm
     sudo systemctl enable ntfy 
     sudo systemctl start ntfy
     ```
@@ -189,18 +189,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
 
 ## macOS
 The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. 
-To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_macOS_all.tar.gz), 
+To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_macOS_all.tar.gz), 
 extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). 
 
 If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at 
 `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
 
 ```bash
-curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_macOS_all.tar.gz > ntfy_1.28.0_macOS_all.tar.gz
-tar zxvf ntfy_1.28.0_macOS_all.tar.gz
-sudo cp -a ntfy_1.28.0_macOS_all/ntfy /usr/local/bin/ntfy
+curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_macOS_all.tar.gz > ntfy_1.29.0_macOS_all.tar.gz
+tar zxvf ntfy_1.29.0_macOS_all.tar.gz
+sudo cp -a ntfy_1.29.0_macOS_all/ntfy /usr/local/bin/ntfy
 mkdir ~/Library/Application\ Support/ntfy 
-cp ntfy_1.28.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
+cp ntfy_1.29.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
 ntfy --help
 ```
 
@@ -212,7 +212,7 @@ ntfy --help
 
 ## Windows
 The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
-To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_windows_x86_64.zip),
+To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_windows_x86_64.zip),
 extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. 
 
 The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
diff --git a/docs/releases.md b/docs/releases.md
index 08e903c0..d7990a6e 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -2,14 +2,24 @@
 Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
 and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
 
+<!--
 ## ntfy Android app v1.14.0 (UNRELEASED)
 
 **Additional translations:**
 
 * Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
+-->
 
+## ntfy server v1.29.0
+Released November 12, 2022
 
-## ntfy server v1.29.0 (UNRELEASED)
+This release adds the ability to add rate limit exemptions for IP ranges instead of just specific IP addresses. It also fixes 
+a few bugs in the web app and the CLI and adds lots of new examples and install instructions.
+
+Thanks to [some love on HN](https://news.ycombinator.com/item?id=33517944), we got so many new ntfy users trying out ntfy
+and joining the [chat rooms](https://github.com/binwiederhier/ntfy#chat--forum). **Welcome to the ntfy community to all of you!** 
+We also got a ton of new **[sponsors and donations](https://github.com/sponsors/binwiederhier)** 💸, which is amazing. I'd like to thank
+all of you for believing in the project, and for helping me pay the server cost. The HN spike increased the AWS cost quite a bit.
 
 **Features:**
 
diff --git a/go.sum b/go.sum
index 302bdc88..8c269ead 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,4 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
-cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go v0.106.0 h1:AWaMWuZb2oFeiV91OfNHZbmwUhMVuXEaLPm9sqDAOl8=
 cloud.google.com/go v0.106.0/go.mod h1:5NEGxGuIeMQiPaWLwLYZ7kfNWiP6w1+QJK+xqyIT+dw=
 cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
@@ -9,20 +7,12 @@ cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22m
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/firestore v1.8.0 h1:HokMB9Io0hAyYzlGFeFVMgE3iaPXNvaIsDx5JzblGLI=
 cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=
-cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ=
-cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
 cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
 cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
-cloud.google.com/go/longrunning v0.2.1 h1:x3E/YapFCMe2G1D9qCv9COrBldOwK/n0OC7w9PLzeX0=
-cloud.google.com/go/longrunning v0.2.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE=
 cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
 cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
-cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
-cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
 cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
 cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
-firebase.google.com/go/v4 v4.9.0 h1:VCagv+hYOxUGeuyu7J+o2rKJkDp5JQBbA3Bzlof+LMk=
-firebase.google.com/go/v4 v4.9.0/go.mod h1:bHhRkM3VtGJx19rQdW7GDNLdnA8/T6SsnN5nXk/xdw8=
 firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
 firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
 github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
@@ -87,8 +77,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
-github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU=
-github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
 github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
 github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -109,27 +97,18 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/urfave/cli/v2 v2.23.0 h1:pkly7gKIeYv3olPAeNajNpLjeJrmTPYCoZWaV+2VfvE=
-github.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
 github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
 github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
-golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
 golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -145,13 +124,9 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
-golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
 golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
 golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -165,13 +140,9 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
-golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -180,8 +151,6 @@ 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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
-golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
 golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -192,8 +161,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I=
-google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
 google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
 google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -205,10 +172,6 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
-google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
-google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd h1:1eV6KuDTxraYYsYGWksp1thEGP+8dtX/TINL9h+ppiI=
-google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M=
 google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
diff --git a/web/package-lock.json b/web/package-lock.json
index 2fd105be..3ff6b519 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -5815,9 +5815,9 @@
       "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
     },
     "node_modules/cosmiconfig": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
-      "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
       "dependencies": {
         "@types/parse-json": "^4.0.0",
         "import-fresh": "^3.2.1",
@@ -21021,9 +21021,9 @@
       "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
     },
     "cosmiconfig": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
-      "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
       "requires": {
         "@types/parse-json": "^4.0.0",
         "import-fresh": "^3.2.1",

From a43a4aea5ec437d5ddd00038a7ec038b204ff26d Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sat, 12 Nov 2022 14:41:28 -0500
Subject: [PATCH 07/36] Docs

---
 docs/integrations.md | 2 ++
 server/server.yml    | 5 +++--
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/docs/integrations.md b/docs/integrations.md
index 7275553c..c1ff77d1 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -93,6 +93,8 @@ messages until I finally finish implementing end-to-end encryption.
 
 ## Blog + forum posts
 
+- [Envie Push Notifications por POST (de graça e sem cadastro)](https://www.tabnews.com.br/filipedeschamps/envie-push-notifications-por-post-de-graca-e-sem-cadastro) - 11/2022
+- [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - 11/2022
 - [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) - 11/2022
 - [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - 11/2022
 - [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - 11/2022
diff --git a/server/server.yml b/server/server.yml
index 245bc4da..9476478f 100644
--- a/server/server.yml
+++ b/server/server.yml
@@ -173,8 +173,9 @@
 # Rate limiting: Allowed GET/PUT/POST requests per second, per visitor:
 # - visitor-request-limit-burst is the initial bucket of requests each visitor has
 # - visitor-request-limit-replenish is the rate at which the bucket is refilled
-# - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames and IPs to be
-#   exempt from request rate limiting; hostnames are resolved at the time the server is started
+# - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames, IPs or CIDRs to be
+#   exempt from request rate limiting. Hostnames are resolved at the time the server is started.
+#   Example: "1.2.3.4,ntfy.example.com,8.7.6.0/24"
 #
 # visitor-request-limit-burst: 60
 # visitor-request-limit-replenish: "5s"

From b103caf9d410b59afbd2f04b1670c1afeaf48f09 Mon Sep 17 00:00:00 2001
From: ksurl <ksurl@users.noreply.github.com>
Date: Sat, 12 Nov 2022 13:05:19 -0800
Subject: [PATCH 08/36] add github actions example

---
 docs/examples.md | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/docs/examples.md b/docs/examples.md
index 4ac7a4b2..b9adf443 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -122,6 +122,19 @@ to ntfy at its default URL (`attrs` and other attributes are optional):
           priority: 1
 ```
 
+## GitHub Actions
+You can send a message during a workflow run with curl. Here is an example sending info about the repo, commit and job status.
+``` yaml
+- name: Actions Ntfy
+  run: |
+    curl \
+      -u ${{ secrets.NTFY_CRED }} \
+      -H "Title: Title here" \
+      -H "Content-Type: text/plain" \
+      -d $'Repo: ${{ github.repository }}\nCommit: ${{ github.sha }}\nRef: ${{ github.ref }}\nStatus: ${{ job.status}}' \
+      ${{ secrets.NTFY_URL }}
+```
+
 ## Watchtower (shoutrrr)
 You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send 
 [Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.

From e236214fd52ac81fd07babf43e29d61c2a7446f5 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 06:24:57 -0500
Subject: [PATCH 09/36] Add post

---
 docs/integrations.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/integrations.md b/docs/integrations.md
index c1ff77d1..5329766b 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -93,6 +93,7 @@ messages until I finally finish implementing end-to-end encryption.
 
 ## Blog + forum posts
 
+- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - 11/2022
 - [Envie Push Notifications por POST (de graça e sem cadastro)](https://www.tabnews.com.br/filipedeschamps/envie-push-notifications-por-post-de-graca-e-sem-cadastro) - 11/2022
 - [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - 11/2022
 - [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) - 11/2022

From bcc20e0aec31eb443d543a3223f44ecb7702680c Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 06:28:10 -0500
Subject: [PATCH 10/36] Release notes

---
 docs/releases.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/docs/releases.md b/docs/releases.md
index d7990a6e..0f5add76 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -5,6 +5,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 <!--
 ## ntfy Android app v1.14.0 (UNRELEASED)
 
+**Bug fixes:**
+
+* Remove timestamp when copying message text ([#471](https://github.com/binwiederhier/ntfy/issues/471), thanks to [@wunter8](https://github.com/wunter8))
+
 **Additional translations:**
 
 * Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))

From cf7a451198aeaf092e52c269b1296958820dece0 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 06:41:26 -0500
Subject: [PATCH 11/36] Release notes

---
 docs/releases.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/docs/releases.md b/docs/releases.md
index 0f5add76..eaa09841 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -14,6 +14,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 * Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
 -->
 
+## ntfy server v1.30.0 (UNRELREASED)
+
+**Documentation:**
+
+* GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl))
+
 ## ntfy server v1.29.0
 Released November 12, 2022
 

From 08bb0103e86b302eba5eaddc3e7559600ddd829c Mon Sep 17 00:00:00 2001
From: Jonathan Carroll <github@jcarroll.com.au>
Date: Sun, 13 Nov 2022 14:07:27 -0800
Subject: [PATCH 12/36] add R wrapper

---
 docs/integrations.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/integrations.md b/docs/integrations.md
index 5329766b..008d37c1 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -47,6 +47,7 @@ messages until I finally finish implementing end-to-end encryption.
 - [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python)
 - [ntfy-dotnet](https://github.com/nwithan8/ntfy-dotnet) - .NET client library to interact with a ntfy server (C# / .NET)
 - [node-ntfy-publish](https://github.com/cityssm/node-ntfy-publish) - A Node package to publish notifications to an ntfy server (Node)
+- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
 
 ## CLIs + GUIs
 

From aff193a0036d0120c515e301d7e06f3ce865728a Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 20:59:12 -0500
Subject: [PATCH 13/36] Testing docs workflow (1)

---
 .github/workflows/docs.yaml | 36 ++++++++++++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)
 create mode 100644 .github/workflows/docs.yaml

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
new file mode 100644
index 00000000..2a744a3a
--- /dev/null
+++ b/.github/workflows/docs.yaml
@@ -0,0 +1,36 @@
+name: docs
+on: [push, pull_request]
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      -
+        name: Checkout ntfy code
+        uses: actions/checkout@v3
+        with:
+          path: ntfy
+      -
+        name: Checkout docs pages code
+        uses: actions/checkout@v3
+        with:
+          repository: binwiederhier/ntfy-docs.github.io
+          path: ntfy-docs.github.io
+      -
+        name: Build docs
+        run: |
+          pwd
+          ls
+          cd ntfy
+          make docs
+          rsync -av --delete docs/ ../ntfy-docs.github.io/docs/
+      -
+        name: Publish docs
+        run: |
+          pwd
+          ls
+          cd ntfy-docs.github.io
+          git config user.name "GitHub Actions Bot"
+          git config user.email "<>"          
+          git add docs/
+          git commit -m "Updated docs"
+          git push origin main

From 069617eba0f67425c1312622502da0009310dfdd Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:05:03 -0500
Subject: [PATCH 14/36] Testing docs workflow (2)

---
 .github/workflows/docs.yaml | 18 ++++++------------
 1 file changed, 6 insertions(+), 12 deletions(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 2a744a3a..238b99dd 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -7,28 +7,22 @@ jobs:
       -
         name: Checkout ntfy code
         uses: actions/checkout@v3
-        with:
-          path: ntfy
       -
         name: Checkout docs pages code
         uses: actions/checkout@v3
         with:
           repository: binwiederhier/ntfy-docs.github.io
-          path: ntfy-docs.github.io
+          path: build/ntfy-docs.github.io
       -
         name: Build docs
-        run: |
-          pwd
-          ls
-          cd ntfy
-          make docs
-          rsync -av --delete docs/ ../ntfy-docs.github.io/docs/
+        run: make docs
+      -
+        name: Copy generated docs
+        run: rsync -av --delete docs/ build/ntfy-docs.github.io/docs/
       -
         name: Publish docs
         run: |
-          pwd
-          ls
-          cd ntfy-docs.github.io
+          cd build/ntfy-docs.github.io
           git config user.name "GitHub Actions Bot"
           git config user.email "<>"          
           git add docs/

From f4cb447f0a57f0cb82f0bcc9a9083d87a7f96761 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:08:25 -0500
Subject: [PATCH 15/36] Testing docs workflow (3)

---
 .github/workflows/docs.yaml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 238b99dd..88c7cf46 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -15,10 +15,12 @@ jobs:
           path: build/ntfy-docs.github.io
       -
         name: Build docs
-        run: make docs
+        run: | 
+          make docs
+          ls server/docs/
       -
         name: Copy generated docs
-        run: rsync -av --delete docs/ build/ntfy-docs.github.io/docs/
+        run: rsync -av --delete server/docs/ build/ntfy-docs.github.io/docs/
       -
         name: Publish docs
         run: |

From 18d36e1b30c1e6c63a7b42350c99649e41fcce14 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:11:51 -0500
Subject: [PATCH 16/36] Testing docs workflow (4)

---
 .github/workflows/docs.yaml | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 88c7cf46..a974fb68 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -1,7 +1,7 @@
 name: docs
 on: [push, pull_request]
 jobs:
-  build:
+  docs:
     runs-on: ubuntu-latest
     steps:
       -
@@ -20,7 +20,12 @@ jobs:
           ls server/docs/
       -
         name: Copy generated docs
-        run: rsync -av --delete server/docs/ build/ntfy-docs.github.io/docs/
+        run: | 
+          echo "New docs:"
+          ls -1 server/docs/
+          echo "Existing docs:"
+          ls -1 build/ntfy-docs.github.io/docs/
+          rsync -av --delete server/docs/ build/ntfy-docs.github.io/docs/
       -
         name: Publish docs
         run: |

From b92b5b37fb9ece825283d3ba2da89100570b1843 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:23:25 -0500
Subject: [PATCH 17/36] Testing docs workflow (5)

---
 .github/workflows/docs.yaml | 12 +++---------
 1 file changed, 3 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index a974fb68..2777d301 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -13,19 +13,13 @@ jobs:
         with:
           repository: binwiederhier/ntfy-docs.github.io
           path: build/ntfy-docs.github.io
+          token: ${{secrets.NTFY_DOCS_PUBLISH_TOKEN}}
       -
         name: Build docs
-        run: | 
-          make docs
-          ls server/docs/
+        run: make docs
       -
         name: Copy generated docs
-        run: | 
-          echo "New docs:"
-          ls -1 server/docs/
-          echo "Existing docs:"
-          ls -1 build/ntfy-docs.github.io/docs/
-          rsync -av --delete server/docs/ build/ntfy-docs.github.io/docs/
+        run: rsync -av --exclude CNAME --delete server/docs/ build/ntfy-docs.github.io/docs/
       -
         name: Publish docs
         run: |

From adda27ec577efa9b9e46b89cc3a06b4d5d47abd7 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:33:27 -0500
Subject: [PATCH 18/36] Rename secret token

---
 .github/workflows/docs.yaml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 2777d301..dee19c6f 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -13,7 +13,9 @@ jobs:
         with:
           repository: binwiederhier/ntfy-docs.github.io
           path: build/ntfy-docs.github.io
-          token: ${{secrets.NTFY_DOCS_PUBLISH_TOKEN}}
+          token: ${{secrets.NTFY_DOCS_PUSH_TOKEN}}
+          # Expires after 1 year, re-generate via
+          # User -> Settings -> Developer options -> Personal Access Tokens -> Fine Grained Token
       -
         name: Build docs
         run: make docs

From 61b2d925950332ceb1a5cd082efe2ddd38050829 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:39:36 -0500
Subject: [PATCH 19/36] Update "on:" config

---
 .github/workflows/docs.yaml | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index dee19c6f..acd637bb 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -1,7 +1,11 @@
 name: docs
-on: [push, pull_request]
+on:
+  push:
+    branches:
+      - main
+      - publish-docs
 jobs:
-  docs:
+  publish-docs:
     runs-on: ubuntu-latest
     steps:
       -

From df45459618ff03c159111eccecd988cb61f50c4d Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 13 Nov 2022 21:40:39 -0500
Subject: [PATCH 20/36] Remove test branch

---
 .github/workflows/docs.yaml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index acd637bb..2ba9b9c6 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -3,7 +3,6 @@ on:
   push:
     branches:
       - main
-      - publish-docs
 jobs:
   publish-docs:
     runs-on: ubuntu-latest

From a0f2d81337f54fd0ede6679bbff43f60a349f34f Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Mon, 14 Nov 2022 06:52:41 -0500
Subject: [PATCH 21/36] Release notes

---
 docs/releases.md | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/docs/releases.md b/docs/releases.md
index eaa09841..48c396bd 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -2,7 +2,6 @@
 Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
 and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
 
-<!--
 ## ntfy Android app v1.14.0 (UNRELEASED)
 
 **Bug fixes:**
@@ -12,7 +11,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Additional translations:**
 
 * Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
--->
 
 ## ntfy server v1.30.0 (UNRELREASED)
 
@@ -20,6 +18,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 * GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl))
 
+**Other things:**
+
+* Put ntfy.sh/docs assets on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491))
+
 ## ntfy server v1.29.0
 Released November 12, 2022
 

From e7b575badcc793c85c9e8c66e62c8a68801a06f4 Mon Sep 17 00:00:00 2001
From: bt90 <btom1990@googlemail.com>
Date: Mon, 14 Nov 2022 19:38:55 +0100
Subject: [PATCH 22/36] Add UnifiedPush section

---
 docs/config.md | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/docs/config.md b/docs/config.md
index babb5839..fd367f00 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -309,6 +309,22 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
     ]));
     ```
 
+### UnifiedPush compatibility
+
+UnifiedPush requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …) has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages. The topic names used by UnifiedPush all start with the `up` prefix. You should either allow anonymous write access for the entire prefix or explicitly per topic:
+
+=== "Prefix"
+    ```
+    $ ntfy access '*' 'up*' write-only
+    ```
+
+=== "Explicitly"
+    ```
+    $ ntfy access '*' 'upYzMtZGZiYTY5' write-only
+    ```
+
+see https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users
+
 ## E-mail notifications
 To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured, 
 you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g. 

From fd7f83378d74864be430e03ee7b3d34015a18d31 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Mon, 14 Nov 2022 15:21:02 -0500
Subject: [PATCH 23/36] Refine UP docs

---
 docs/config.md   | 13 ++++++++-----
 docs/releases.md |  3 ++-
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/docs/config.md b/docs/config.md
index fd367f00..655b56cf 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -309,9 +309,14 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
     ]));
     ```
 
-### UnifiedPush compatibility
+### Example: UnifiedPush
+[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …) 
+has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages. 
+The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the 
+**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details.
 
-UnifiedPush requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …) has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages. The topic names used by UnifiedPush all start with the `up` prefix. You should either allow anonymous write access for the entire prefix or explicitly per topic:
+To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either 
+allow anonymous write access for the entire prefix or explicitly per topic:
 
 === "Prefix"
     ```
@@ -320,11 +325,9 @@ UnifiedPush requires that the [application server](https://unifiedpush.org/spec/
 
 === "Explicitly"
     ```
-    $ ntfy access '*' 'upYzMtZGZiYTY5' write-only
+    $ ntfy access '*' upYzMtZGZiYTY5 write-only
     ```
 
-see https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users
-
 ## E-mail notifications
 To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured, 
 you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g. 
diff --git a/docs/releases.md b/docs/releases.md
index 48c396bd..f5fc9a49 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -17,10 +17,11 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Documentation:**
 
 * GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl))
+* UnifiedPush ACL clarification ([#497](https://github.com/binwiederhier/ntfy/issues/497), thanks to [@bt90](https://github.com/bt90)) 
 
 **Other things:**
 
-* Put ntfy.sh/docs assets on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491))
+* Put ntfy.sh docs on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491))
 
 ## ntfy server v1.29.0
 Released November 12, 2022

From 499ac76c4343b728caac5851fcb0fbc5ac288a93 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Tue, 15 Nov 2022 09:09:31 -0500
Subject: [PATCH 24/36] Thank you @finngreig for your sponsorship

---
 README.md            | 1 +
 docs/integrations.md | 1 +
 2 files changed, 2 insertions(+)

diff --git a/README.md b/README.md
index aa41f056..d9bfae0f 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,7 @@ appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
 <a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
 <a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
 <a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
+<a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
 
 ## License
 Made with ❤️ by [Philipp C. Heckel](https://heckel.io).   
diff --git a/docs/integrations.md b/docs/integrations.md
index 008d37c1..d6837a3b 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -94,6 +94,7 @@ messages until I finally finish implementing end-to-end encryption.
 
 ## Blog + forum posts
 
+- [Tracking layoffs, tech worker demand still high, ntfy, devenv, Markdoc & Mike Bifulco](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) - 11/2022
 - [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - 11/2022
 - [Envie Push Notifications por POST (de graça e sem cadastro)](https://www.tabnews.com.br/filipedeschamps/envie-push-notifications-por-post-de-graca-e-sem-cadastro) - 11/2022
 - [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - 11/2022

From ebfbf7cc8efc627eeb4bf6ffc94da98d7cedd6ff Mon Sep 17 00:00:00 2001
From: Quentin JOLY <82603435+QJoly@users.noreply.github.com>
Date: Tue, 15 Nov 2022 14:10:55 +0000
Subject: [PATCH 25/36] Bad indent

---
 docs/install.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/install.md b/docs/install.md
index 034c88d3..2ee0f584 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -431,7 +431,7 @@ Configuration is relatively straightforward. As an exmaple, a minimal configurat
     metadata:
       name: ntfy
     data:
-    server.yml: |
+      server.yml: |
         # Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml
         base-url: https://ntfy.sh
     ```

From b4933a5645f296c519f12a8f78bdeb7cfd3f17b9 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Tue, 15 Nov 2022 14:24:56 -0500
Subject: [PATCH 26/36] WIP: Batch message INSERTs

---
 server/message_cache.go     | 28 +++++++++++++++----
 server/server.go            |  6 ++--
 util/batching_queue.go      | 56 +++++++++++++++++++++++++++++++++++++
 util/batching_queue_test.go | 25 +++++++++++++++++
 4 files changed, 107 insertions(+), 8 deletions(-)
 create mode 100644 util/batching_queue.go
 create mode 100644 util/batching_queue_test.go

diff --git a/server/message_cache.go b/server/message_cache.go
index f4433399..ec710e4f 100644
--- a/server/message_cache.go
+++ b/server/message_cache.go
@@ -188,8 +188,9 @@ const (
 )
 
 type messageCache struct {
-	db  *sql.DB
-	nop bool
+	db    *sql.DB
+	queue *util.BatchingQueue[*message]
+	nop   bool
 }
 
 // newSqliteCache creates a SQLite file-backed cache
@@ -201,10 +202,21 @@ func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, e
 	if err := setupCacheDB(db, startupQueries); err != nil {
 		return nil, err
 	}
-	return &messageCache{
-		db:  db,
-		nop: nop,
-	}, nil
+	queue := util.NewBatchingQueue[*message](20, 500*time.Millisecond)
+	cache := &messageCache{
+		db:    db,
+		queue: queue,
+		nop:   nop,
+	}
+	go func() {
+		for messages := range queue.Pop() {
+			log.Debug("Adding %d messages to cache", len(messages))
+			if err := cache.addMessages(messages); err != nil {
+				log.Error("error: %s", err.Error())
+			}
+		}
+	}()
+	return cache, nil
 }
 
 // newMemCache creates an in-memory cache
@@ -232,6 +244,10 @@ func (c *messageCache) AddMessage(m *message) error {
 	return c.addMessages([]*message{m})
 }
 
+func (c *messageCache) QueueMessage(m *message) {
+	c.queue.Push(m)
+}
+
 func (c *messageCache) addMessages(ms []*message) error {
 	if c.nop {
 		return nil
diff --git a/server/server.go b/server/server.go
index ef09100d..b90b7630 100644
--- a/server/server.go
+++ b/server/server.go
@@ -491,9 +491,11 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
 		log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m))
 	}
 	if cache {
-		if err := s.messageCache.AddMessage(m); err != nil {
+		log.Trace("%s Queuing for cache", logMessagePrefix(v, m))
+		s.messageCache.QueueMessage(m)
+		/*if err := s.messageCache.AddMessage(m); err != nil {
 			return nil, err
-		}
+		}*/
 	}
 	s.mu.Lock()
 	s.messages++
diff --git a/util/batching_queue.go b/util/batching_queue.go
new file mode 100644
index 00000000..78116470
--- /dev/null
+++ b/util/batching_queue.go
@@ -0,0 +1,56 @@
+package util
+
+import (
+	"sync"
+	"time"
+)
+
+type BatchingQueue[T any] struct {
+	batchSize int
+	timeout   time.Duration
+	in        []T
+	out       chan []T
+	mu        sync.Mutex
+}
+
+func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *BatchingQueue[T] {
+	q := &BatchingQueue[T]{
+		batchSize: batchSize,
+		timeout:   timeout,
+		in:        make([]T, 0),
+		out:       make(chan []T),
+	}
+	ticker := time.NewTicker(timeout)
+	go func() {
+		for range ticker.C {
+			elements := q.popAll()
+			if len(elements) > 0 {
+				q.out <- elements
+			}
+		}
+	}()
+	return q
+}
+
+func (c *BatchingQueue[T]) Push(element T) {
+	c.mu.Lock()
+	c.in = append(c.in, element)
+	limitReached := len(c.in) == c.batchSize
+	c.mu.Unlock()
+	if limitReached {
+		c.out <- c.popAll()
+	}
+}
+
+func (c *BatchingQueue[T]) Pop() <-chan []T {
+	return c.out
+}
+
+func (c *BatchingQueue[T]) popAll() []T {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	elements := make([]T, len(c.in))
+	copy(elements, c.in)
+	c.in = c.in[:0]
+	return elements
+}
diff --git a/util/batching_queue_test.go b/util/batching_queue_test.go
new file mode 100644
index 00000000..46bc06b8
--- /dev/null
+++ b/util/batching_queue_test.go
@@ -0,0 +1,25 @@
+package util_test
+
+import (
+	"fmt"
+	"heckel.io/ntfy/util"
+	"math/rand"
+	"testing"
+	"time"
+)
+
+func TestConcurrentQueue_Next(t *testing.T) {
+	q := util.NewBatchingQueue[int](25, 200*time.Millisecond)
+	go func() {
+		for batch := range q.Pop() {
+			fmt.Printf("Batch of %d items\n", len(batch))
+		}
+	}()
+	for i := 0; i < 1000; i++ {
+		go func(i int) {
+			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
+			q.Push(i)
+		}(i)
+	}
+	time.Sleep(2 * time.Second)
+}

From ad860afb8b3715794d7a6d6d6bd3e25440895c12 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 10:28:20 -0500
Subject: [PATCH 27/36] Polish async batching

---
 cmd/serve.go                 |  6 +++
 server/config.go             |  4 ++
 server/message_cache.go      | 80 +++++++++++++++++++++++++++---------
 server/message_cache_test.go | 10 ++---
 server/server.go             |  9 ++--
 server/server.yml            |  2 +
 util/batching_queue.go       | 73 +++++++++++++++++++++-----------
 util/batching_queue_test.go  | 43 +++++++++++++++----
 8 files changed, 166 insertions(+), 61 deletions(-)

diff --git a/cmd/serve.go b/cmd/serve.go
index aff7c7c8..ecc4d4a1 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -44,6 +44,8 @@ var flagsServe = append(
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
+	altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}),
+	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
@@ -110,6 +112,8 @@ func execServe(c *cli.Context) error {
 	cacheFile := c.String("cache-file")
 	cacheDuration := c.Duration("cache-duration")
 	cacheStartupQueries := c.String("cache-startup-queries")
+	cacheBatchSize := c.Int("cache-batch-size")
+	cacheBatchTimeout := c.Duration("cache-batch-timeout")
 	authFile := c.String("auth-file")
 	authDefaultAccess := c.String("auth-default-access")
 	attachmentCacheDir := c.String("attachment-cache-dir")
@@ -233,6 +237,8 @@ func execServe(c *cli.Context) error {
 	conf.CacheFile = cacheFile
 	conf.CacheDuration = cacheDuration
 	conf.CacheStartupQueries = cacheStartupQueries
+	conf.CacheBatchSize = cacheBatchSize
+	conf.CacheBatchTimeout = cacheBatchTimeout
 	conf.AuthFile = authFile
 	conf.AuthDefaultRead = authDefaultRead
 	conf.AuthDefaultWrite = authDefaultWrite
diff --git a/server/config.go b/server/config.go
index d8fd429e..1e2b517c 100644
--- a/server/config.go
+++ b/server/config.go
@@ -61,6 +61,8 @@ type Config struct {
 	CacheFile                            string
 	CacheDuration                        time.Duration
 	CacheStartupQueries                  string
+	CacheBatchSize                       int
+	CacheBatchTimeout                    time.Duration
 	AuthFile                             string
 	AuthDefaultRead                      bool
 	AuthDefaultWrite                     bool
@@ -114,6 +116,8 @@ func NewConfig() *Config {
 		FirebaseKeyFile:                      "",
 		CacheFile:                            "",
 		CacheDuration:                        DefaultCacheDuration,
+		CacheBatchSize:                       0,
+		CacheBatchTimeout:                    0,
 		AuthFile:                             "",
 		AuthDefaultRead:                      true,
 		AuthDefaultWrite:                     true,
diff --git a/server/message_cache.go b/server/message_cache.go
index ec710e4f..7eb37cf9 100644
--- a/server/message_cache.go
+++ b/server/message_cache.go
@@ -44,6 +44,7 @@ const (
 			published INT NOT NULL
 		);
 		CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
+		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
 		COMMIT;
 	`
@@ -92,7 +93,7 @@ const (
 
 // Schema management queries
 const (
-	currentSchemaVersion          = 8
+	currentSchemaVersion          = 9
 	createSchemaVersionTableQuery = `
 		CREATE TABLE IF NOT EXISTS schemaVersion (
 			id INT PRIMARY KEY,
@@ -185,6 +186,11 @@ const (
 	migrate7To8AlterMessagesTableQuery = `
 		ALTER TABLE messages ADD COLUMN icon TEXT NOT NULL DEFAULT('');
 	`
+
+	// 8 -> 9
+	migrate8To9AlterMessagesTableQuery = `
+		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);	
+	`
 )
 
 type messageCache struct {
@@ -194,7 +200,7 @@ type messageCache struct {
 }
 
 // newSqliteCache creates a SQLite file-backed cache
-func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, error) {
+func newSqliteCache(filename, startupQueries string, batchSize int, batchTimeout time.Duration, nop bool) (*messageCache, error) {
 	db, err := sql.Open("sqlite3", filename)
 	if err != nil {
 		return nil, err
@@ -202,32 +208,28 @@ func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, e
 	if err := setupCacheDB(db, startupQueries); err != nil {
 		return nil, err
 	}
-	queue := util.NewBatchingQueue[*message](20, 500*time.Millisecond)
+	var queue *util.BatchingQueue[*message]
+	if batchSize > 0 || batchTimeout > 0 {
+		queue = util.NewBatchingQueue[*message](batchSize, batchTimeout)
+	}
 	cache := &messageCache{
 		db:    db,
 		queue: queue,
 		nop:   nop,
 	}
-	go func() {
-		for messages := range queue.Pop() {
-			log.Debug("Adding %d messages to cache", len(messages))
-			if err := cache.addMessages(messages); err != nil {
-				log.Error("error: %s", err.Error())
-			}
-		}
-	}()
+	go cache.processMessageBatches()
 	return cache, nil
 }
 
 // newMemCache creates an in-memory cache
 func newMemCache() (*messageCache, error) {
-	return newSqliteCache(createMemoryFilename(), "", false)
+	return newSqliteCache(createMemoryFilename(), "", 0, 0, false)
 }
 
 // newNopCache creates an in-memory cache that discards all messages;
 // it is always empty and can be used if caching is entirely disabled
 func newNopCache() (*messageCache, error) {
-	return newSqliteCache(createMemoryFilename(), "", true)
+	return newSqliteCache(createMemoryFilename(), "", 0, 0, true)
 }
 
 // createMemoryFilename creates a unique memory filename to use for the SQLite backend.
@@ -240,18 +242,23 @@ func createMemoryFilename() string {
 	return fmt.Sprintf("file:%s?mode=memory&cache=shared", util.RandomString(10))
 }
 
+// AddMessage stores a message to the message cache synchronously, or queues it to be stored at a later date asyncronously.
+// The message is queued only if "batchSize" or "batchTimeout" are passed to the constructor.
 func (c *messageCache) AddMessage(m *message) error {
+	if c.queue != nil {
+		c.queue.Enqueue(m)
+		return nil
+	}
 	return c.addMessages([]*message{m})
 }
 
-func (c *messageCache) QueueMessage(m *message) {
-	c.queue.Push(m)
-}
-
+// addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until
+// SQLite's busy_timeout is exceeded before erroring out.
 func (c *messageCache) addMessages(ms []*message) error {
 	if c.nop {
 		return nil
 	}
+	start := time.Now()
 	tx, err := c.db.Begin()
 	if err != nil {
 		return err
@@ -305,7 +312,12 @@ func (c *messageCache) addMessages(ms []*message) error {
 			return err
 		}
 	}
-	return tx.Commit()
+	if err := tx.Commit(); err != nil {
+		log.Warn("Cache: Writing %d message(s) failed (took %v)", len(ms), time.Since(start))
+		return err
+	}
+	log.Debug("Cache: Wrote %d message(s) in %v", len(ms), time.Since(start))
+	return nil
 }
 
 func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
@@ -411,8 +423,12 @@ func (c *messageCache) Topics() (map[string]*topic, error) {
 }
 
 func (c *messageCache) Prune(olderThan time.Time) error {
-	_, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix())
-	return err
+	start := time.Now()
+	if _, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix()); err != nil {
+		log.Warn("Cache: Pruning failed (after %v): %s", time.Since(start), err.Error())
+	}
+	log.Debug("Cache: Pruning successful (took %v)", time.Since(start))
+	return nil
 }
 
 func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
@@ -433,6 +449,17 @@ func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
 	return size, nil
 }
 
+func (c *messageCache) processMessageBatches() {
+	if c.queue == nil {
+		return
+	}
+	for messages := range c.queue.Dequeue() {
+		if err := c.addMessages(messages); err != nil {
+			log.Error("Cache: %s", err.Error())
+		}
+	}
+}
+
 func readMessages(rows *sql.Rows) ([]*message, error) {
 	defer rows.Close()
 	messages := make([]*message, 0)
@@ -558,6 +585,8 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
 		return migrateFrom6(db)
 	} else if schemaVersion == 7 {
 		return migrateFrom7(db)
+	} else if schemaVersion == 8 {
+		return migrateFrom8(db)
 	}
 	return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
 }
@@ -663,5 +692,16 @@ func migrateFrom7(db *sql.DB) error {
 	if _, err := db.Exec(updateSchemaVersion, 8); err != nil {
 		return err
 	}
+	return migrateFrom8(db)
+}
+
+func migrateFrom8(db *sql.DB) error {
+	log.Info("Migrating cache database schema: from 8 to 9")
+	if _, err := db.Exec(migrate8To9AlterMessagesTableQuery); err != nil {
+		return err
+	}
+	if _, err := db.Exec(updateSchemaVersion, 9); err != nil {
+		return err
+	}
 	return nil // Update this when a new version is added
 }
diff --git a/server/message_cache_test.go b/server/message_cache_test.go
index c72debca..c3b7305e 100644
--- a/server/message_cache_test.go
+++ b/server/message_cache_test.go
@@ -450,7 +450,7 @@ func TestSqliteCache_StartupQueries_WAL(t *testing.T) {
 	startupQueries := `pragma journal_mode = WAL; 
 pragma synchronous = normal; 
 pragma temp_store = memory;`
-	db, err := newSqliteCache(filename, startupQueries, false)
+	db, err := newSqliteCache(filename, startupQueries, 0, 0, false)
 	require.Nil(t, err)
 	require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
 	require.FileExists(t, filename)
@@ -461,7 +461,7 @@ pragma temp_store = memory;`
 func TestSqliteCache_StartupQueries_None(t *testing.T) {
 	filename := newSqliteTestCacheFile(t)
 	startupQueries := ""
-	db, err := newSqliteCache(filename, startupQueries, false)
+	db, err := newSqliteCache(filename, startupQueries, 0, 0, false)
 	require.Nil(t, err)
 	require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
 	require.FileExists(t, filename)
@@ -472,7 +472,7 @@ func TestSqliteCache_StartupQueries_None(t *testing.T) {
 func TestSqliteCache_StartupQueries_Fail(t *testing.T) {
 	filename := newSqliteTestCacheFile(t)
 	startupQueries := `xx error`
-	_, err := newSqliteCache(filename, startupQueries, false)
+	_, err := newSqliteCache(filename, startupQueries, 0, 0, false)
 	require.Error(t, err)
 }
 
@@ -501,7 +501,7 @@ func TestMemCache_NopCache(t *testing.T) {
 }
 
 func newSqliteTestCache(t *testing.T) *messageCache {
-	c, err := newSqliteCache(newSqliteTestCacheFile(t), "", false)
+	c, err := newSqliteCache(newSqliteTestCacheFile(t), "", 0, 0, false)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -513,7 +513,7 @@ func newSqliteTestCacheFile(t *testing.T) string {
 }
 
 func newSqliteTestCacheFromFile(t *testing.T, filename, startupQueries string) *messageCache {
-	c, err := newSqliteCache(filename, startupQueries, false)
+	c, err := newSqliteCache(filename, startupQueries, 0, 0, false)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/server/server.go b/server/server.go
index b90b7630..fe729b1b 100644
--- a/server/server.go
+++ b/server/server.go
@@ -159,7 +159,7 @@ func createMessageCache(conf *Config) (*messageCache, error) {
 	if conf.CacheDuration == 0 {
 		return newNopCache()
 	} else if conf.CacheFile != "" {
-		return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, false)
+		return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, conf.CacheBatchSize, conf.CacheBatchTimeout, false)
 	}
 	return newMemCache()
 }
@@ -491,11 +491,10 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
 		log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m))
 	}
 	if cache {
-		log.Trace("%s Queuing for cache", logMessagePrefix(v, m))
-		s.messageCache.QueueMessage(m)
-		/*if err := s.messageCache.AddMessage(m); err != nil {
+		log.Debug("%s Adding message to cache", logMessagePrefix(v, m))
+		if err := s.messageCache.AddMessage(m); err != nil {
 			return nil, err
-		}*/
+		}
 	}
 	s.mu.Lock()
 	s.messages++
diff --git a/server/server.yml b/server/server.yml
index 9476478f..4b08129b 100644
--- a/server/server.yml
+++ b/server/server.yml
@@ -65,6 +65,8 @@
 # cache-file: <filename>
 # cache-duration: "12h"
 # cache-startup-queries:
+# cache-batch-size: 0
+# cache-batch-timeout: "0ms"
 
 # If set, access to the ntfy server and API can be controlled on a granular level using
 # the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs.
diff --git a/util/batching_queue.go b/util/batching_queue.go
index 78116470..86901bcd 100644
--- a/util/batching_queue.go
+++ b/util/batching_queue.go
@@ -5,6 +5,24 @@ import (
 	"time"
 )
 
+// BatchingQueue is a queue that creates batches of the enqueued elements based on a
+// max batch size and a batch timeout.
+//
+// Example:
+//
+//	q := NewBatchingQueue[int](2, 500 * time.Millisecond)
+//	go func() {
+//	  for batch := range q.Dequeue() {
+//	    fmt.Println(batch)
+//	  }
+//	}()
+//	q.Enqueue(1)
+//	q.Enqueue(2)
+//	q.Enqueue(3)
+//	time.Sleep(time.Second)
+//
+// This example will emit batch [1, 2] immediately (because the batch size is 2), and
+// a batch [3] after 500ms.
 type BatchingQueue[T any] struct {
 	batchSize int
 	timeout   time.Duration
@@ -13,6 +31,7 @@ type BatchingQueue[T any] struct {
 	mu        sync.Mutex
 }
 
+// NewBatchingQueue creates a new BatchingQueue
 func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *BatchingQueue[T] {
 	q := &BatchingQueue[T]{
 		batchSize: batchSize,
@@ -20,37 +39,45 @@ func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *BatchingQueu
 		in:        make([]T, 0),
 		out:       make(chan []T),
 	}
-	ticker := time.NewTicker(timeout)
-	go func() {
-		for range ticker.C {
-			elements := q.popAll()
-			if len(elements) > 0 {
-				q.out <- elements
-			}
-		}
-	}()
+	go q.timeoutTicker()
 	return q
 }
 
-func (c *BatchingQueue[T]) Push(element T) {
-	c.mu.Lock()
-	c.in = append(c.in, element)
-	limitReached := len(c.in) == c.batchSize
-	c.mu.Unlock()
+// Enqueue enqueues an element to the queue. If the configured batch size is reached,
+// the batch will be emitted immediately.
+func (q *BatchingQueue[T]) Enqueue(element T) {
+	q.mu.Lock()
+	q.in = append(q.in, element)
+	limitReached := len(q.in) == q.batchSize
+	q.mu.Unlock()
 	if limitReached {
-		c.out <- c.popAll()
+		q.out <- q.dequeueAll()
 	}
 }
 
-func (c *BatchingQueue[T]) Pop() <-chan []T {
-	return c.out
+// Dequeue returns a channel emitting batches of elements
+func (q *BatchingQueue[T]) Dequeue() <-chan []T {
+	return q.out
 }
 
-func (c *BatchingQueue[T]) popAll() []T {
-	c.mu.Lock()
-	defer c.mu.Unlock()
-	elements := make([]T, len(c.in))
-	copy(elements, c.in)
-	c.in = c.in[:0]
+func (q *BatchingQueue[T]) dequeueAll() []T {
+	q.mu.Lock()
+	defer q.mu.Unlock()
+	elements := make([]T, len(q.in))
+	copy(elements, q.in)
+	q.in = q.in[:0]
 	return elements
 }
+
+func (q *BatchingQueue[T]) timeoutTicker() {
+	if q.timeout == 0 {
+		return
+	}
+	ticker := time.NewTicker(q.timeout)
+	for range ticker.C {
+		elements := q.dequeueAll()
+		if len(elements) > 0 {
+			q.out <- elements
+		}
+	}
+}
diff --git a/util/batching_queue_test.go b/util/batching_queue_test.go
index 46bc06b8..28764f18 100644
--- a/util/batching_queue_test.go
+++ b/util/batching_queue_test.go
@@ -2,24 +2,51 @@ package util_test
 
 import (
 	"fmt"
+	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/util"
 	"math/rand"
 	"testing"
 	"time"
 )
 
-func TestConcurrentQueue_Next(t *testing.T) {
-	q := util.NewBatchingQueue[int](25, 200*time.Millisecond)
+func TestBatchingQueue_InfTimeout(t *testing.T) {
+	q := util.NewBatchingQueue[int](25, 1*time.Hour)
+	batches := make([][]int, 0)
+	total := 0
 	go func() {
-		for batch := range q.Pop() {
-			fmt.Printf("Batch of %d items\n", len(batch))
+		for batch := range q.Dequeue() {
+			batches = append(batches, batch)
+			total += len(batch)
 		}
 	}()
-	for i := 0; i < 1000; i++ {
+	for i := 0; i < 101; i++ {
+		go q.Enqueue(i)
+	}
+	time.Sleep(500 * time.Millisecond)
+	require.Equal(t, 100, total) // One is missing, stuck in the last batch!
+	require.Equal(t, 4, len(batches))
+}
+
+func TestBatchingQueue_WithTimeout(t *testing.T) {
+	q := util.NewBatchingQueue[int](25, 100*time.Millisecond)
+	batches := make([][]int, 0)
+	total := 0
+	go func() {
+		for batch := range q.Dequeue() {
+			batches = append(batches, batch)
+			total += len(batch)
+		}
+	}()
+	for i := 0; i < 101; i++ {
 		go func(i int) {
-			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
-			q.Push(i)
+			time.Sleep(time.Duration(rand.Intn(700)) * time.Millisecond)
+			q.Enqueue(i)
 		}(i)
 	}
-	time.Sleep(2 * time.Second)
+	time.Sleep(time.Second)
+	fmt.Println(len(batches))
+	fmt.Println(batches)
+	require.Equal(t, 101, total)
+	require.True(t, len(batches) > 4) // 101/25
+	require.True(t, len(batches) < 21)
 }

From 497f871447a9b0f8f9e802831e8a4eb8c47401a7 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 10:33:12 -0500
Subject: [PATCH 28/36] Docs

---
 server/message_cache.go | 2 +-
 server/server.yml       | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/server/message_cache.go b/server/message_cache.go
index 7eb37cf9..bce94220 100644
--- a/server/message_cache.go
+++ b/server/message_cache.go
@@ -313,7 +313,7 @@ func (c *messageCache) addMessages(ms []*message) error {
 		}
 	}
 	if err := tx.Commit(); err != nil {
-		log.Warn("Cache: Writing %d message(s) failed (took %v)", len(ms), time.Since(start))
+		log.Error("Cache: Writing %d message(s) failed (took %v)", len(ms), time.Since(start))
 		return err
 	}
 	log.Debug("Cache: Wrote %d message(s) in %v", len(ms), time.Since(start))
diff --git a/server/server.yml b/server/server.yml
index 4b08129b..1b268995 100644
--- a/server/server.yml
+++ b/server/server.yml
@@ -53,6 +53,12 @@
 #       pragma journal_mode = WAL;
 #       pragma synchronous = normal;
 #       pragma temp_store = memory;
+#       pragma busy_timeout = 15000;
+#       vacuum;
+#
+# The "cache-batch-size" and "cache-batch-timeout" parameter allow enabling async batch writing
+# of messages. If set, messages will be queued and written to the database in batches of the given
+# size, or after the given timeout. This is only required for high volume servers.
 #
 # Debian/RPM package users:
 #   Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package

From e147a41f928241f8497ead40d6aada293cd6255d Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 10:44:10 -0500
Subject: [PATCH 29/36] Fix race in tests

---
 util/batching_queue_test.go | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/util/batching_queue_test.go b/util/batching_queue_test.go
index 28764f18..f7dccfab 100644
--- a/util/batching_queue_test.go
+++ b/util/batching_queue_test.go
@@ -1,40 +1,46 @@
 package util_test
 
 import (
-	"fmt"
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/util"
 	"math/rand"
+	"sync"
 	"testing"
 	"time"
 )
 
 func TestBatchingQueue_InfTimeout(t *testing.T) {
 	q := util.NewBatchingQueue[int](25, 1*time.Hour)
-	batches := make([][]int, 0)
-	total := 0
+	batches, total := make([][]int, 0), 0
+	var mu sync.Mutex
 	go func() {
 		for batch := range q.Dequeue() {
+			mu.Lock()
 			batches = append(batches, batch)
 			total += len(batch)
+			mu.Unlock()
 		}
 	}()
 	for i := 0; i < 101; i++ {
 		go q.Enqueue(i)
 	}
 	time.Sleep(500 * time.Millisecond)
+	mu.Lock()
 	require.Equal(t, 100, total) // One is missing, stuck in the last batch!
 	require.Equal(t, 4, len(batches))
+	mu.Unlock()
 }
 
 func TestBatchingQueue_WithTimeout(t *testing.T) {
 	q := util.NewBatchingQueue[int](25, 100*time.Millisecond)
-	batches := make([][]int, 0)
-	total := 0
+	batches, total := make([][]int, 0), 0
+	var mu sync.Mutex
 	go func() {
 		for batch := range q.Dequeue() {
+			mu.Lock()
 			batches = append(batches, batch)
 			total += len(batch)
+			mu.Unlock()
 		}
 	}()
 	for i := 0; i < 101; i++ {
@@ -44,9 +50,9 @@ func TestBatchingQueue_WithTimeout(t *testing.T) {
 		}(i)
 	}
 	time.Sleep(time.Second)
-	fmt.Println(len(batches))
-	fmt.Println(batches)
+	mu.Lock()
 	require.Equal(t, 101, total)
 	require.True(t, len(batches) > 4) // 101/25
 	require.True(t, len(batches) < 21)
+	mu.Unlock()
 }

From db9ca80b69978e33154dc0c22b5631fe62ac8348 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 11:16:07 -0500
Subject: [PATCH 30/36] Fix race condition making it possible for batches to be
 >batchSize

---
 util/batching_queue.go      | 13 ++++++++-----
 util/batching_queue_test.go |  2 +-
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/util/batching_queue.go b/util/batching_queue.go
index 86901bcd..85ba9be9 100644
--- a/util/batching_queue.go
+++ b/util/batching_queue.go
@@ -48,10 +48,13 @@ func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *BatchingQueu
 func (q *BatchingQueue[T]) Enqueue(element T) {
 	q.mu.Lock()
 	q.in = append(q.in, element)
-	limitReached := len(q.in) == q.batchSize
+	var elements []T
+	if len(q.in) == q.batchSize {
+		elements = q.dequeueAll()
+	}
 	q.mu.Unlock()
-	if limitReached {
-		q.out <- q.dequeueAll()
+	if len(elements) > 0 {
+		q.out <- elements
 	}
 }
 
@@ -61,8 +64,6 @@ func (q *BatchingQueue[T]) Dequeue() <-chan []T {
 }
 
 func (q *BatchingQueue[T]) dequeueAll() []T {
-	q.mu.Lock()
-	defer q.mu.Unlock()
 	elements := make([]T, len(q.in))
 	copy(elements, q.in)
 	q.in = q.in[:0]
@@ -75,7 +76,9 @@ func (q *BatchingQueue[T]) timeoutTicker() {
 	}
 	ticker := time.NewTicker(q.timeout)
 	for range ticker.C {
+		q.mu.Lock()
 		elements := q.dequeueAll()
+		q.mu.Unlock()
 		if len(elements) > 0 {
 			q.out <- elements
 		}
diff --git a/util/batching_queue_test.go b/util/batching_queue_test.go
index f7dccfab..b3c41a4c 100644
--- a/util/batching_queue_test.go
+++ b/util/batching_queue_test.go
@@ -24,7 +24,7 @@ func TestBatchingQueue_InfTimeout(t *testing.T) {
 	for i := 0; i < 101; i++ {
 		go q.Enqueue(i)
 	}
-	time.Sleep(500 * time.Millisecond)
+	time.Sleep(time.Second)
 	mu.Lock()
 	require.Equal(t, 100, total) // One is missing, stuck in the last batch!
 	require.Equal(t, 4, len(batches))

From 4a91da60dd3373c791575025901c659d0bf19203 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 11:27:46 -0500
Subject: [PATCH 31/36] Docs

---
 docs/config.md | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/docs/config.md b/docs/config.md
index 655b56cf..a127a5ac 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -825,19 +825,27 @@ out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/
 
 Depending on *how you run it*, here are a few limits that are relevant:
 
-### WAL for message cache
+### Message cache
 By default, the [message cache](#message-cache) (defined by `cache-file`) uses the SQLite default settings, which means it
 syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh,
 the [write-ahead log (WAL)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted. 
 See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details.
 
+In addition to that, for very high load servers (such as ntfy.sh), it may be beneficial to write messages to the cache
+in batches, and asynchronously. This can be enabled with the `cache-batch-size` and `cache-batch-timeout`. If you start
+seeing `database locked` messages in the logs, you should probably enable that.
+
 Here's how ntfy.sh has been tuned in the `server.yml` file:
 
 ``` yaml
+cache-batch-size: 25
+cache-batch-timeout: "1s"
 cache-startup-queries: |
     pragma journal_mode = WAL;
     pragma synchronous = normal;
     pragma temp_store = memory;
+    pragma busy_timeout = 15000;
+    vacuum;
 ```
 
 ### For systemd services
@@ -990,6 +998,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
 | `cache-file`                               | `NTFY_CACHE_FILE`                               | *filename*                                          | -                 | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache).             |
 | `cache-duration`                           | `NTFY_CACHE_DURATION`                           | *duration*                                          | 12h               | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely.                                        |
 | `cache-startup-queries`                    | `NTFY_CACHE_STARTUP_QUERIES`                    | *string (SQL queries)*                              | -                 | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache)                                                                                                           |
+| `cache-batch-size`                         | `NTFY_CACHE_BATCH_SIZE`                         | *int*                                               | 0                 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous)                                                                                                                          |
+| `cache-batch-timeout`                      | `NTFY_CACHE_BATCH_TIMEOUT`                      | *duration*                                          | 0s                | Timeout for batched async writes to the message cache (if zero, writes are synchronous)                                                                                                                                         |
 | `auth-file`                                | `NTFY_AUTH_FILE`                                | *filename*                                          | -                 | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control).                                                                                           |
 | `auth-default-access`                      | `NTFY_AUTH_DEFAULT_ACCESS`                      | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write`      | Default permissions if no matching entries in the auth database are found. Default is `read-write`.                                                                                                                             |
 | `behind-proxy`                             | `NTFY_BEHIND_PROXY`                             | *bool*                                              | false             | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection.                                                                                                 |
@@ -1054,6 +1064,8 @@ OPTIONS:
    --behind-proxy, --behind_proxy, -P                                                                  if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
    --cache-duration since, --cache_duration since, -b since                                            buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
    --cache-file value, --cache_file value, -C value                                                    cache file used for message caching [$NTFY_CACHE_FILE]
+   --cache-batch-size value, --cache_batch_size value                                                  max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE]
+   --cache-batch-timeout value, --cache_batch_timeout value                                            timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]   
    --cache-startup-queries value, --cache_startup_queries value                                        queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
    --cert-file value, --cert_file value, -E value                                                      certificate file, if listen-https is set [$NTFY_CERT_FILE]
    --config value, -c value                                                                            config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]

From 978118a4007456b1393c68da902ab254a0764f5e Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 11:31:29 -0500
Subject: [PATCH 32/36] Release notes

---
 docs/releases.md | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/docs/releases.md b/docs/releases.md
index f5fc9a49..e662faec 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -14,6 +14,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 ## ntfy server v1.30.0 (UNRELREASED)
 
+**Features:**
+
+* High-load servers: Allow asynchronous batch-writing of messages to cache via `cache-batch-*` options ([#498](https://github.com/binwiederhier/ntfy/issues/498)/[#502](https://github.com/binwiederhier/ntfy/pull/502))   
+
 **Documentation:**
 
 * GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl))
@@ -22,6 +26,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Other things:**
 
 * Put ntfy.sh docs on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491))
+* The ntfy.sh server hardware was upgraded to a bigger box. If you'd like to help out carrying the server cost, **[sponsorships and donations](https://github.com/sponsors/binwiederhier)** 💸 would be very much appreciated
 
 ## ntfy server v1.29.0
 Released November 12, 2022

From 755155479aafe2af911c2669b4b4436f630473e8 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 14:26:54 -0500
Subject: [PATCH 33/36] Thank you @skrollme for your sponsorship

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index d9bfae0f..a00d062c 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
 <a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
 <a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
 <a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
+<a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
 
 ## License
 Made with ❤️ by [Philipp C. Heckel](https://heckel.io).   

From 5b2fe66903581d847edc9a3ab8ff7ffb69830423 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 21:12:52 -0500
Subject: [PATCH 34/36] Fix test

---
 cmd/publish_test.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/cmd/publish_test.go b/cmd/publish_test.go
index dde279da..f818cdc3 100644
--- a/cmd/publish_test.go
+++ b/cmd/publish_test.go
@@ -17,6 +17,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
 
 	app, _, _, _ := newTestApp()
 	require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
+	time.Sleep(3 * time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
 
 	app2, _, stdout, _ := newTestApp()
 	require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))

From aee791a17d629f92ad5f750d1f67e146807fe62a Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Wed, 16 Nov 2022 21:21:41 -0500
Subject: [PATCH 35/36] Bump versions

---
 go.mod                |   6 +-
 go.sum                |   6 +
 web/package-lock.json | 651 +++++++++++++++++++++---------------------
 3 files changed, 336 insertions(+), 327 deletions(-)

diff --git a/go.mod b/go.mod
index 81a20a1f..e1f3261d 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
 	github.com/stretchr/testify v1.8.1
 	github.com/urfave/cli/v2 v2.23.5
-	golang.org/x/crypto v0.2.0
+	golang.org/x/crypto v0.3.0
 	golang.org/x/oauth2 v0.2.0 // indirect
 	golang.org/x/sync v0.1.0
 	golang.org/x/term v0.2.0
@@ -28,7 +28,7 @@ require github.com/pkg/errors v0.9.1 // indirect
 require firebase.google.com/go/v4 v4.10.0
 
 require (
-	cloud.google.com/go v0.106.0 // indirect
+	cloud.google.com/go v0.107.0 // indirect
 	cloud.google.com/go/compute v1.12.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
 	cloud.google.com/go/iam v0.7.0 // indirect
@@ -54,7 +54,7 @@ require (
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/appengine/v2 v2.0.2 // indirect
-	google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e // indirect
+	google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472 // indirect
 	google.golang.org/grpc v1.50.1 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 8c269ead..168bcd42 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.106.0 h1:AWaMWuZb2oFeiV91OfNHZbmwUhMVuXEaLPm9sqDAOl8=
 cloud.google.com/go v0.106.0/go.mod h1:5NEGxGuIeMQiPaWLwLYZ7kfNWiP6w1+QJK+xqyIT+dw=
+cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
+cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
 cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
@@ -111,6 +113,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
 golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -174,6 +178,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M=
 google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
+google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472 h1:kIfItBRE5gkUKpH4H5lNGciZbka1JrmRli3ArqrKFkA=
+google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/web/package-lock.json b/web/package-lock.json
index 3ff6b519..da6edf3d 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -3069,14 +3069,14 @@
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
     },
     "node_modules/@mui/base": {
-      "version": "5.0.0-alpha.105",
-      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.105.tgz",
-      "integrity": "sha512-4IPBcJQIgVVXQvN6DQMoCHed52GBtwSqYs0jD0dDcMR3o76AodQtpEeWFz3p7mJoc6f/IHBl9U6jEfL1r/kM4g==",
+      "version": "5.0.0-alpha.106",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.106.tgz",
+      "integrity": "sha512-xJQQtwPCPwr6hGWTBdvDwHYwExn3Bw7nPQkN8Fuz8kHpZqoMVWQvvaFS557AIkkI2AFLV3DxVIMjbCvrIntBWg==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0",
+        "@babel/runtime": "^7.20.1",
         "@emotion/is-prop-valid": "^1.2.0",
-        "@mui/types": "^7.2.0",
-        "@mui/utils": "^5.10.9",
+        "@mui/types": "^7.2.1",
+        "@mui/utils": "^5.10.14",
         "@popperjs/core": "^2.11.6",
         "clsx": "^1.2.1",
         "prop-types": "^15.8.1",
@@ -3101,20 +3101,20 @@
       }
     },
     "node_modules/@mui/core-downloads-tracker": {
-      "version": "5.10.13",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.13.tgz",
-      "integrity": "sha512-zWkWPV/SaNdsIdxAWiuVGZ+Ue3BkfSIlU/BFIrJmuUcwiIa7gQsbI/DOpj1KzLvqZhdEe2wC1aG4nCHfzgc1Hg==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.14.tgz",
+      "integrity": "sha512-qLgIJNOR9Dre8JiZ/neVzOf4jf88J6YtOkQqugtMrleLjbfRVUSS4LWl9CSOjNq76quYdmYWnSDgfQqOooT2cQ==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui"
       }
     },
     "node_modules/@mui/icons-material": {
-      "version": "5.10.9",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz",
-      "integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.14.tgz",
+      "integrity": "sha512-qtH60slQa+7MZRn6kyui8rKuoGDglPqaHX+pzBKNvd8JCOlrnfY5DmGGDdToTXyXl8xJ8nhANZbrbpg7UVKq/Q==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0"
+        "@babel/runtime": "^7.20.1"
       },
       "engines": {
         "node": ">=12.0.0"
@@ -3135,16 +3135,16 @@
       }
     },
     "node_modules/@mui/material": {
-      "version": "5.10.13",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.13.tgz",
-      "integrity": "sha512-TkkT1rNc0/hhL4/+zv4gYcA6egNWBH/1Tz+azoTnQIUdZ32fgwFI2pFX2KVJNTt30xnLznxDWtTv7ilmJQ52xw==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.14.tgz",
+      "integrity": "sha512-HWzKVAykePMx54WtxVwZyL1W4k3xlHYIqwMw0CaXAvgB3UE9yjABZuuGr8vG5Z6CSNWamzd+s1x8u7pQPFl9og==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0",
-        "@mui/base": "5.0.0-alpha.105",
-        "@mui/core-downloads-tracker": "^5.10.13",
-        "@mui/system": "^5.10.13",
-        "@mui/types": "^7.2.0",
-        "@mui/utils": "^5.10.9",
+        "@babel/runtime": "^7.20.1",
+        "@mui/base": "5.0.0-alpha.106",
+        "@mui/core-downloads-tracker": "^5.10.14",
+        "@mui/system": "^5.10.14",
+        "@mui/types": "^7.2.1",
+        "@mui/utils": "^5.10.14",
         "@types/react-transition-group": "^4.4.5",
         "clsx": "^1.2.1",
         "csstype": "^3.1.1",
@@ -3179,12 +3179,12 @@
       }
     },
     "node_modules/@mui/private-theming": {
-      "version": "5.10.9",
-      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.9.tgz",
-      "integrity": "sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.14.tgz",
+      "integrity": "sha512-3aIBe8WK65CwAPDY8nB11hYnzE1CZMymi76UnaFrA/DdGDwl5Y8F6uB+StKrkVmsqF1po7Mp2odqVkHj320gXw==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0",
-        "@mui/utils": "^5.10.9",
+        "@babel/runtime": "^7.20.1",
+        "@mui/utils": "^5.10.14",
         "prop-types": "^15.8.1"
       },
       "engines": {
@@ -3205,12 +3205,12 @@
       }
     },
     "node_modules/@mui/styled-engine": {
-      "version": "5.10.8",
-      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.8.tgz",
-      "integrity": "sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.14.tgz",
+      "integrity": "sha512-bgKdM57ExogWpIfhL/ngSlzF4FhbH00vYF+Y5VALTob4uslFqje0xzoWmbfcCn4cZt2NXxZJIwhsq4vzo5itlw==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0",
-        "@emotion/cache": "^11.10.3",
+        "@babel/runtime": "^7.20.1",
+        "@emotion/cache": "^11.10.5",
         "csstype": "^3.1.1",
         "prop-types": "^15.8.1"
       },
@@ -3236,15 +3236,15 @@
       }
     },
     "node_modules/@mui/system": {
-      "version": "5.10.13",
-      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.13.tgz",
-      "integrity": "sha512-Xzx26Asu5fVlm0ucm+gnJmeX4Y1isrpVDvqxX4yJaOT7Fzmd8Lfq9ih3QMfZajns5LMtUiOuCQlVFRtUG5IY7A==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.14.tgz",
+      "integrity": "sha512-2de7XCjRb1j8Od0Stmo0LwFMLpOMNT4wzfINuExXI1TVSuyxXIXUxiC5FEgJW3GMvf/a7SUR8VOiMoKlKWzukw==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0",
-        "@mui/private-theming": "^5.10.9",
-        "@mui/styled-engine": "^5.10.8",
-        "@mui/types": "^7.2.0",
-        "@mui/utils": "^5.10.9",
+        "@babel/runtime": "^7.20.1",
+        "@mui/private-theming": "^5.10.14",
+        "@mui/styled-engine": "^5.10.14",
+        "@mui/types": "^7.2.1",
+        "@mui/utils": "^5.10.14",
         "clsx": "^1.2.1",
         "csstype": "^3.1.1",
         "prop-types": "^15.8.1"
@@ -3275,9 +3275,9 @@
       }
     },
     "node_modules/@mui/types": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.0.tgz",
-      "integrity": "sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==",
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.1.tgz",
+      "integrity": "sha512-c5mSM7ivD8EsqK6HUi9hQPr5V7TJ/IRThUQ9nWNYPdhCGriTSQV4vL6DflT99LkM+wLiIS1rVjphpEWxERep7A==",
       "peerDependencies": {
         "@types/react": "*"
       },
@@ -3288,11 +3288,11 @@
       }
     },
     "node_modules/@mui/utils": {
-      "version": "5.10.9",
-      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.9.tgz",
-      "integrity": "sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.14.tgz",
+      "integrity": "sha512-12p59+wDZpA++XVJmKwqsZmrA1nmUQ5d0a1yQWtcDjxNyER1EDzozYN/db+FY2i5ceQh2TynPTEwGms2mXDwFg==",
       "dependencies": {
-        "@babel/runtime": "^7.19.0",
+        "@babel/runtime": "^7.20.1",
         "@types/prop-types": "^15.7.5",
         "@types/react-is": "^16.7.1 || ^17.0.0",
         "prop-types": "^15.8.1",
@@ -4090,13 +4090,13 @@
       "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA=="
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.1.tgz",
-      "integrity": "sha512-LyR6x784JCiJ1j6sH5Y0K6cdExqCCm8DJUTcwG5ThNXJj/G8o5E56u5EdG4SLy+bZAwZBswC+GYn3eGdttBVCg==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz",
+      "integrity": "sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.42.1",
-        "@typescript-eslint/type-utils": "5.42.1",
-        "@typescript-eslint/utils": "5.42.1",
+        "@typescript-eslint/scope-manager": "5.43.0",
+        "@typescript-eslint/type-utils": "5.43.0",
+        "@typescript-eslint/utils": "5.43.0",
         "debug": "^4.3.4",
         "ignore": "^5.2.0",
         "natural-compare-lite": "^1.4.0",
@@ -4136,11 +4136,11 @@
       }
     },
     "node_modules/@typescript-eslint/experimental-utils": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.42.1.tgz",
-      "integrity": "sha512-qona75z2MLpeZADEuCet5Pwvh1g/0cWScEEDy43chuUPc4klgDiwz5hLFk5dHcjFEETSYQHRPYiiHKW24EMPjw==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.43.0.tgz",
+      "integrity": "sha512-WkT637CumTJbm/hRbFfnHBMgfUYTKr08LitVsD7gQId7bi6rnkx3pu3jac67lmp5ObW4MpJ9SNFZAIOUB/Qbsw==",
       "dependencies": {
-        "@typescript-eslint/utils": "5.42.1"
+        "@typescript-eslint/utils": "5.43.0"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -4154,13 +4154,13 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.1.tgz",
-      "integrity": "sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz",
+      "integrity": "sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.42.1",
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/typescript-estree": "5.42.1",
+        "@typescript-eslint/scope-manager": "5.43.0",
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/typescript-estree": "5.43.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -4180,12 +4180,12 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.1.tgz",
-      "integrity": "sha512-QAZY/CBP1Emx4rzxurgqj3rUinfsh/6mvuKbLNMfJMMKYLRBfweus8brgXF8f64ABkIZ3zdj2/rYYtF8eiuksQ==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz",
+      "integrity": "sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw==",
       "dependencies": {
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/visitor-keys": "5.42.1"
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/visitor-keys": "5.43.0"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -4196,12 +4196,12 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.1.tgz",
-      "integrity": "sha512-WWiMChneex5w4xPIX56SSnQQo0tEOy5ZV2dqmj8Z371LJ0E+aymWD25JQ/l4FOuuX+Q49A7pzh/CGIQflxMVXg==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz",
+      "integrity": "sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "5.42.1",
-        "@typescript-eslint/utils": "5.42.1",
+        "@typescript-eslint/typescript-estree": "5.43.0",
+        "@typescript-eslint/utils": "5.43.0",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       },
@@ -4222,9 +4222,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.1.tgz",
-      "integrity": "sha512-Qrco9dsFF5lhalz+lLFtxs3ui1/YfC6NdXu+RAGBa8uSfn01cjO7ssCsjIsUs484vny9Xm699FSKwpkCcqwWwA==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz",
+      "integrity": "sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg==",
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
@@ -4234,12 +4234,12 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.1.tgz",
-      "integrity": "sha512-qElc0bDOuO0B8wDhhW4mYVgi/LZL+igPwXtV87n69/kYC/7NG3MES0jHxJNCr4EP7kY1XVsRy8C/u3DYeTKQmw==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz",
+      "integrity": "sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==",
       "dependencies": {
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/visitor-keys": "5.42.1",
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/visitor-keys": "5.43.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -4274,15 +4274,15 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.1.tgz",
-      "integrity": "sha512-Gxvf12xSp3iYZd/fLqiQRD4uKZjDNR01bQ+j8zvhPjpsZ4HmvEFL/tC4amGNyxN9Rq+iqvpHLhlqx6KTxz9ZyQ==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.43.0.tgz",
+      "integrity": "sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==",
       "dependencies": {
         "@types/json-schema": "^7.0.9",
         "@types/semver": "^7.3.12",
-        "@typescript-eslint/scope-manager": "5.42.1",
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/typescript-estree": "5.42.1",
+        "@typescript-eslint/scope-manager": "5.43.0",
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/typescript-estree": "5.43.0",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0",
         "semver": "^7.3.7"
@@ -4333,11 +4333,11 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.1.tgz",
-      "integrity": "sha512-LOQtSF4z+hejmpUvitPlc4hA7ERGoj2BVkesOcG91HCn8edLGUXbTrErmutmPbl8Bo9HjAvOO/zBKQHExXNA2A==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz",
+      "integrity": "sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg==",
       "dependencies": {
-        "@typescript-eslint/types": "5.42.1",
+        "@typescript-eslint/types": "5.43.0",
         "eslint-visitor-keys": "^3.3.0"
       },
       "engines": {
@@ -4645,9 +4645,9 @@
       }
     },
     "node_modules/ajv-formats/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -4897,9 +4897,9 @@
       }
     },
     "node_modules/axe-core": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.1.tgz",
-      "integrity": "sha512-1exVbW0X1O/HSr/WMwnaweyqcWOgZgLiVxdLG34pvSQk4NlYQr9OUy0JLwuhFfuVNQzzqgH57eYzkFBCb3bIsQ==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz",
+      "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==",
       "engines": {
         "node": ">=4"
       }
@@ -5543,9 +5543,12 @@
       }
     },
     "node_modules/ci-info": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
-      "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw=="
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz",
+      "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==",
+      "engines": {
+        "node": ">=8"
+      }
     },
     "node_modules/cjs-module-lexer": {
       "version": "1.2.2",
@@ -5778,9 +5781,9 @@
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
     },
     "node_modules/core-js": {
-      "version": "3.26.0",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
-      "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==",
+      "version": "3.26.1",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz",
+      "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==",
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
@@ -5788,9 +5791,9 @@
       }
     },
     "node_modules/core-js-compat": {
-      "version": "3.26.0",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz",
-      "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==",
+      "version": "3.26.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz",
+      "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==",
       "dependencies": {
         "browserslist": "^4.21.4"
       },
@@ -5800,9 +5803,9 @@
       }
     },
     "node_modules/core-js-pure": {
-      "version": "3.26.0",
-      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.0.tgz",
-      "integrity": "sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA==",
+      "version": "3.26.1",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz",
+      "integrity": "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==",
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
@@ -5904,18 +5907,18 @@
       }
     },
     "node_modules/css-loader": {
-      "version": "6.7.1",
-      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
-      "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz",
+      "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==",
       "dependencies": {
         "icss-utils": "^5.1.0",
-        "postcss": "^8.4.7",
+        "postcss": "^8.4.18",
         "postcss-modules-extract-imports": "^3.0.0",
         "postcss-modules-local-by-default": "^4.0.0",
         "postcss-modules-scope": "^3.0.0",
         "postcss-modules-values": "^4.0.0",
         "postcss-value-parser": "^4.2.0",
-        "semver": "^7.3.5"
+        "semver": "^7.3.8"
       },
       "engines": {
         "node": ">= 12.13.0"
@@ -5980,9 +5983,9 @@
       }
     },
     "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -7302,9 +7305,9 @@
       }
     },
     "node_modules/eslint-webpack-plugin/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -10222,9 +10225,9 @@
       }
     },
     "node_modules/jest-pnp-resolver": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
-      "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+      "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
       "engines": {
         "node": ">=6"
       },
@@ -11265,9 +11268,9 @@
       }
     },
     "node_modules/js-base64": {
-      "version": "3.7.2",
-      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz",
-      "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ=="
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.3.tgz",
+      "integrity": "sha512-PAr6Xg2jvd7MCR6Ld9Jg3BmTcjYsHEBx1VlwEwULb/qowPf5VD9kEMagj23Gm7JRnSvE/Da/57nChZjnvL8v6A=="
     },
     "node_modules/js-sdsl": {
       "version": "4.1.5",
@@ -11642,9 +11645,9 @@
       }
     },
     "node_modules/memfs": {
-      "version": "3.4.10",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.10.tgz",
-      "integrity": "sha512-0bCUP+L79P4am30yP1msPzApwuMQG23TjwlwdHeEV5MxioDR1a0AgB0T9FfggU52eJuDCq8WVwb5ekznFyWiTQ==",
+      "version": "3.4.11",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.11.tgz",
+      "integrity": "sha512-GvsCITGAyDCxxsJ+X6prJexFQEhOCJaIlUbsAvjzSI5o5O7j2dle3jWvz5Z5aOdpOxW6ol3vI1+0ut+641F1+w==",
       "dependencies": {
         "fs-monkey": "^1.0.3"
       },
@@ -11729,9 +11732,9 @@
       }
     },
     "node_modules/mini-css-extract-plugin": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz",
-      "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.0.tgz",
+      "integrity": "sha512-auqtVo8KhTScMsba7MbijqZTfibbXiBNlPAQbsVt7enQfcDYLdgG57eGxMqwVU3mfeWANY4F1wUg+rMF+ycZgw==",
       "dependencies": {
         "schema-utils": "^4.0.0"
       },
@@ -11747,9 +11750,9 @@
       }
     },
     "node_modules/mini-css-extract-plugin/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -13435,11 +13438,11 @@
       }
     },
     "node_modules/postcss-preset-env": {
-      "version": "7.8.2",
-      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.2.tgz",
-      "integrity": "sha512-rSMUEaOCnovKnwc5LvBDHUDzpGP+nrUeWZGWt9M72fBvckCi45JmnJigUr4QG4zZeOHmOCNCZnd2LKDvP++ZuQ==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz",
+      "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==",
       "dependencies": {
-        "@csstools/postcss-cascade-layers": "^1.1.0",
+        "@csstools/postcss-cascade-layers": "^1.1.1",
         "@csstools/postcss-color-function": "^1.1.1",
         "@csstools/postcss-font-format-keywords": "^1.0.1",
         "@csstools/postcss-hwb-function": "^1.0.2",
@@ -13453,19 +13456,19 @@
         "@csstools/postcss-text-decoration-shorthand": "^1.0.0",
         "@csstools/postcss-trigonometric-functions": "^1.0.2",
         "@csstools/postcss-unset-value": "^1.0.2",
-        "autoprefixer": "^10.4.11",
-        "browserslist": "^4.21.3",
+        "autoprefixer": "^10.4.13",
+        "browserslist": "^4.21.4",
         "css-blank-pseudo": "^3.0.3",
         "css-has-pseudo": "^3.0.4",
         "css-prefers-color-scheme": "^6.0.3",
-        "cssdb": "^7.0.1",
+        "cssdb": "^7.1.0",
         "postcss-attribute-case-insensitive": "^5.0.2",
         "postcss-clamp": "^4.1.0",
         "postcss-color-functional-notation": "^4.2.4",
         "postcss-color-hex-alpha": "^8.0.4",
         "postcss-color-rebeccapurple": "^7.1.1",
         "postcss-custom-media": "^8.0.2",
-        "postcss-custom-properties": "^12.1.9",
+        "postcss-custom-properties": "^12.1.10",
         "postcss-custom-selectors": "^6.0.3",
         "postcss-dir-pseudo-class": "^6.0.5",
         "postcss-double-position-gradients": "^3.1.2",
@@ -14348,16 +14351,16 @@
       }
     },
     "node_modules/regexpu-core": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz",
-      "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz",
+      "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==",
       "dependencies": {
         "regenerate": "^1.4.2",
         "regenerate-unicode-properties": "^10.1.0",
         "regjsgen": "^0.7.1",
         "regjsparser": "^0.9.1",
         "unicode-match-property-ecmascript": "^2.0.0",
-        "unicode-match-property-value-ecmascript": "^2.0.0"
+        "unicode-match-property-value-ecmascript": "^2.1.0"
       },
       "engines": {
         "node": ">=4"
@@ -15863,9 +15866,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "4.8.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
-      "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
+      "version": "4.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
+      "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
       "peer": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -15910,9 +15913,9 @@
       }
     },
     "node_modules/unicode-match-property-value-ecmascript": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz",
-      "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
+      "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
       "engines": {
         "node": ">=4"
       }
@@ -16210,9 +16213,9 @@
       }
     },
     "node_modules/webpack-dev-middleware/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -16313,9 +16316,9 @@
       }
     },
     "node_modules/webpack-dev-server/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -16631,9 +16634,9 @@
       }
     },
     "node_modules/workbox-build/node_modules/ajv": {
-      "version": "8.11.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+      "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
@@ -19056,14 +19059,14 @@
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
     },
     "@mui/base": {
-      "version": "5.0.0-alpha.105",
-      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.105.tgz",
-      "integrity": "sha512-4IPBcJQIgVVXQvN6DQMoCHed52GBtwSqYs0jD0dDcMR3o76AodQtpEeWFz3p7mJoc6f/IHBl9U6jEfL1r/kM4g==",
+      "version": "5.0.0-alpha.106",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.106.tgz",
+      "integrity": "sha512-xJQQtwPCPwr6hGWTBdvDwHYwExn3Bw7nPQkN8Fuz8kHpZqoMVWQvvaFS557AIkkI2AFLV3DxVIMjbCvrIntBWg==",
       "requires": {
-        "@babel/runtime": "^7.19.0",
+        "@babel/runtime": "^7.20.1",
         "@emotion/is-prop-valid": "^1.2.0",
-        "@mui/types": "^7.2.0",
-        "@mui/utils": "^5.10.9",
+        "@mui/types": "^7.2.1",
+        "@mui/utils": "^5.10.14",
         "@popperjs/core": "^2.11.6",
         "clsx": "^1.2.1",
         "prop-types": "^15.8.1",
@@ -19071,29 +19074,29 @@
       }
     },
     "@mui/core-downloads-tracker": {
-      "version": "5.10.13",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.13.tgz",
-      "integrity": "sha512-zWkWPV/SaNdsIdxAWiuVGZ+Ue3BkfSIlU/BFIrJmuUcwiIa7gQsbI/DOpj1KzLvqZhdEe2wC1aG4nCHfzgc1Hg=="
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.14.tgz",
+      "integrity": "sha512-qLgIJNOR9Dre8JiZ/neVzOf4jf88J6YtOkQqugtMrleLjbfRVUSS4LWl9CSOjNq76quYdmYWnSDgfQqOooT2cQ=="
     },
     "@mui/icons-material": {
-      "version": "5.10.9",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.9.tgz",
-      "integrity": "sha512-sqClXdEM39WKQJOQ0ZCPTptaZgqwibhj2EFV9N0v7BU1PO8y4OcX/a2wIQHn4fNuDjIZktJIBrmU23h7aqlGgg==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.14.tgz",
+      "integrity": "sha512-qtH60slQa+7MZRn6kyui8rKuoGDglPqaHX+pzBKNvd8JCOlrnfY5DmGGDdToTXyXl8xJ8nhANZbrbpg7UVKq/Q==",
       "requires": {
-        "@babel/runtime": "^7.19.0"
+        "@babel/runtime": "^7.20.1"
       }
     },
     "@mui/material": {
-      "version": "5.10.13",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.13.tgz",
-      "integrity": "sha512-TkkT1rNc0/hhL4/+zv4gYcA6egNWBH/1Tz+azoTnQIUdZ32fgwFI2pFX2KVJNTt30xnLznxDWtTv7ilmJQ52xw==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.10.14.tgz",
+      "integrity": "sha512-HWzKVAykePMx54WtxVwZyL1W4k3xlHYIqwMw0CaXAvgB3UE9yjABZuuGr8vG5Z6CSNWamzd+s1x8u7pQPFl9og==",
       "requires": {
-        "@babel/runtime": "^7.19.0",
-        "@mui/base": "5.0.0-alpha.105",
-        "@mui/core-downloads-tracker": "^5.10.13",
-        "@mui/system": "^5.10.13",
-        "@mui/types": "^7.2.0",
-        "@mui/utils": "^5.10.9",
+        "@babel/runtime": "^7.20.1",
+        "@mui/base": "5.0.0-alpha.106",
+        "@mui/core-downloads-tracker": "^5.10.14",
+        "@mui/system": "^5.10.14",
+        "@mui/types": "^7.2.1",
+        "@mui/utils": "^5.10.14",
         "@types/react-transition-group": "^4.4.5",
         "clsx": "^1.2.1",
         "csstype": "^3.1.1",
@@ -19103,53 +19106,53 @@
       }
     },
     "@mui/private-theming": {
-      "version": "5.10.9",
-      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.9.tgz",
-      "integrity": "sha512-BN7/CnsVPVyBaQpDTij4uV2xGYHHHhOgpdxeYLlIu+TqnsVM7wUeF+37kXvHovxM6xmL5qoaVUD98gDC0IZnHg==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.14.tgz",
+      "integrity": "sha512-3aIBe8WK65CwAPDY8nB11hYnzE1CZMymi76UnaFrA/DdGDwl5Y8F6uB+StKrkVmsqF1po7Mp2odqVkHj320gXw==",
       "requires": {
-        "@babel/runtime": "^7.19.0",
-        "@mui/utils": "^5.10.9",
+        "@babel/runtime": "^7.20.1",
+        "@mui/utils": "^5.10.14",
         "prop-types": "^15.8.1"
       }
     },
     "@mui/styled-engine": {
-      "version": "5.10.8",
-      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.8.tgz",
-      "integrity": "sha512-w+y8WI18EJV6zM/q41ug19cE70JTeO6sWFsQ7tgePQFpy6ToCVPh0YLrtqxUZXSoMStW5FMw0t9fHTFAqPbngw==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.14.tgz",
+      "integrity": "sha512-bgKdM57ExogWpIfhL/ngSlzF4FhbH00vYF+Y5VALTob4uslFqje0xzoWmbfcCn4cZt2NXxZJIwhsq4vzo5itlw==",
       "requires": {
-        "@babel/runtime": "^7.19.0",
-        "@emotion/cache": "^11.10.3",
+        "@babel/runtime": "^7.20.1",
+        "@emotion/cache": "^11.10.5",
         "csstype": "^3.1.1",
         "prop-types": "^15.8.1"
       }
     },
     "@mui/system": {
-      "version": "5.10.13",
-      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.13.tgz",
-      "integrity": "sha512-Xzx26Asu5fVlm0ucm+gnJmeX4Y1isrpVDvqxX4yJaOT7Fzmd8Lfq9ih3QMfZajns5LMtUiOuCQlVFRtUG5IY7A==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.10.14.tgz",
+      "integrity": "sha512-2de7XCjRb1j8Od0Stmo0LwFMLpOMNT4wzfINuExXI1TVSuyxXIXUxiC5FEgJW3GMvf/a7SUR8VOiMoKlKWzukw==",
       "requires": {
-        "@babel/runtime": "^7.19.0",
-        "@mui/private-theming": "^5.10.9",
-        "@mui/styled-engine": "^5.10.8",
-        "@mui/types": "^7.2.0",
-        "@mui/utils": "^5.10.9",
+        "@babel/runtime": "^7.20.1",
+        "@mui/private-theming": "^5.10.14",
+        "@mui/styled-engine": "^5.10.14",
+        "@mui/types": "^7.2.1",
+        "@mui/utils": "^5.10.14",
         "clsx": "^1.2.1",
         "csstype": "^3.1.1",
         "prop-types": "^15.8.1"
       }
     },
     "@mui/types": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.0.tgz",
-      "integrity": "sha512-lGXtFKe5lp3UxTBGqKI1l7G8sE2xBik8qCfrLHD5olwP/YU0/ReWoWT7Lp1//ri32dK39oPMrJN8TgbkCSbsNA==",
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.1.tgz",
+      "integrity": "sha512-c5mSM7ivD8EsqK6HUi9hQPr5V7TJ/IRThUQ9nWNYPdhCGriTSQV4vL6DflT99LkM+wLiIS1rVjphpEWxERep7A==",
       "requires": {}
     },
     "@mui/utils": {
-      "version": "5.10.9",
-      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.9.tgz",
-      "integrity": "sha512-2tdHWrq3+WCy+G6TIIaFx3cg7PorXZ71P375ExuX61od1NOAJP1mK90VxQ8N4aqnj2vmO3AQDkV4oV2Ktvt4bA==",
+      "version": "5.10.14",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.14.tgz",
+      "integrity": "sha512-12p59+wDZpA++XVJmKwqsZmrA1nmUQ5d0a1yQWtcDjxNyER1EDzozYN/db+FY2i5ceQh2TynPTEwGms2mXDwFg==",
       "requires": {
-        "@babel/runtime": "^7.19.0",
+        "@babel/runtime": "^7.20.1",
         "@types/prop-types": "^15.7.5",
         "@types/react-is": "^16.7.1 || ^17.0.0",
         "prop-types": "^15.8.1",
@@ -19753,13 +19756,13 @@
       "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA=="
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.1.tgz",
-      "integrity": "sha512-LyR6x784JCiJ1j6sH5Y0K6cdExqCCm8DJUTcwG5ThNXJj/G8o5E56u5EdG4SLy+bZAwZBswC+GYn3eGdttBVCg==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz",
+      "integrity": "sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==",
       "requires": {
-        "@typescript-eslint/scope-manager": "5.42.1",
-        "@typescript-eslint/type-utils": "5.42.1",
-        "@typescript-eslint/utils": "5.42.1",
+        "@typescript-eslint/scope-manager": "5.43.0",
+        "@typescript-eslint/type-utils": "5.43.0",
+        "@typescript-eslint/utils": "5.43.0",
         "debug": "^4.3.4",
         "ignore": "^5.2.0",
         "natural-compare-lite": "^1.4.0",
@@ -19779,56 +19782,56 @@
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.42.1.tgz",
-      "integrity": "sha512-qona75z2MLpeZADEuCet5Pwvh1g/0cWScEEDy43chuUPc4klgDiwz5hLFk5dHcjFEETSYQHRPYiiHKW24EMPjw==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.43.0.tgz",
+      "integrity": "sha512-WkT637CumTJbm/hRbFfnHBMgfUYTKr08LitVsD7gQId7bi6rnkx3pu3jac67lmp5ObW4MpJ9SNFZAIOUB/Qbsw==",
       "requires": {
-        "@typescript-eslint/utils": "5.42.1"
+        "@typescript-eslint/utils": "5.43.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.1.tgz",
-      "integrity": "sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz",
+      "integrity": "sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==",
       "requires": {
-        "@typescript-eslint/scope-manager": "5.42.1",
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/typescript-estree": "5.42.1",
+        "@typescript-eslint/scope-manager": "5.43.0",
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/typescript-estree": "5.43.0",
         "debug": "^4.3.4"
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.1.tgz",
-      "integrity": "sha512-QAZY/CBP1Emx4rzxurgqj3rUinfsh/6mvuKbLNMfJMMKYLRBfweus8brgXF8f64ABkIZ3zdj2/rYYtF8eiuksQ==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz",
+      "integrity": "sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw==",
       "requires": {
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/visitor-keys": "5.42.1"
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/visitor-keys": "5.43.0"
       }
     },
     "@typescript-eslint/type-utils": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.1.tgz",
-      "integrity": "sha512-WWiMChneex5w4xPIX56SSnQQo0tEOy5ZV2dqmj8Z371LJ0E+aymWD25JQ/l4FOuuX+Q49A7pzh/CGIQflxMVXg==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz",
+      "integrity": "sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==",
       "requires": {
-        "@typescript-eslint/typescript-estree": "5.42.1",
-        "@typescript-eslint/utils": "5.42.1",
+        "@typescript-eslint/typescript-estree": "5.43.0",
+        "@typescript-eslint/utils": "5.43.0",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       }
     },
     "@typescript-eslint/types": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.1.tgz",
-      "integrity": "sha512-Qrco9dsFF5lhalz+lLFtxs3ui1/YfC6NdXu+RAGBa8uSfn01cjO7ssCsjIsUs484vny9Xm699FSKwpkCcqwWwA=="
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz",
+      "integrity": "sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg=="
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.1.tgz",
-      "integrity": "sha512-qElc0bDOuO0B8wDhhW4mYVgi/LZL+igPwXtV87n69/kYC/7NG3MES0jHxJNCr4EP7kY1XVsRy8C/u3DYeTKQmw==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz",
+      "integrity": "sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==",
       "requires": {
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/visitor-keys": "5.42.1",
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/visitor-keys": "5.43.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -19847,15 +19850,15 @@
       }
     },
     "@typescript-eslint/utils": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.1.tgz",
-      "integrity": "sha512-Gxvf12xSp3iYZd/fLqiQRD4uKZjDNR01bQ+j8zvhPjpsZ4HmvEFL/tC4amGNyxN9Rq+iqvpHLhlqx6KTxz9ZyQ==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.43.0.tgz",
+      "integrity": "sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==",
       "requires": {
         "@types/json-schema": "^7.0.9",
         "@types/semver": "^7.3.12",
-        "@typescript-eslint/scope-manager": "5.42.1",
-        "@typescript-eslint/types": "5.42.1",
-        "@typescript-eslint/typescript-estree": "5.42.1",
+        "@typescript-eslint/scope-manager": "5.43.0",
+        "@typescript-eslint/types": "5.43.0",
+        "@typescript-eslint/typescript-estree": "5.43.0",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0",
         "semver": "^7.3.7"
@@ -19886,11 +19889,11 @@
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "5.42.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.1.tgz",
-      "integrity": "sha512-LOQtSF4z+hejmpUvitPlc4hA7ERGoj2BVkesOcG91HCn8edLGUXbTrErmutmPbl8Bo9HjAvOO/zBKQHExXNA2A==",
+      "version": "5.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz",
+      "integrity": "sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg==",
       "requires": {
-        "@typescript-eslint/types": "5.42.1",
+        "@typescript-eslint/types": "5.43.0",
         "eslint-visitor-keys": "^3.3.0"
       }
     },
@@ -20146,9 +20149,9 @@
       },
       "dependencies": {
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
@@ -20321,9 +20324,9 @@
       }
     },
     "axe-core": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.1.tgz",
-      "integrity": "sha512-1exVbW0X1O/HSr/WMwnaweyqcWOgZgLiVxdLG34pvSQk4NlYQr9OUy0JLwuhFfuVNQzzqgH57eYzkFBCb3bIsQ=="
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz",
+      "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA=="
     },
     "axobject-query": {
       "version": "2.2.0",
@@ -20802,9 +20805,9 @@
       "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
     },
     "ci-info": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz",
-      "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw=="
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz",
+      "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w=="
     },
     "cjs-module-lexer": {
       "version": "1.2.2",
@@ -20998,22 +21001,22 @@
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
     },
     "core-js": {
-      "version": "3.26.0",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
-      "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw=="
+      "version": "3.26.1",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz",
+      "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA=="
     },
     "core-js-compat": {
-      "version": "3.26.0",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz",
-      "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==",
+      "version": "3.26.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz",
+      "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==",
       "requires": {
         "browserslist": "^4.21.4"
       }
     },
     "core-js-pure": {
-      "version": "3.26.0",
-      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.0.tgz",
-      "integrity": "sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA=="
+      "version": "3.26.1",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz",
+      "integrity": "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ=="
     },
     "core-util-is": {
       "version": "1.0.3",
@@ -21078,18 +21081,18 @@
       }
     },
     "css-loader": {
-      "version": "6.7.1",
-      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
-      "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
+      "version": "6.7.2",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz",
+      "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==",
       "requires": {
         "icss-utils": "^5.1.0",
-        "postcss": "^8.4.7",
+        "postcss": "^8.4.18",
         "postcss-modules-extract-imports": "^3.0.0",
         "postcss-modules-local-by-default": "^4.0.0",
         "postcss-modules-scope": "^3.0.0",
         "postcss-modules-values": "^4.0.0",
         "postcss-value-parser": "^4.2.0",
-        "semver": "^7.3.5"
+        "semver": "^7.3.8"
       },
       "dependencies": {
         "semver": {
@@ -21116,9 +21119,9 @@
       },
       "dependencies": {
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
@@ -22159,9 +22162,9 @@
       },
       "dependencies": {
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
@@ -24172,9 +24175,9 @@
       }
     },
     "jest-pnp-resolver": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
-      "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+      "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
       "requires": {}
     },
     "jest-regex-util": {
@@ -24938,9 +24941,9 @@
       }
     },
     "js-base64": {
-      "version": "3.7.2",
-      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz",
-      "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ=="
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.3.tgz",
+      "integrity": "sha512-PAr6Xg2jvd7MCR6Ld9Jg3BmTcjYsHEBx1VlwEwULb/qowPf5VD9kEMagj23Gm7JRnSvE/Da/57nChZjnvL8v6A=="
     },
     "js-sdsl": {
       "version": "4.1.5",
@@ -25232,9 +25235,9 @@
       "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
     },
     "memfs": {
-      "version": "3.4.10",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.10.tgz",
-      "integrity": "sha512-0bCUP+L79P4am30yP1msPzApwuMQG23TjwlwdHeEV5MxioDR1a0AgB0T9FfggU52eJuDCq8WVwb5ekznFyWiTQ==",
+      "version": "3.4.11",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.11.tgz",
+      "integrity": "sha512-GvsCITGAyDCxxsJ+X6prJexFQEhOCJaIlUbsAvjzSI5o5O7j2dle3jWvz5Z5aOdpOxW6ol3vI1+0ut+641F1+w==",
       "requires": {
         "fs-monkey": "^1.0.3"
       }
@@ -25292,17 +25295,17 @@
       "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
     },
     "mini-css-extract-plugin": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz",
-      "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.0.tgz",
+      "integrity": "sha512-auqtVo8KhTScMsba7MbijqZTfibbXiBNlPAQbsVt7enQfcDYLdgG57eGxMqwVU3mfeWANY4F1wUg+rMF+ycZgw==",
       "requires": {
         "schema-utils": "^4.0.0"
       },
       "dependencies": {
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
@@ -26328,11 +26331,11 @@
       }
     },
     "postcss-preset-env": {
-      "version": "7.8.2",
-      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.2.tgz",
-      "integrity": "sha512-rSMUEaOCnovKnwc5LvBDHUDzpGP+nrUeWZGWt9M72fBvckCi45JmnJigUr4QG4zZeOHmOCNCZnd2LKDvP++ZuQ==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz",
+      "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==",
       "requires": {
-        "@csstools/postcss-cascade-layers": "^1.1.0",
+        "@csstools/postcss-cascade-layers": "^1.1.1",
         "@csstools/postcss-color-function": "^1.1.1",
         "@csstools/postcss-font-format-keywords": "^1.0.1",
         "@csstools/postcss-hwb-function": "^1.0.2",
@@ -26346,19 +26349,19 @@
         "@csstools/postcss-text-decoration-shorthand": "^1.0.0",
         "@csstools/postcss-trigonometric-functions": "^1.0.2",
         "@csstools/postcss-unset-value": "^1.0.2",
-        "autoprefixer": "^10.4.11",
-        "browserslist": "^4.21.3",
+        "autoprefixer": "^10.4.13",
+        "browserslist": "^4.21.4",
         "css-blank-pseudo": "^3.0.3",
         "css-has-pseudo": "^3.0.4",
         "css-prefers-color-scheme": "^6.0.3",
-        "cssdb": "^7.0.1",
+        "cssdb": "^7.1.0",
         "postcss-attribute-case-insensitive": "^5.0.2",
         "postcss-clamp": "^4.1.0",
         "postcss-color-functional-notation": "^4.2.4",
         "postcss-color-hex-alpha": "^8.0.4",
         "postcss-color-rebeccapurple": "^7.1.1",
         "postcss-custom-media": "^8.0.2",
-        "postcss-custom-properties": "^12.1.9",
+        "postcss-custom-properties": "^12.1.10",
         "postcss-custom-selectors": "^6.0.3",
         "postcss-dir-pseudo-class": "^6.0.5",
         "postcss-double-position-gradients": "^3.1.2",
@@ -26991,16 +26994,16 @@
       "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="
     },
     "regexpu-core": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz",
-      "integrity": "sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz",
+      "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==",
       "requires": {
         "regenerate": "^1.4.2",
         "regenerate-unicode-properties": "^10.1.0",
         "regjsgen": "^0.7.1",
         "regjsparser": "^0.9.1",
         "unicode-match-property-ecmascript": "^2.0.0",
-        "unicode-match-property-value-ecmascript": "^2.0.0"
+        "unicode-match-property-value-ecmascript": "^2.1.0"
       }
     },
     "regjsgen": {
@@ -28120,9 +28123,9 @@
       }
     },
     "typescript": {
-      "version": "4.8.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
-      "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
+      "version": "4.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
+      "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
       "peer": true
     },
     "unbox-primitive": {
@@ -28151,9 +28154,9 @@
       }
     },
     "unicode-match-property-value-ecmascript": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz",
-      "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw=="
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
+      "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA=="
     },
     "unicode-property-aliases-ecmascript": {
       "version": "2.1.0",
@@ -28383,9 +28386,9 @@
       },
       "dependencies": {
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
@@ -28456,9 +28459,9 @@
       },
       "dependencies": {
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
@@ -28684,9 +28687,9 @@
           }
         },
         "ajv": {
-          "version": "8.11.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
-          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "version": "8.11.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
+          "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",

From fcbf71dad7149abb3d7d45c9e54fafbcfb9cf808 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Thu, 17 Nov 2022 06:40:59 -0500
Subject: [PATCH 36/36] Thank you @gergepalfi for your sponsorship!

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index a00d062c..c42f349a 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,7 @@ appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
 <a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
 <a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
 <a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
+<a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a>
 
 ## License
 Made with ❤️ by [Philipp C. Heckel](https://heckel.io).