Explorar o código

Profiling support

binwiederhier %!s(int64=2) %!d(string=hai) anos
pai
achega
29c9551548
Modificáronse 11 ficheiros con 350 adicións e 194 borrados
  1. 7 7
      Makefile
  2. 3 0
      cmd/serve.go
  3. 5 0
      docs/config.md
  4. 5 1
      docs/releases.md
  5. 4 4
      go.mod
  6. 8 16
      go.sum
  7. 1 0
      server/config.go
  8. 19 0
      server/server.go
  9. 8 0
      server/server.yml
  10. 69 0
      tools/loadgen/main.go
  11. 221 166
      web/package-lock.json

+ 7 - 7
Makefile

@@ -141,25 +141,25 @@ web-deps-update:
 # Main server/client build
 
 cli: cli-deps
-	goreleaser build --snapshot --rm-dist
+	goreleaser build --snapshot --clean
 
 cli-linux-amd64: cli-deps-static-sites
-	goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64
+	goreleaser build --snapshot --clean --id ntfy_linux_amd64
 
 cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
-	goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6
+	goreleaser build --snapshot --clean --id ntfy_linux_armv6
 
 cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
-	goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7
+	goreleaser build --snapshot --clean --id ntfy_linux_armv7
 
 cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
-	goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64
+	goreleaser build --snapshot --clean --id ntfy_linux_arm64
 
 cli-windows-amd64: cli-deps-static-sites
-	goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64
+	goreleaser build --snapshot --clean --id ntfy_windows_amd64
 
 cli-darwin-all: cli-deps-static-sites
-	goreleaser build --snapshot --rm-dist --id ntfy_darwin_all
+	goreleaser build --snapshot --clean --id ntfy_darwin_all
 
 cli-linux-server: cli-deps-static-sites
 	# This is a target to build the CLI (including the server) manually.

+ 3 - 0
cmd/serve.go

@@ -88,6 +88,7 @@ var flagsServe = append(
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
+	altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
 )
 
 var cmdServe = &cli.Command{
@@ -167,6 +168,7 @@ func execServe(c *cli.Context) error {
 	billingContact := c.String("billing-contact")
 	metricsListenHTTP := c.String("metrics-listen-http")
 	enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
+	profileListenHTTP := c.String("profile-listen-http")
 
 	// Check values
 	if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
@@ -321,6 +323,7 @@ func execServe(c *cli.Context) error {
 	conf.EnableReservations = enableReservations
 	conf.EnableMetrics = enableMetrics
 	conf.MetricsListenHTTP = metricsListenHTTP
+	conf.ProfileListenHTTP = profileListenHTTP
 	conf.Version = c.App.Version
 
 	// Set up hot-reloading of config

+ 5 - 0
docs/config.md

@@ -1138,6 +1138,11 @@ Here's an example Grafana dashboard built from the metrics (see [Grafana JSON on
   <figcaption>ntfy Grafana dashboard</figcaption>
 </figure>
 
+## Profiling
+ntfy can expose Go's [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoints to support profiling of the ntfy server. 
+If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on `http://<ip>:<port>/debug/pprof/`.
+This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the `profile-listen-http` config option.
+
 ## Logging & debugging
 By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.
 

+ 5 - 1
docs/releases.md

@@ -1125,7 +1125,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 **Features:**
 
-* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
+* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) 
 
 **Bug fixes + maintenance:**
 
@@ -1138,6 +1138,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 ## ntfy server v2.3.0 (UNRELEASED)
 
+**Features:**
+
+* ntfy now supports Go's `net/http/pprof` profiler, if enabled (relates to [#677](https://github.com/binwiederhier/ntfy/issues/677))
+
 **Bug fixes + maintenance:**
 
 * Fix delayed message sending from authenticated users ([#679](https://github.com/binwiederhier/ntfy/issues/679))

+ 4 - 4
go.mod

@@ -13,7 +13,7 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.16
 	github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
 	github.com/stretchr/testify v1.8.1
-	github.com/urfave/cli/v2 v2.25.0
+	github.com/urfave/cli/v2 v2.25.1
 	golang.org/x/crypto v0.7.0
 	golang.org/x/oauth2 v0.6.0 // indirect
 	golang.org/x/sync v0.1.0
@@ -28,12 +28,12 @@ require github.com/pkg/errors v0.9.1 // indirect
 require (
 	firebase.google.com/go/v4 v4.10.0
 	github.com/prometheus/client_golang v1.14.0
-	github.com/stripe/stripe-go/v74 v74.12.0
+	github.com/stripe/stripe-go/v74 v74.13.0
 )
 
 require (
 	cloud.google.com/go v0.110.0 // indirect
-	cloud.google.com/go/compute v1.18.0 // indirect
+	cloud.google.com/go/compute v1.19.0 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	cloud.google.com/go/iam v0.13.0 // indirect
 	cloud.google.com/go/longrunning v0.4.1 // indirect
@@ -66,7 +66,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-20230322174352-cde4c949918d // indirect
+	google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5 // indirect
 	google.golang.org/grpc v1.54.0 // indirect
 	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 8 - 16
go.sum

@@ -1,8 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
 cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
-cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
-cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
+cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
@@ -11,8 +11,6 @@ cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
 cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
 cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
 cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
-cloud.google.com/go/storage v1.30.0 h1:g1yrbxAWOrvg/594228pETWkOi00MLTrOWfh56veU5o=
-cloud.google.com/go/storage v1.30.0/go.mod h1:xAVretHSROm1BQX4IIsoVgJqw0LqOyX+I/O2GzRAzdE=
 cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
 cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
 firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
@@ -126,10 +124,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 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/stripe/stripe-go/v74 v74.12.0 h1:uakz8Ubngok3G6Pcwc1ssqI3msONE4tdeyi84UooLQk=
-github.com/stripe/stripe-go/v74 v74.12.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
-github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8=
-github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/stripe/stripe-go/v74 v74.13.0 h1:n9VIeApHaGsqRQcEsr8ANldfFrLzFSasfNBkq0roPTw=
+github.com/stripe/stripe-go/v74 v74.13.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
+github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
+github.com/urfave/cli/v2 v2.25.1/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.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
@@ -193,8 +191,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.113.0 h1:3zLZyS9hgne8yoXUFy871yWdQcA2tA6wp59aaCT6Cp4=
-google.golang.org/api v0.113.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
 google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -206,17 +202,13 @@ 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-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
-google.golang.org/genproto v0.0.0-20230322174352-cde4c949918d h1:OE8TncEeAei3Tehf/P/Jdt/K+8GnTUrRY6wzYpbCes4=
-google.golang.org/genproto v0.0.0-20230322174352-cde4c949918d/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
+google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5 h1:Kd6tRRHXw8z4TlPlWi+NaK10gsePL6GdZBQChptOLGA=
+google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 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=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=

+ 1 - 0
server/config.go

@@ -107,6 +107,7 @@ type Config struct {
 	SMTPServerAddrPrefix                 string
 	MetricsEnable                        bool
 	MetricsListenHTTP                    string
+	ProfileListenHTTP                    string
 	MessageLimit                         int
 	MinDelay                             time.Duration
 	MaxDelay                             time.Duration

+ 19 - 0
server/server.go

@@ -19,6 +19,7 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"net/http/pprof"
 	"net/netip"
 	"net/url"
 	"os"
@@ -33,12 +34,15 @@ import (
 	"unicode/utf8"
 )
 
+import _ "net/http/pprof"
+
 // Server is the main server, providing the UI and API for ntfy
 type Server struct {
 	config            *Config
 	httpServer        *http.Server
 	httpsServer       *http.Server
 	httpMetricsServer *http.Server
+	httpProfileServer *http.Server
 	unixListener      net.Listener
 	smtpServer        *smtp.Server
 	smtpServerBackend *smtpBackend
@@ -217,6 +221,9 @@ func (s *Server) Run() error {
 	if s.config.MetricsListenHTTP != "" {
 		listenStr += fmt.Sprintf(" %s[http/metrics]", s.config.MetricsListenHTTP)
 	}
+	if s.config.ProfileListenHTTP != "" {
+		listenStr += fmt.Sprintf(" %s[http/profile]", s.config.ProfileListenHTTP)
+	}
 	log.Tag(tagStartup).Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
 	if log.IsFile() {
 		fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
@@ -273,6 +280,18 @@ func (s *Server) Run() error {
 		initMetrics()
 		s.metricsHandler = promhttp.Handler()
 	}
+	if s.config.ProfileListenHTTP != "" {
+		profileMux := http.NewServeMux()
+		profileMux.HandleFunc("/debug/pprof/", pprof.Index)
+		profileMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
+		profileMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
+		profileMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
+		profileMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
+		s.httpProfileServer = &http.Server{Addr: s.config.ProfileListenHTTP, Handler: profileMux}
+		go func() {
+			errChan <- s.httpProfileServer.ListenAndServe()
+		}()
+	}
 	if s.config.SMTPServerListen != "" {
 		go func() {
 			errChan <- s.runSMTPServer()

+ 8 - 0
server/server.yml

@@ -276,6 +276,14 @@
 # enable-metrics: false
 # metrics-listen-http:
 
+# Profiling
+#
+# ntfy can expose Go's net/http/pprof endpoints to support profiling of the ntfy server. If enabled, ntfy will listen
+# on a dedicated listen IP/port, which can be accessed via the web browser on http://<ip>:<port>/debug/pprof/.
+# This can be helpful to expose bottlenecks, and visualize call flows. See https://pkg.go.dev/net/http/pprof for details.
+#
+# profile-listen-http:
+
 # Logging options
 #
 # By default, ntfy logs to the console (stderr), with an "info" log level, and in a human-readable text format.

+ 69 - 0
tools/loadgen/main.go

@@ -0,0 +1,69 @@
+package main
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"net/http"
+	"os"
+	"time"
+)
+
+func main() {
+	baseURL := "https://staging.ntfy.sh"
+	if len(os.Args) > 1 {
+		baseURL = os.Args[1]
+	}
+	for i := 0; i < 2000; i++ {
+		go subscribe(i, baseURL)
+	}
+	time.Sleep(5 * time.Second)
+	for i := 0; i < 2000; i++ {
+		go func(worker int) {
+			for {
+				poll(worker, baseURL)
+			}
+		}(i)
+	}
+	time.Sleep(time.Hour)
+}
+
+func subscribe(worker int, baseURL string) {
+	fmt.Printf("[subscribe] worker=%d STARTING\n", worker)
+	start := time.Now()
+	topic, ip := fmt.Sprintf("subtopic%d", worker), fmt.Sprintf("1.2.%d.%d", (worker/255)%255, worker%255)
+	req, _ := http.NewRequest("GET", fmt.Sprintf("%s/%s/json", baseURL, topic), nil)
+	req.Header.Set("X-Forwarded-For", ip)
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		fmt.Printf("[subscribe] worker=%d time=%d error=%s\n", worker, time.Since(start).Milliseconds(), err.Error())
+		return
+	}
+	defer resp.Body.Close()
+	scanner := bufio.NewScanner(resp.Body)
+	for scanner.Scan() {
+		// Do nothing
+	}
+	fmt.Printf("[subscribe] worker=%d status=%d time=%d EXITED\n", worker, resp.StatusCode, time.Since(start).Milliseconds())
+}
+
+func poll(worker int, baseURL string) {
+	fmt.Printf("[poll] worker=%d STARTING\n", worker)
+	topic, ip := fmt.Sprintf("polltopic%d", worker), fmt.Sprintf("1.2.%d.%d", (worker/255)%255, worker%255)
+	start := time.Now()
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
+	defer cancel()
+
+	//req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://staging.ntfy.sh/%s/json?poll=1&since=all", topic), nil)
+	req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/json?poll=1&since=all", baseURL, topic), nil)
+	req.Header.Set("X-Forwarded-For", ip)
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		fmt.Printf("[poll] worker=%d time=%d status=- error=%s\n", worker, time.Since(start).Milliseconds(), err.Error())
+		cancel()
+		return
+	}
+	defer resp.Body.Close()
+	fmt.Printf("[poll] worker=%d time=%d status=%s\n", worker, time.Since(start).Milliseconds(), resp.Status)
+}

+ 221 - 166
web/package-lock.json

@@ -2271,9 +2271,9 @@
       "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
     },
     "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz",
-      "integrity": "sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA==",
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
       "dependencies": {
         "eslint-visitor-keys": "^3.3.0"
       },
@@ -2285,9 +2285,9 @@
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz",
-      "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.1.tgz",
+      "integrity": "sha512-BISJ6ZE4xQsuL/FmsyRaiffpq977bMlsKfGHTQrOGFErfByxIe6iZTxPf/00Zon9b9a7iUykfQwejN3s2ZW/Bw==",
       "engines": {
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
@@ -3134,15 +3134,15 @@
       "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
     },
     "node_modules/@mui/base": {
-      "version": "5.0.0-alpha.122",
-      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.122.tgz",
-      "integrity": "sha512-IgZEFQyHa39J1+Q3tekVdhPuUm1fr3icddaNLmiAIeYTVXmR7KR5FhBAIL0P+4shlPq0liUPGlXryoTm0iCeFg==",
+      "version": "5.0.0-alpha.123",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.123.tgz",
+      "integrity": "sha512-pxzcAfET3I6jvWqS4kijiLMn1OmdMw+mGmDa0SqmDZo3bXXdvLhpCCPqCkULG3UykhvFCOcU5HclOX3JCA+Zhg==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
         "@emotion/is-prop-valid": "^1.2.0",
         "@mui/types": "^7.2.3",
         "@mui/utils": "^5.11.13",
-        "@popperjs/core": "^2.11.6",
+        "@popperjs/core": "^2.11.7",
         "clsx": "^1.2.1",
         "prop-types": "^15.8.1",
         "react-is": "^18.2.0"
@@ -3166,9 +3166,9 @@
       }
     },
     "node_modules/@mui/core-downloads-tracker": {
-      "version": "5.11.14",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.14.tgz",
-      "integrity": "sha512-rfc08z6+3Fif+Gopx2/qmk+MEQlwYeA+gOcSK048BHkTty/ol/boHuVeL2BNC/cf9OVRjJLYHtVb/DeW791LSQ==",
+      "version": "5.11.15",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.15.tgz",
+      "integrity": "sha512-Q0e2oBsjHyIWWj1wLzl14btunvBYC0yl+px7zL9R69tF87uenj6q72ieS369BJ6jxYpJwvXfR6/f+TC+ZUsKKg==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui"
@@ -3200,14 +3200,14 @@
       }
     },
     "node_modules/@mui/material": {
-      "version": "5.11.14",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.14.tgz",
-      "integrity": "sha512-uoiUyybmo+M+nYARBygmbXgX6s/hH0NKD56LCAv9XvmdGVoXhEGjOvxI5/Bng6FS3NNybnA8V+rgZW1Z/9OJtA==",
+      "version": "5.11.15",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.15.tgz",
+      "integrity": "sha512-E5RbLq9/OvRKmGyeZawdnmFBCvhKkI/Zqgr0xFqW27TGwKLxObq/BreJc6Uu5Sbv8Fjj34vEAbRx6otfOyxn5w==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
-        "@mui/base": "5.0.0-alpha.122",
-        "@mui/core-downloads-tracker": "^5.11.14",
-        "@mui/system": "^5.11.14",
+        "@mui/base": "5.0.0-alpha.123",
+        "@mui/core-downloads-tracker": "^5.11.15",
+        "@mui/system": "^5.11.15",
         "@mui/types": "^7.2.3",
         "@mui/utils": "^5.11.13",
         "@types/react-transition-group": "^4.4.5",
@@ -3301,9 +3301,9 @@
       }
     },
     "node_modules/@mui/system": {
-      "version": "5.11.14",
-      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.14.tgz",
-      "integrity": "sha512-/MBv5dUoijJNEKEGi5tppIszGN0o2uejmeISi5vl0CLcaQsI1cd+uBgK+JYUP1VWvI/MtkWRLVSWtF2FWhu5Nw==",
+      "version": "5.11.15",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.15.tgz",
+      "integrity": "sha512-vCatoWCTnAPquoNifHbqMCMnOElEbLosVUeW0FQDyjCq+8yMABD9E6iY0s14O7iq1wD+qqU7rFAuDIVvJ/AzzA==",
       "dependencies": {
         "@babel/runtime": "^7.21.0",
         "@mui/private-theming": "^5.11.13",
@@ -3492,9 +3492,9 @@
       }
     },
     "node_modules/@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
+      "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/popperjs"
@@ -4015,9 +4015,9 @@
       "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA=="
     },
     "node_modules/@types/node": {
-      "version": "18.15.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.5.tgz",
-      "integrity": "sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew=="
+      "version": "18.15.10",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz",
+      "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ=="
     },
     "node_modules/@types/parse-json": {
       "version": "4.0.0",
@@ -4050,9 +4050,9 @@
       "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
     },
     "node_modules/@types/react": {
-      "version": "18.0.28",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
-      "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
+      "version": "18.0.30",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.30.tgz",
+      "integrity": "sha512-AnME2cHDH11Pxt/yYX6r0w448BfTwQOLEhQEjCdwB7QskEI7EKtxhGUsExTQe/MsY3D9D5rMtu62WRocw9A8FA==",
       "dependencies": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
@@ -4089,9 +4089,9 @@
       "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
     },
     "node_modules/@types/scheduler": {
-      "version": "0.16.2",
-      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
-      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+      "version": "0.16.3",
+      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
+      "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
     },
     "node_modules/@types/semver": {
       "version": "7.3.13",
@@ -4155,14 +4155,14 @@
       "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA=="
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.56.0.tgz",
-      "integrity": "sha512-ZNW37Ccl3oMZkzxrYDUX4o7cnuPgU+YrcaYXzsRtLB16I1FR5SHMqga3zGsaSliZADCWo2v8qHWqAYIj8nWCCg==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.0.tgz",
+      "integrity": "sha512-itag0qpN6q2UMM6Xgk6xoHa0D0/P+M17THnr4SVgqn9Rgam5k/He33MA7/D7QoJcdMxHFyX7U9imaBonAX/6qA==",
       "dependencies": {
         "@eslint-community/regexpp": "^4.4.0",
-        "@typescript-eslint/scope-manager": "5.56.0",
-        "@typescript-eslint/type-utils": "5.56.0",
-        "@typescript-eslint/utils": "5.56.0",
+        "@typescript-eslint/scope-manager": "5.57.0",
+        "@typescript-eslint/type-utils": "5.57.0",
+        "@typescript-eslint/utils": "5.57.0",
         "debug": "^4.3.4",
         "grapheme-splitter": "^1.0.4",
         "ignore": "^5.2.0",
@@ -4188,11 +4188,11 @@
       }
     },
     "node_modules/@typescript-eslint/experimental-utils": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.56.0.tgz",
-      "integrity": "sha512-sxWuj0eO5nItmKgZmsBbChVt90EhfkuncDCPbLAVeEJ+SCjXMcZN3AhhNbxed7IeGJ4XwsdL3/FMvD4r+FLqqA==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.57.0.tgz",
+      "integrity": "sha512-0RnrwGQ7MmgtOSnzB/rSGYr2iXENi6L+CtPzX3g5ovo0HlruLukSEKcc4s+q0IEc+DLTDc7Edan0Y4WSQ/bFhw==",
       "dependencies": {
-        "@typescript-eslint/utils": "5.56.0"
+        "@typescript-eslint/utils": "5.57.0"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -4206,13 +4206,13 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.56.0.tgz",
-      "integrity": "sha512-sn1OZmBxUsgxMmR8a8U5QM/Wl+tyqlH//jTqCg8daTAmhAk26L2PFhcqPLlYBhYUJMZJK276qLXlHN3a83o2cg==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz",
+      "integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.56.0",
-        "@typescript-eslint/types": "5.56.0",
-        "@typescript-eslint/typescript-estree": "5.56.0",
+        "@typescript-eslint/scope-manager": "5.57.0",
+        "@typescript-eslint/types": "5.57.0",
+        "@typescript-eslint/typescript-estree": "5.57.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -4232,12 +4232,12 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.56.0.tgz",
-      "integrity": "sha512-jGYKyt+iBakD0SA5Ww8vFqGpoV2asSjwt60Gl6YcO8ksQ8s2HlUEyHBMSa38bdLopYqGf7EYQMUIGdT/Luw+sw==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.57.0.tgz",
+      "integrity": "sha512-NANBNOQvllPlizl9LatX8+MHi7bx7WGIWYjPHDmQe5Si/0YEYfxSljJpoTyTWFTgRy3X8gLYSE4xQ2U+aCozSw==",
       "dependencies": {
-        "@typescript-eslint/types": "5.56.0",
-        "@typescript-eslint/visitor-keys": "5.56.0"
+        "@typescript-eslint/types": "5.57.0",
+        "@typescript-eslint/visitor-keys": "5.57.0"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -4248,12 +4248,12 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.56.0.tgz",
-      "integrity": "sha512-8WxgOgJjWRy6m4xg9KoSHPzBNZeQbGlQOH7l2QEhQID/+YseaFxg5J/DLwWSsi9Axj4e/cCiKx7PVzOq38tY4A==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.57.0.tgz",
+      "integrity": "sha512-kxXoq9zOTbvqzLbdNKy1yFrxLC6GDJFE2Yuo3KqSwTmDOFjUGeWSakgoXT864WcK5/NAJkkONCiKb1ddsqhLXQ==",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "5.56.0",
-        "@typescript-eslint/utils": "5.56.0",
+        "@typescript-eslint/typescript-estree": "5.57.0",
+        "@typescript-eslint/utils": "5.57.0",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       },
@@ -4274,9 +4274,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.56.0.tgz",
-      "integrity": "sha512-JyAzbTJcIyhuUhogmiu+t79AkdnqgPUEsxMTMc/dCZczGMJQh1MK2wgrju++yMN6AWroVAy2jxyPcPr3SWCq5w==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.57.0.tgz",
+      "integrity": "sha512-mxsod+aZRSyLT+jiqHw1KK6xrANm19/+VFALVFP5qa/aiJnlP38qpyaTd0fEKhWvQk6YeNZ5LGwI1pDpBRBhtQ==",
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
@@ -4286,12 +4286,12 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.56.0.tgz",
-      "integrity": "sha512-41CH/GncsLXOJi0jb74SnC7jVPWeVJ0pxQj8bOjH1h2O26jXN3YHKDT1ejkVz5YeTEQPeLCCRY0U2r68tfNOcg==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.0.tgz",
+      "integrity": "sha512-LTzQ23TV82KpO8HPnWuxM2V7ieXW8O142I7hQTxWIHDcCEIjtkat6H96PFkYBQqGFLW/G/eVVOB9Z8rcvdY/Vw==",
       "dependencies": {
-        "@typescript-eslint/types": "5.56.0",
-        "@typescript-eslint/visitor-keys": "5.56.0",
+        "@typescript-eslint/types": "5.57.0",
+        "@typescript-eslint/visitor-keys": "5.57.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -4312,16 +4312,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.56.0.tgz",
-      "integrity": "sha512-XhZDVdLnUJNtbzaJeDSCIYaM+Tgr59gZGbFuELgF7m0IY03PlciidS7UQNKLE0+WpUTn1GlycEr6Ivb/afjbhA==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.57.0.tgz",
+      "integrity": "sha512-ps/4WohXV7C+LTSgAL5CApxvxbMkl9B9AUZRtnEFonpIxZDIT7wC1xfvuJONMidrkB9scs4zhtRyIwHh4+18kw==",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@types/json-schema": "^7.0.9",
         "@types/semver": "^7.3.12",
-        "@typescript-eslint/scope-manager": "5.56.0",
-        "@typescript-eslint/types": "5.56.0",
-        "@typescript-eslint/typescript-estree": "5.56.0",
+        "@typescript-eslint/scope-manager": "5.57.0",
+        "@typescript-eslint/types": "5.57.0",
+        "@typescript-eslint/typescript-estree": "5.57.0",
         "eslint-scope": "^5.1.1",
         "semver": "^7.3.7"
       },
@@ -4357,11 +4357,11 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "5.56.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.56.0.tgz",
-      "integrity": "sha512-1mFdED7u5bZpX6Xxf5N9U2c18sb+8EvU3tyOIj6LQZ5OOvnmj8BVeNNP603OFPm5KkS1a7IvCIcwrdHXaEMG/Q==",
+      "version": "5.57.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.0.tgz",
+      "integrity": "sha512-ery2g3k0hv5BLiKpPuwYt9KBkAp2ugT6VvyShXdLOkax895EC55sP0Tx5L0fZaQueiK3fBLvHVvEl3jFS5ia+g==",
       "dependencies": {
-        "@typescript-eslint/types": "5.56.0",
+        "@typescript-eslint/types": "5.57.0",
         "eslint-visitor-keys": "^3.3.0"
       },
       "engines": {
@@ -4577,27 +4577,6 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
-    "node_modules/acorn-node": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
-      "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
-      "dependencies": {
-        "acorn": "^7.0.0",
-        "acorn-walk": "^7.0.0",
-        "xtend": "^4.0.2"
-      }
-    },
-    "node_modules/acorn-node/node_modules/acorn": {
-      "version": "7.4.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
-      "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
-      "bin": {
-        "acorn": "bin/acorn"
-      },
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
     "node_modules/acorn-walk": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
@@ -4740,6 +4719,11 @@
         "node": ">=4"
       }
     },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+    },
     "node_modules/anymatch": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -5507,9 +5491,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001469",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz",
-      "integrity": "sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==",
+      "version": "1.0.30001472",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001472.tgz",
+      "integrity": "sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg==",
       "funding": [
         {
           "type": "opencollective",
@@ -5518,6 +5502,10 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ]
     },
@@ -5951,9 +5939,9 @@
       }
     },
     "node_modules/css-declaration-sorter": {
-      "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz",
-      "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==",
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
+      "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==",
       "engines": {
         "node": "^10 || ^12 || >=14"
       },
@@ -6163,9 +6151,9 @@
       }
     },
     "node_modules/cssdb": {
-      "version": "7.4.1",
-      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.4.1.tgz",
-      "integrity": "sha512-0Q8NOMpXJ3iTDDbUv9grcmQAfdDx4qz+fN/+Md2FGbevT+6+bJNQ2LjB2YIUlLbpBTM32idU1Sb+tb/uGt6/XQ==",
+      "version": "7.5.2",
+      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.5.2.tgz",
+      "integrity": "sha512-Xpu7Bf5Vlw+G7ikA2Lg/lVCRTSY8D5M5qFUgGNFyS4pa8ufGLyCBxIX/3if3krHlF1SKSfVPI/YsAWLDVEbocw==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/csstools"
@@ -6459,14 +6447,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/defined": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz",
-      "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -6534,22 +6514,6 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
     },
-    "node_modules/detective": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz",
-      "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==",
-      "dependencies": {
-        "acorn-node": "^1.8.2",
-        "defined": "^1.0.0",
-        "minimist": "^1.2.6"
-      },
-      "bin": {
-        "detective": "bin/detective.js"
-      },
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
     "node_modules/dexie": {
       "version": "3.2.3",
       "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.3.tgz",
@@ -6603,9 +6567,9 @@
       "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg=="
     },
     "node_modules/dns-packet": {
-      "version": "5.4.0",
-      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz",
-      "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==",
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz",
+      "integrity": "sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==",
       "dependencies": {
         "@leichtgewicht/ip-codec": "^2.0.1"
       },
@@ -6758,9 +6722,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.335",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.335.tgz",
-      "integrity": "sha512-l/eowQqTnrq3gu+WSrdfkhfNHnPgYqlKAwxz7MTOj6mom19vpEDHNXl6dxDxyTiYuhemydprKr/HCrHfgk+OfQ=="
+      "version": "1.4.342",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.342.tgz",
+      "integrity": "sha512-dTei3VResi5bINDENswBxhL+N0Mw5YnfWyTqO75KGsVldurEkhC9+CelJVAse8jycWyP8pv3VSj4BSyP8wTWJA=="
     },
     "node_modules/emittery": {
       "version": "0.8.1",
@@ -7403,11 +7367,14 @@
       }
     },
     "node_modules/eslint-visitor-keys": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
-      "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz",
+      "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==",
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
     "node_modules/eslint-webpack-plugin": {
@@ -8977,9 +8944,9 @@
       }
     },
     "node_modules/immer": {
-      "version": "9.0.19",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz",
-      "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==",
+      "version": "9.0.21",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+      "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/immer"
@@ -11138,9 +11105,9 @@
       }
     },
     "node_modules/jest-watch-typeahead/node_modules/@types/yargs": {
-      "version": "17.0.23",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.23.tgz",
-      "integrity": "sha512-yuogunc04OnzGQCrfHx+Kk883Q4X0aSwmYZhKjI21m+SVYzjIbrWl8dOOwSv5hf2Um2pdCOXWo9isteZTNXUZQ==",
+      "version": "17.0.24",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
+      "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
       "dependencies": {
         "@types/yargs-parser": "*"
       }
@@ -11512,6 +11479,14 @@
         "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
+    "node_modules/jiti": {
+      "version": "1.18.2",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
+      "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
     "node_modules/js-base64": {
       "version": "3.7.5",
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz",
@@ -12113,10 +12088,26 @@
         "multicast-dns": "cli.js"
       }
     },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
     "node_modules/nanoid": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
-      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+      "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
@@ -15647,6 +15638,53 @@
       "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
       "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
     },
+    "node_modules/sucrase": {
+      "version": "3.31.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.31.0.tgz",
+      "integrity": "sha512-6QsHnkqyVEzYcaiHsOKkzOtOgdJcb8i54x6AV2hDwyZcY9ZyykGZVw6L/YN98xC0evwTP6utsWWrKRaa8QlfEQ==",
+      "dependencies": {
+        "commander": "^4.0.0",
+        "glob": "7.1.6",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sucrase/node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/sucrase/node_modules/glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -15791,19 +15829,19 @@
       "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
     },
     "node_modules/tailwindcss": {
-      "version": "3.2.7",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz",
-      "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.0.tgz",
+      "integrity": "sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==",
       "dependencies": {
         "arg": "^5.0.2",
         "chokidar": "^3.5.3",
         "color-name": "^1.1.4",
-        "detective": "^5.2.1",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
         "fast-glob": "^3.2.12",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
+        "jiti": "^1.17.2",
         "lilconfig": "^2.0.6",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
@@ -15817,7 +15855,8 @@
         "postcss-selector-parser": "^6.0.11",
         "postcss-value-parser": "^4.2.0",
         "quick-lru": "^5.1.1",
-        "resolve": "^1.22.1"
+        "resolve": "^1.22.1",
+        "sucrase": "^3.29.0"
       },
       "bin": {
         "tailwind": "lib/cli.js",
@@ -15895,9 +15934,9 @@
       }
     },
     "node_modules/terser": {
-      "version": "5.16.6",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.6.tgz",
-      "integrity": "sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg==",
+      "version": "5.16.8",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz",
+      "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.2",
         "acorn": "^8.5.0",
@@ -15967,6 +16006,25 @@
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
     },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/throat": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz",
@@ -16049,6 +16107,11 @@
       "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
       "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="
     },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+    },
     "node_modules/tsconfig-paths": {
       "version": "3.14.2",
       "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
@@ -17277,14 +17340,6 @@
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
       "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
     },
-    "node_modules/xtend": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
-      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
-      "engines": {
-        "node": ">=0.4"
-      }
-    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",