From 1b8954e9f32935f1cb8fe8ac4f7c8e2458bf30c4 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Mon, 24 Mar 2025 23:20:08 +0100 Subject: [PATCH 001/165] chore: fix linting issues of web_tls.go --- web_tls.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web_tls.go b/web_tls.go index fff9253..b1bb60e 100644 --- a/web_tls.go +++ b/web_tls.go @@ -38,7 +38,7 @@ func RunWebSecureServer() { TLSConfig: &tls.Config{ // TODO: cache certificate in persistent storage GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - hostname := WebSecureSelfSignedDefaultDomain + var hostname string if info.ServerName != "" { hostname = info.ServerName } else { @@ -58,7 +58,6 @@ func RunWebSecureServer() { if err != nil { panic(err) } - return } func createSelfSignedCert(hostname string) *tls.Certificate { From b5e0f894bc678e7a407af9d3545d77a9e03a8219 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Tue, 25 Mar 2025 14:53:59 +0100 Subject: [PATCH 002/165] chore: move smoketest to private repo --- .github/workflows/build.yml | 107 +--------------------------- .github/workflows/smoketest.yml | 122 ++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 105 deletions(-) create mode 100644 .github/workflows/smoketest.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84bc4b1..b31041f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,7 @@ jobs: build: runs-on: buildjet-4vcpu-ubuntu-2204 name: Build + if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'" steps: - name: Checkout uses: actions/checkout@v4 @@ -35,108 +36,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: jetkvm-app - path: bin/jetkvm_app - deploy_and_test: - runs-on: buildjet-4vcpu-ubuntu-2204 - name: Smoke test - needs: build - concurrency: - group: smoketest-jk - steps: - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: jetkvm-app - - name: Configure WireGuard and check connectivity - run: | - WG_KEY_FILE=$(mktemp) - echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ - sudo apt-get update && sudo apt-get install -y wireguard-tools && \ - sudo ip link add dev wg-ci type wireguard && \ - sudo ip addr add $CI_WG_IPS dev wg-ci && \ - sudo wg set wg-ci listen-port 51820 \ - private-key $WG_KEY_FILE \ - peer $CI_WG_PUBLIC \ - allowed-ips $CI_WG_ALLOWED_IPS \ - endpoint $CI_WG_ENDPOINT \ - persistent-keepalive 15 && \ - sudo ip link set up dev wg-ci && \ - sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci - ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) - env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }} - CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }} - CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }} - CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }} - CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }} - CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }} - - name: Configure SSH - run: | - # Write SSH private key to a file - SSH_PRIVATE_KEY=$(mktemp) - echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY - chmod 0600 $SSH_PRIVATE_KEY - # Configure SSH - mkdir -p ~/.ssh - cat <> ~/.ssh/config - Host jkci - HostName $CI_HOST - User $CI_USER - StrictHostKeyChecking no - UserKnownHostsFile /dev/null - IdentityFile $SSH_PRIVATE_KEY - EOF - env: - CI_USER: ${{ vars.JETKVM_CI_USER }} - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }} - - name: Deploy application - run: | - set -e - # Copy the binary to the remote host - echo "+ Copying the application to the remote host" - cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz" - # Deploy and run the application on the remote host - echo "+ Deploying the application on the remote host" - ssh jkci ash < /proc/sys/vm/drop_caches - # Reboot the application - reboot -d 5 -f & - EOF - sleep 10 - echo "Deployment complete, waiting for JetKVM to come back online " - function check_online() { - for i in {1..60}; do - if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then - echo "JetKVM is back online" - return 0 - fi - echo -n "." - sleep 1 - done - echo "JetKVM did not come back online within 60 seconds" - return 1 - } - check_online - env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - - name: Run smoke tests - run: | - echo "+ Checking the status of the device" - curl -v http://$CI_HOST/device/status && echo - echo "+ Collecting logs" - ssh jkci "cat /userdata/jetkvm/last.log" > last.log - cat last.log - env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - - name: Upload logs - uses: actions/upload-artifact@v4 - with: - name: device-logs - path: last.log + path: bin/jetkvm_app \ No newline at end of file diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml new file mode 100644 index 0000000..d5493e7 --- /dev/null +++ b/.github/workflows/smoketest.yml @@ -0,0 +1,122 @@ +name: smoketest +on: + repository_dispatch: + types: [smoketest] + +jobs: + ghbot_payload: + name: Ghbot payload + runs-on: ubuntu-latest + steps: + - name: "GH_CHECK_RUN_ID=${{ github.event.client_payload.check_run_id }}" + run: | + echo "== START GHBOT_PAYLOAD ==" + cat <<'GHPAYLOAD_EOF' | base64 + ${{ toJson(github.event.client_payload) }} + GHPAYLOAD_EOF + echo "== END GHBOT_PAYLOAD ==" + deploy_and_test: + runs-on: buildjet-4vcpu-ubuntu-2204 + name: Smoke test + concurrency: + group: smoketest-jk + steps: + - name: Download artifact + run: | + wget -O /tmp/jk.zip "${{ github.event.client_payload.artifact_download_url }}" + unzip /tmp/jk.zip + - name: Configure WireGuard and check connectivity + run: | + WG_KEY_FILE=$(mktemp) + echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ + sudo apt-get update && sudo apt-get install -y wireguard-tools && \ + sudo ip link add dev wg-ci type wireguard && \ + sudo ip addr add $CI_WG_IPS dev wg-ci && \ + sudo wg set wg-ci listen-port 51820 \ + private-key $WG_KEY_FILE \ + peer $CI_WG_PUBLIC \ + allowed-ips $CI_WG_ALLOWED_IPS \ + endpoint $CI_WG_ENDPOINT \ + persistent-keepalive 15 && \ + sudo ip link set up dev wg-ci && \ + sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci + ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }} + CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }} + CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }} + CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }} + CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }} + CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }} + - name: Configure SSH + run: | + # Write SSH private key to a file + SSH_PRIVATE_KEY=$(mktemp) + echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY + chmod 0600 $SSH_PRIVATE_KEY + # Configure SSH + mkdir -p ~/.ssh + cat <> ~/.ssh/config + Host jkci + HostName $CI_HOST + User $CI_USER + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + IdentityFile $SSH_PRIVATE_KEY + EOF + env: + CI_USER: ${{ vars.JETKVM_CI_USER }} + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }} + - name: Deploy application + run: | + set -e + # Copy the binary to the remote host + echo "+ Copying the application to the remote host" + cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz" + # Deploy and run the application on the remote host + echo "+ Deploying the application on the remote host" + ssh jkci ash < /proc/sys/vm/drop_caches + # Reboot the application + reboot -d 5 -f & + EOF + sleep 10 + echo "Deployment complete, waiting for JetKVM to come back online " + function check_online() { + for i in {1..60}; do + if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then + echo "JetKVM is back online" + return 0 + fi + echo -n "." + sleep 1 + done + echo "JetKVM did not come back online within 60 seconds" + return 1 + } + check_online + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Run smoke tests + run: | + echo "+ Checking the status of the device" + curl -v http://$CI_HOST/device/status && echo + echo "+ Waiting for 10 seconds to allow all services to start" + sleep 10 + echo "+ Collecting logs" + ssh jkci "cat /userdata/jetkvm/last.log" > last.log + cat last.log + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Upload logs + uses: actions/upload-artifact@v4 + with: + name: device-logs + path: last.log From aed453cc8cf7da19cd26a97f4cf1bf7b1291ac7b Mon Sep 17 00:00:00 2001 From: SuperQ Date: Wed, 12 Mar 2025 16:29:45 +0100 Subject: [PATCH 003/165] chore: Enable more linters Enable more golangci-lint linters. * `forbidigo` to stop use of non-logger console printing. * `goimports` to make sure `import` blocks are formatted nicely. * `misspell` to catch spelling mistakes. * `whitespace` to catch whitespace issues. Signed-off-by: SuperQ --- .golangci.yml | 14 ++++++++++++-- prometheus.go | 4 ---- serial.go | 1 - web.go | 2 +- web_tls.go | 8 ++++---- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ddf4443..95a1cb8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,12 +1,22 @@ --- linters: enable: - # - goimports - # - misspell + - forbidigo + - goimports + - misspell # - revive + - whitespace issues: exclude-rules: - path: _test.go linters: - errcheck + +linters-settings: + forbidigo: + forbid: + - p: ^fmt\.Print.*$ + msg: Do not commit print statements. Use logger package. + - p: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ + msg: Do not commit log statements. Use logger package. diff --git a/prometheus.go b/prometheus.go index 8ebf259..5d4c5e7 100644 --- a/prometheus.go +++ b/prometheus.go @@ -1,15 +1,11 @@ package kvm import ( - "net/http" - "github.com/prometheus/client_golang/prometheus" versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/common/version" ) -var promHandler http.Handler - func initPrometheus() { // A Prometheus metrics endpoint. version.Version = builtAppVersion diff --git a/serial.go b/serial.go index a4ab7d5..31fd553 100644 --- a/serial.go +++ b/serial.go @@ -66,7 +66,6 @@ func runATXControl() { newLedPWRState != ledPWRState || newBtnRSTState != btnRSTState || newBtnPWRState != btnPWRState { - logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v", newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState) diff --git a/web.go b/web.go index b35a2db..9201e7b 100644 --- a/web.go +++ b/web.go @@ -16,6 +16,7 @@ import ( "golang.org/x/crypto/bcrypt" ) +//nolint:typecheck //go:embed all:static var staticFiles embed.FS @@ -419,7 +420,6 @@ func handleSetup(c *gin.Context) { // Set the cookie c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true) - } else { // For noPassword mode, ensure the password field is empty config.HashedPassword = "" diff --git a/web_tls.go b/web_tls.go index fff9253..976cff6 100644 --- a/web_tls.go +++ b/web_tls.go @@ -8,10 +8,10 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" - "log" "math/big" "net" "net/http" + "os" "strings" "sync" "time" @@ -38,7 +38,7 @@ func RunWebSecureServer() { TLSConfig: &tls.Config{ // TODO: cache certificate in persistent storage GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - hostname := WebSecureSelfSignedDefaultDomain + var hostname string if info.ServerName != "" { hostname = info.ServerName } else { @@ -58,7 +58,6 @@ func RunWebSecureServer() { if err != nil { panic(err) } - return } func createSelfSignedCert(hostname string) *tls.Certificate { @@ -72,7 +71,8 @@ func createSelfSignedCert(hostname string) *tls.Certificate { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - log.Fatalf("Failed to generate private key: %v", err) + logger.Errorf("Failed to generate private key: %v", err) + os.Exit(1) } keyUsage := x509.KeyUsageDigitalSignature From df0d083a28552a0d0a9e693cfb7a91fe0062f28f Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sat, 29 Mar 2025 21:13:59 +0000 Subject: [PATCH 004/165] chore: Update README Discord Link Corrects Discord link in the help section. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b516d7..5d0e9d7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ We welcome contributions from the community! Whether it's improving the firmware ## I need help -The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://discord.gg/8MaAhua7NW). +The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://jetkvm.com/discord). ## I want to report an issue From 1e9adf81d433a23202be881de7a8cb4cbfca9953 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Thu, 3 Apr 2025 18:16:41 +0200 Subject: [PATCH 005/165] chore: skip websocket client if net isn't up or time sync hasn't complete --- cloud.go | 26 +++++++++++++++++++++----- main.go | 8 +++----- ntp.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/cloud.go b/cloud.go index a30a14c..4b9c2b4 100644 --- a/cloud.go +++ b/cloud.go @@ -90,11 +90,6 @@ func handleCloudRegister(c *gin.Context) { return } - if config.CloudToken == "" { - cloudLogger.Info("Starting websocket client due to adoption") - go RunWebsocketClient() - } - config.CloudToken = tokenResp.SecretToken provider, err := oidc.NewProvider(c, "https://accounts.google.com") @@ -130,6 +125,7 @@ func runWebsocketClient() error { time.Sleep(5 * time.Second) return fmt.Errorf("cloud token is not set") } + wsURL, err := url.Parse(config.CloudURL) if err != nil { return fmt.Errorf("failed to parse config.CloudURL: %w", err) @@ -253,6 +249,26 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess func RunWebsocketClient() { for { + // If the cloud token is not set, we don't need to run the websocket client. + if config.CloudToken == "" { + time.Sleep(5 * time.Second) + continue + } + + // If the network is not up, well, we can't connect to the cloud. + if !networkState.Up { + cloudLogger.Warn("waiting for network to be up, will retry in 3 seconds") + time.Sleep(3 * time.Second) + continue + } + + // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. + if isTimeSyncNeeded() && !timeSyncSuccess { + cloudLogger.Warn("system time is not synced, will retry in 3 seconds") + time.Sleep(3 * time.Second) + continue + } + err := runWebsocketClient() if err != nil { cloudLogger.Errorf("websocket client error: %v", err) diff --git a/main.go b/main.go index 6a55595..aeb3d85 100644 --- a/main.go +++ b/main.go @@ -72,11 +72,9 @@ func Main() { if config.TLSMode != "" { go RunWebSecureServer() } - // If the cloud token isn't set, the client won't be started by default. - // However, if the user adopts the device via the web interface, handleCloudRegister will start the client. - if config.CloudToken != "" { - go RunWebsocketClient() - } + // As websocket client already checks if the cloud token is set, we can start it here. + go RunWebsocketClient() + initSerialPort() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) diff --git a/ntp.go b/ntp.go index 39ea7af..27ec100 100644 --- a/ntp.go +++ b/ntp.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "os/exec" + "strconv" "time" "github.com/beevik/ntp" @@ -20,13 +21,41 @@ const ( ) var ( + builtTimestamp string timeSyncRetryInterval = 0 * time.Second + timeSyncSuccess = false defaultNTPServers = []string{ "time.cloudflare.com", "time.apple.com", } ) +func isTimeSyncNeeded() bool { + if builtTimestamp == "" { + logger.Warnf("Built timestamp is not set, time sync is needed") + return true + } + + ts, err := strconv.Atoi(builtTimestamp) + if err != nil { + logger.Warnf("Failed to parse built timestamp: %v", err) + return true + } + + // builtTimestamp is UNIX timestamp in seconds + builtTime := time.Unix(int64(ts), 0) + now := time.Now() + + logger.Tracef("Built time: %v, now: %v", builtTime, now) + + if now.Sub(builtTime) < 0 { + logger.Warnf("System time is behind the built time, time sync is needed") + return true + } + + return false +} + func TimeSyncLoop() { for { if !networkState.checked { @@ -40,6 +69,9 @@ func TimeSyncLoop() { continue } + // check if time sync is needed, but do nothing for now + isTimeSyncNeeded() + logger.Infof("Syncing system time") start := time.Now() err := SyncSystemTime() @@ -56,6 +88,7 @@ func TimeSyncLoop() { continue } + timeSyncSuccess = true logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start)) time.Sleep(timeSyncInterval) // after the first sync is done } From f3b5011d65ade31b34aad01a2c1e670582810f28 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Thu, 3 Apr 2025 19:06:21 +0200 Subject: [PATCH 006/165] feat(cloud): add metrics for cloud connections --- cloud.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/cloud.go b/cloud.go index 4b9c2b4..be53b08 100644 --- a/cloud.go +++ b/cloud.go @@ -10,6 +10,8 @@ import ( "time" "github.com/coder/websocket/wsjson" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/coreos/go-oidc/v3/oidc" @@ -36,6 +38,97 @@ const ( CloudWebSocketPingInterval = 15 * time.Second ) +var ( + metricCloudConnectionStatus = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_status", + Help: "The status of the cloud connection", + }, + ) + metricCloudConnectionEstablishedTimestamp = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_established_timestamp", + Help: "The timestamp when the cloud connection was established", + }, + ) + metricCloudConnectionLastPingTimestamp = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_last_ping_timestamp", + Help: "The timestamp when the last ping response was received", + }, + ) + metricCloudConnectionLastPingDuration = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_last_ping_duration", + Help: "The duration of the last ping response", + }, + ) + metricCloudConnectionPingDuration = promauto.NewHistogram( + prometheus.HistogramOpts{ + Name: "jetkvm_cloud_connection_ping_duration", + Help: "The duration of the ping response", + Buckets: []float64{ + 0.1, 0.5, 1, 10, + }, + }, + ) + metricCloudConnectionTotalPingCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_cloud_connection_total_ping_count", + Help: "The total number of pings sent to the cloud", + }, + ) + metricCloudConnectionSessionRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_cloud_connection_session_total_request_count", + Help: "The total number of session requests received from the cloud", + }, + ) + metricCloudConnectionSessionRequestDuration = promauto.NewHistogram( + prometheus.HistogramOpts{ + Name: "jetkvm_cloud_connection_session_request_duration", + Help: "The duration of session requests", + Buckets: []float64{ + 0.1, 0.5, 1, 10, + }, + }, + ) + metricCloudConnectionLastSessionRequestTimestamp = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_last_session_request_timestamp", + Help: "The timestamp of the last session request", + }, + ) + metricCloudConnectionLastSessionRequestDuration = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_last_session_request_duration", + Help: "The duration of the last session request", + }, + ) + metricCloudConnectionFailureCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_cloud_connection_failure_count", + Help: "The number of times the cloud connection has failed", + }, + ) +) + +func cloudResetMetrics(established bool) { + metricCloudConnectionLastPingTimestamp.Set(-1) + metricCloudConnectionLastPingDuration.Set(-1) + + metricCloudConnectionLastSessionRequestTimestamp.Set(-1) + metricCloudConnectionLastSessionRequestDuration.Set(-1) + + if established { + metricCloudConnectionEstablishedTimestamp.SetToCurrentTime() + metricCloudConnectionStatus.Set(1) + } else { + metricCloudConnectionEstablishedTimestamp.Set(-1) + metricCloudConnectionStatus.Set(-1) + } +} + func handleCloudRegister(c *gin.Context) { var req CloudRegisterRequest @@ -130,15 +223,18 @@ func runWebsocketClient() error { if err != nil { return fmt.Errorf("failed to parse config.CloudURL: %w", err) } + if wsURL.Scheme == "http" { wsURL.Scheme = "ws" } else { wsURL.Scheme = "wss" } + header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) header.Set("Authorization", "Bearer "+config.CloudToken) dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) + defer cancelDial() c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, @@ -148,17 +244,35 @@ func runWebsocketClient() error { } defer c.CloseNow() //nolint:errcheck cloudLogger.Infof("websocket connected to %s", wsURL) + + // set the metrics when we successfully connect to the cloud. + cloudResetMetrics(true) + runCtx, cancelRun := context.WithCancel(context.Background()) defer cancelRun() go func() { for { time.Sleep(CloudWebSocketPingInterval) + + // set the timer for the ping duration + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + metricCloudConnectionLastPingDuration.Set(v) + metricCloudConnectionPingDuration.Observe(v) + })) + err := c.Ping(runCtx) + if err != nil { cloudLogger.Warnf("websocket ping error: %v", err) cancelRun() return } + + // dont use `defer` here because we want to observe the duration of the ping + timer.ObserveDuration() + + metricCloudConnectionTotalPingCount.Inc() + metricCloudConnectionLastPingTimestamp.SetToCurrentTime() } }() for { @@ -180,6 +294,8 @@ func runWebsocketClient() error { cloudLogger.Infof("new session request: %v", req.OidcGoogle) cloudLogger.Tracef("session request info: %v", req) + metricCloudConnectionSessionRequestCount.Inc() + metricCloudConnectionLastSessionRequestTimestamp.SetToCurrentTime() err = handleSessionRequest(runCtx, c, req) if err != nil { cloudLogger.Infof("error starting new session: %v", err) @@ -189,6 +305,12 @@ func runWebsocketClient() error { } func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + metricCloudConnectionLastSessionRequestDuration.Set(v) + metricCloudConnectionSessionRequestDuration.Observe(v) + })) + defer timer.ObserveDuration() + oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout) defer cancelOIDC() provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") @@ -249,6 +371,9 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess func RunWebsocketClient() { for { + // reset the metrics when we start the websocket client. + cloudResetMetrics(false) + // If the cloud token is not set, we don't need to run the websocket client. if config.CloudToken == "" { time.Sleep(5 * time.Second) @@ -272,6 +397,8 @@ func RunWebsocketClient() { err := runWebsocketClient() if err != nil { cloudLogger.Errorf("websocket client error: %v", err) + metricCloudConnectionStatus.Set(0) + metricCloudConnectionFailureCount.Inc() time.Sleep(5 * time.Second) } } From 8268b20f325abb23cdeb833204754856c0d82339 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 3 Apr 2025 19:32:14 +0200 Subject: [PATCH 007/165] refactor: Update WebRTC connection handling and overlays (#320) * refactor: Update WebRTC connection handling and overlays * fix: Update comments for WebRTC connection handling in KvmIdRoute * chore: Clean up import statements in devices.$id.tsx --- ui/src/components/Header.tsx | 6 +- ui/src/components/USBStateStatus.tsx | 2 +- ui/src/components/VideoOverlay.tsx | 67 ++++- ui/src/components/WebRTCVideo.tsx | 101 +++++--- ui/src/routes/devices.$id.other-session.tsx | 4 +- ui/src/routes/devices.$id.tsx | 263 ++++++++++---------- 6 files changed, 263 insertions(+), 180 deletions(-) diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index cdbc3c4..03a907e 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -36,7 +36,7 @@ export default function DashboardNavbar({ picture, kvmName, }: NavbarProps) { - const peerConnectionState = useRTCStore(state => state.peerConnectionState); + const peerConnection = useRTCStore(state => state.peerConnection); const setUser = useUserStore(state => state.setUser); const navigate = useNavigate(); const onLogout = useCallback(async () => { @@ -82,14 +82,14 @@ export default function DashboardNavbar({
diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/USBStateStatus.tsx index 8feb458..f0b2cb2 100644 --- a/ui/src/components/USBStateStatus.tsx +++ b/ui/src/components/USBStateStatus.tsx @@ -30,7 +30,7 @@ export default function USBStateStatus({ peerConnectionState, }: { state: USBStates; - peerConnectionState: RTCPeerConnectionState | null; + peerConnectionState?: RTCPeerConnectionState | null; }) { const StatusCardProps: StatusProps = { configured: { diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index f13b6ce..0620af4 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -1,6 +1,6 @@ import React from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; -import { ArrowRightIcon } from "@heroicons/react/16/solid"; +import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; import { LuPlay } from "react-icons/lu"; @@ -25,12 +25,12 @@ interface LoadingOverlayProps { show: boolean; } -export function LoadingOverlay({ show }: LoadingOverlayProps) { +export function LoadingVideoOverlay({ show }: LoadingOverlayProps) { return ( {show && ( {show && ( + +
+
+ +
+

+ {text} +

+
+
+
+ )} +
+ ); +} + +interface ConnectionErrorOverlayProps { + show: boolean; + setupPeerConnection: () => Promise; +} + +export function ConnectionErrorOverlay({ + show, + setupPeerConnection, +}: ConnectionErrorOverlayProps) { + return ( + + {show && ( + @@ -87,14 +125,21 @@ export function ConnectionErrorOverlay({ show }: ConnectionErrorOverlayProps) {
  • Try restarting both the device and your computer
  • -
    +
    +
    diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 911c5ea..5d8fb55 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -19,9 +19,8 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; import { HDMIErrorOverlay, + LoadingVideoOverlay, NoAutoplayPermissionsOverlay, - ConnectionErrorOverlay, - LoadingOverlay, } from "./VideoOverlay"; export default function WebRTCVideo() { @@ -46,15 +45,13 @@ export default function WebRTCVideo() { // RTC related states const peerConnection = useRTCStore(state => state.peerConnection); - const peerConnectionState = useRTCStore(state => state.peerConnectionState); // HDMI and UI states const hdmiState = useVideoStore(state => state.hdmiState); const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); - const isLoading = !hdmiError && !isPlaying; - const isConnectionError = ["error", "failed", "disconnected", "closed"].includes( - peerConnectionState || "", - ); + const isVideoLoading = !isPlaying; + + // console.log("peerConnection?.connectionState", peerConnection?.connectionState); // Keyboard related states const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = @@ -379,25 +376,52 @@ export default function WebRTCVideo() { } }, []); + const addStreamToVideoElm = useCallback( + (mediaStream: MediaStream) => { + if (!videoElm.current) return; + const videoElmRefValue = videoElm.current; + console.log("Adding stream to video element", videoElmRefValue); + videoElmRefValue.srcObject = mediaStream; + updateVideoSizeStore(videoElmRefValue); + }, + [updateVideoSizeStore], + ); + + useEffect( + function updateVideoStreamOnNewTrack() { + if (!peerConnection) return; + const abortController = new AbortController(); + const signal = abortController.signal; + + peerConnection.addEventListener( + "track", + (e: RTCTrackEvent) => { + console.log("Adding stream to video element"); + addStreamToVideoElm(e.streams[0]); + }, + { signal }, + ); + + return () => { + abortController.abort(); + }; + }, + [addStreamToVideoElm, peerConnection], + ); + useEffect( function updateVideoStream() { if (!mediaStream) return; - if (!videoElm.current) return; - if (peerConnection?.iceConnectionState !== "connected") return; - - setTimeout(() => { - if (videoElm?.current) { - videoElm.current.srcObject = mediaStream; - } - }, 0); - updateVideoSizeStore(videoElm.current); + console.log("Updating video stream from mediaStream"); + // We set the as early as possible + addStreamToVideoElm(mediaStream); }, [ setVideoClientSize, - setVideoSize, mediaStream, updateVideoSizeStore, - peerConnection?.iceConnectionState, + peerConnection, + addStreamToVideoElm, ], ); @@ -474,6 +498,8 @@ export default function WebRTCVideo() { const local = resetMousePosition; window.addEventListener("blur", local, { signal }); document.addEventListener("visibilitychange", local, { signal }); + const preventContextMenu = (e: MouseEvent) => e.preventDefault(); + videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal }); return () => { abortController.abort(); @@ -517,17 +543,17 @@ export default function WebRTCVideo() { ); const hasNoAutoPlayPermissions = useMemo(() => { - if (peerConnectionState !== "connected") return false; + if (peerConnection?.connectionState !== "connected") return false; if (isPlaying) return false; if (hdmiError) return false; if (videoHeight === 0 || videoWidth === 0) return false; return true; - }, [peerConnectionState, isPlaying, hdmiError, videoHeight, videoWidth]); + }, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]); return (
    -
    +
    videoElm.current?.requestFullscreen({ @@ -575,28 +601,29 @@ export default function WebRTCVideo() { "cursor-none": settings.mouseMode === "absolute" && settings.isCursorHidden, - "opacity-0": isLoading || isConnectionError || hdmiError, + "opacity-0": isVideoLoading || hdmiError, "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20": isPlaying, }, )} /> -
    -
    - - - - { - videoElm.current?.play(); - }} - /> + {peerConnection?.connectionState == "connected" && ( +
    +
    + + + { + videoElm.current?.play(); + }} + /> +
    -
    + )}
    diff --git a/ui/src/routes/devices.$id.other-session.tsx b/ui/src/routes/devices.$id.other-session.tsx index 2805666..16cb479 100644 --- a/ui/src/routes/devices.$id.other-session.tsx +++ b/ui/src/routes/devices.$id.other-session.tsx @@ -6,7 +6,7 @@ import LogoBlue from "@/assets/logo-blue.svg"; import LogoWhite from "@/assets/logo-white.svg"; interface ContextType { - connectWebRTC: () => Promise; + setupPeerConnection: () => Promise; } /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ @@ -16,7 +16,7 @@ export default function OtherSessionRoute() { // Function to handle closing the modal const handleClose = () => { - outletContext?.connectWebRTC().then(() => navigate("..")); + outletContext?.setupPeerConnection().then(() => navigate("..")); }; return ( diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 50fc79f..d2662fc 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -45,6 +45,10 @@ import Modal from "../components/Modal"; import { useDeviceUiNavigation } from "../hooks/useAppNavigation"; import { FeatureFlagProvider } from "../providers/FeatureFlagProvider"; import notifications from "../notifications"; +import { + ConnectionErrorOverlay, + LoadingConnectionOverlay, +} from "../components/VideoOverlay"; import { SystemVersionInfo } from "./devices.$id.settings.general.update"; import { DeviceStatus } from "./welcome-local"; @@ -126,8 +130,6 @@ export default function KvmIdRoute() { const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse); const peerConnection = useRTCStore(state => state.peerConnection); - const peerConnectionState = useRTCStore(state => state.peerConnectionState); - const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState); const setMediaMediaStream = useRTCStore(state => state.setMediaStream); const setPeerConnection = useRTCStore(state => state.setPeerConnection); const setDiskChannel = useRTCStore(state => state.setDiskChannel); @@ -135,78 +137,55 @@ export default function KvmIdRoute() { const setTransceiver = useRTCStore(state => state.setTransceiver); const location = useLocation(); - const [connectionAttempts, setConnectionAttempts] = useState(0); - - const [startedConnectingAt, setStartedConnectingAt] = useState(null); - const [connectedAt, setConnectedAt] = useState(null); - const [connectionFailed, setConnectionFailed] = useState(false); const navigate = useNavigate(); const { otaState, setOtaState, setModalView } = useUpdateStore(); + const [loadingMessage, setLoadingMessage] = useState("Connecting to device..."); const closePeerConnection = useCallback( function closePeerConnection() { + console.log("Closing peer connection"); + + setConnectionFailed(true); + connectionFailedRef.current = true; + peerConnection?.close(); - // "closed" is a valid RTCPeerConnection state according to the WebRTC spec - // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState#closed - // However, the onconnectionstatechange event doesn't fire when close() is called manually - // So we need to explicitly update our state to maintain consistency - // I don't know why this is happening, but this is the best way I can think of to handle it - // ALSO, this will render the connection error overlay linking to docs - setPeerConnectionState("closed"); + signalingAttempts.current = 0; }, - [peerConnection, setPeerConnectionState], + [peerConnection], ); + // We need to track connectionFailed in a ref to avoid stale closure issues + // This is necessary because syncRemoteSessionDescription is a callback that captures + // the connectionFailed value at creation time, but we need the latest value + // when the function is actually called. Without this ref, the function would use + // a stale value of connectionFailed in some conditions. + // + // We still need the state variable for UI rendering, so we sync the ref with the state. + // This pattern is a workaround for what useEvent hook would solve more elegantly + // (which would give us a callback that always has access to latest state without re-creation). + const connectionFailedRef = useRef(false); useEffect(() => { - const connectionAttemptsThreshold = 30; - if (connectionAttempts > connectionAttemptsThreshold) { - console.log(`Connection failed after ${connectionAttempts} attempts.`); - setConnectionFailed(true); - closePeerConnection(); - } - }, [connectionAttempts, closePeerConnection]); - - useEffect(() => { - // Skip if already connected - if (connectedAt) return; - - // Skip if connection is declared as failed - if (connectionFailed) return; - - const interval = setInterval(() => { - console.log("Checking connection status"); - - // Skip if connection hasn't started - if (!startedConnectingAt) return; - - const elapsedTime = Math.floor( - new Date().getTime() - startedConnectingAt.getTime(), - ); - - // Fail connection if it's been over X seconds since we started connecting - if (elapsedTime > 60 * 1000) { - console.error(`Connection failed after ${elapsedTime} ms.`); - setConnectionFailed(true); - closePeerConnection(); - } - }, 1000); - - return () => clearInterval(interval); - }, [closePeerConnection, connectedAt, connectionFailed, startedConnectingAt]); - - const sdp = useCallback( - async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => { - if (!pc) return; - if (event.candidate !== null) return; + connectionFailedRef.current = connectionFailed; + }, [connectionFailed]); + const signalingAttempts = useRef(0); + const syncRemoteSessionDescription = useCallback( + async function syncRemoteSessionDescription(pc: RTCPeerConnection) { try { + if (!pc) return; + const sd = btoa(JSON.stringify(pc.localDescription)); const sessionUrl = isOnDevice ? `${DEVICE_API}/webrtc/session` : `${CLOUD_API}/webrtc/session`; + + console.log("Trying to get remote session description"); + setLoadingMessage( + `Getting remote session description... ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`, + ); const res = await api.POST(sessionUrl, { sd, // When on device, we don't need to specify the device id, as it's already known @@ -214,73 +193,109 @@ export default function KvmIdRoute() { }); const json = await res.json(); - - if (isOnDevice) { - if (res.status === 401) { - return navigate("/login-local"); - } + if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login"); + if (!res.ok) { + console.error("Error getting SDP", { status: res.status, json }); + throw new Error("Error getting SDP"); } - if (isInCloud) { - // The cloud API returns a 401 if the user is not logged in - // Most likely the session has expired - if (res.status === 401) return navigate("/login"); + console.log("Successfully got Remote Session Description. Setting."); + setLoadingMessage("Setting remote session description..."); - // If can be a few things - // - In cloud mode, the cloud api would return a 404, if the device hasn't contacted the cloud yet - // - In device mode, the device api would timeout, the fetch would throw an error, therefore the catch block would be hit - // Regardless, we should close the peer connection and let the useInterval handle reconnecting - if (!res.ok) { - closePeerConnection(); - console.error(`Error setting SDP - Status: ${res.status}}`, json); - return; - } - } + const decodedSd = atob(json.sd); + const parsedSd = JSON.parse(decodedSd); + pc.setRemoteDescription(new RTCSessionDescription(parsedSd)); - pc.setRemoteDescription( - new RTCSessionDescription(JSON.parse(atob(json.sd))), - ).catch(e => console.log(`Error setting remote description: ${e}`)); + await new Promise((resolve, reject) => { + console.log("Waiting for remote description to be set"); + const maxAttempts = 10; + const interval = 1000; + let attempts = 0; + + const checkInterval = setInterval(() => { + attempts++; + // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects + if (pc.sctp?.state === "connected") { + console.log("Remote description set"); + clearInterval(checkInterval); + resolve(true); + } else if (attempts >= maxAttempts) { + console.log( + `Failed to get remote description after ${maxAttempts} attempts`, + ); + closePeerConnection(); + clearInterval(checkInterval); + reject( + new Error( + `Failed to get remote description after ${maxAttempts} attempts`, + ), + ); + } else { + console.log("Waiting for remote description to be set"); + } + }, interval); + }); } catch (error) { - console.error(`Error setting SDP: ${error}`); - closePeerConnection(); + console.error("Error getting SDP", { error }); + console.log("Connection failed", connectionFailedRef.current); + if (connectionFailedRef.current) return; + if (signalingAttempts.current < 5) { + signalingAttempts.current++; + await new Promise(resolve => setTimeout(resolve, 500)); + console.log("Attempting to get SDP again", signalingAttempts.current); + syncRemoteSessionDescription(pc); + } else { + closePeerConnection(); + } } }, [closePeerConnection, navigate, params.id], ); - const connectWebRTC = useCallback(async () => { - console.log("Attempting to connect WebRTC"); - - // Track connection status to detect failures and show error overlay - setConnectionAttempts(x => x + 1); - setStartedConnectingAt(new Date()); - setConnectedAt(null); + const setupPeerConnection = useCallback(async () => { + console.log("Setting up peer connection"); + setConnectionFailed(false); + setLoadingMessage("Connecting to device..."); let pc: RTCPeerConnection; try { + console.log("Creating peer connection"); + setLoadingMessage("Creating peer connection..."); pc = new RTCPeerConnection({ // We only use STUN or TURN servers if we're in the cloud ...(isInCloud && iceConfig?.iceServers ? { iceServers: [iceConfig?.iceServers] } : {}), }); + console.log("Peer connection created", pc); + setLoadingMessage("Peer connection created"); } catch (e) { console.error(`Error creating peer connection: ${e}`); - closePeerConnection(); + setTimeout(() => { + closePeerConnection(); + }, 1000); return; } // Set up event listeners and data channels pc.onconnectionstatechange = () => { - // If the connection state is connected, we reset the connection attempts. - if (pc.connectionState === "connected") { - setConnectionAttempts(0); - setConnectedAt(new Date()); - } - setPeerConnectionState(pc.connectionState); + console.log("Connection state changed", pc.connectionState); }; - pc.onicecandidate = event => sdp(event, pc); + pc.onicegatheringstatechange = event => { + const pc = event.currentTarget as RTCPeerConnection; + console.log("ICE Gathering State Changed", pc.iceGatheringState); + if (pc.iceGatheringState === "complete") { + console.log("ICE Gathering completed"); + setLoadingMessage("ICE Gathering completed"); + + // We can now start the https/ws connection to get the remote session description from the KVM device + syncRemoteSessionDescription(pc); + } else if (pc.iceGatheringState === "gathering") { + console.log("ICE Gathering Started"); + setLoadingMessage("Gathering ICE candidates..."); + } + }; pc.ontrack = function (event) { setMediaMediaStream(event.streams[0]); @@ -298,56 +313,32 @@ export default function KvmIdRoute() { setDiskChannel(diskDataChannel); }; + setPeerConnection(pc); + try { const offer = await pc.createOffer(); await pc.setLocalDescription(offer); - setPeerConnection(pc); } catch (e) { - console.error(`Error creating offer: ${e}`); + console.error(`Error creating offer: ${e}`, new Date().toISOString()); closePeerConnection(); } }, [ closePeerConnection, iceConfig?.iceServers, - sdp, setDiskChannel, setMediaMediaStream, setPeerConnection, - setPeerConnectionState, setRpcDataChannel, setTransceiver, + syncRemoteSessionDescription, ]); - useEffect(() => { - console.log("Attempting to connect WebRTC"); - - // If we're in an other session, we don't need to connect - if (location.pathname.includes("other-session")) return; - - // If we're already connected or connecting, we don't need to connect - // We have to use the state from the store, because the peerConnection.connectionState doesnt trigger a value change, if called manually from .close() - if (["connected", "connecting", "new"].includes(peerConnectionState ?? "")) { - return; - } - - // In certain cases, we want to never connect again. This happens when we've tried for a long time and failed - if (connectionFailed) { - console.log("Connection failed. We won't attempt to connect again."); - return; - } - - const interval = setInterval(() => { - connectWebRTC(); - }, 3000); - return () => clearInterval(interval); - }, [connectWebRTC, connectionFailed, location.pathname, peerConnectionState]); - // On boot, if the connection state is undefined, we connect to the WebRTC useEffect(() => { if (peerConnection?.connectionState === undefined) { - connectWebRTC(); + setupPeerConnection(); } - }, [connectWebRTC, peerConnection?.connectionState]); + }, [setupPeerConnection, peerConnection?.connectionState]); // Cleanup effect const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats); @@ -601,7 +592,27 @@ export default function KvmIdRoute() { kvmName={deviceName || "JetKVM Device"} /> -
    +
    +
    +
    + + +
    +
    +
    @@ -618,7 +629,7 @@ export default function KvmIdRoute() { > {/* The 'used by other session' modal needs to have access to the connectWebRTC function */} - +
    From 73e715117ebc807cad9aca163c06b9c0fb89807c Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Fri, 4 Apr 2025 12:58:19 +0200 Subject: [PATCH 008/165] feat(cloud): disconnect from cloud immediately when cloud URL changes or user requests to deregister --- cloud.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ jsonrpc.go | 5 +++++ 2 files changed, 49 insertions(+) diff --git a/cloud.go b/cloud.go index be53b08..f91085a 100644 --- a/cloud.go +++ b/cloud.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "sync" "time" "github.com/coder/websocket/wsjson" @@ -113,6 +114,11 @@ var ( ) ) +var ( + cloudDisconnectChan chan error + cloudDisconnectLock = &sync.Mutex{} +) + func cloudResetMetrics(established bool) { metricCloudConnectionLastPingTimestamp.Set(-1) metricCloudConnectionLastPingDuration.Set(-1) @@ -213,6 +219,24 @@ func handleCloudRegister(c *gin.Context) { c.JSON(200, gin.H{"message": "Cloud registration successful"}) } +func disconnectCloud(reason error) { + cloudDisconnectLock.Lock() + defer cloudDisconnectLock.Unlock() + + if cloudDisconnectChan == nil { + cloudLogger.Tracef("cloud disconnect channel is not set, no need to disconnect") + return + } + + // just in case the channel is closed, we don't want to panic + defer func() { + if r := recover(); r != nil { + cloudLogger.Infof("cloud disconnect channel is closed, no need to disconnect: %v", r) + } + }() + cloudDisconnectChan <- reason +} + func runWebsocketClient() error { if config.CloudToken == "" { time.Sleep(5 * time.Second) @@ -275,6 +299,23 @@ func runWebsocketClient() error { metricCloudConnectionLastPingTimestamp.SetToCurrentTime() } }() + + // create a channel to receive the disconnect event, once received, we cancelRun + cloudDisconnectChan = make(chan error) + defer func() { + close(cloudDisconnectChan) + cloudDisconnectChan = nil + }() + go func() { + for err := range cloudDisconnectChan { + if err == nil { + continue + } + cloudLogger.Infof("disconnecting from cloud due to: %v", err) + cancelRun() + } + }() + for { typ, msg, err := c.Read(runCtx) if err != nil { @@ -448,6 +489,9 @@ func rpcDeregisterDevice() error { return fmt.Errorf("failed to save configuration after deregistering: %w", err) } + cloudLogger.Infof("device deregistered, disconnecting from cloud") + disconnectCloud(fmt.Errorf("device deregistered")) + return nil } diff --git a/jsonrpc.go b/jsonrpc.go index 64935e1..9ce1f1b 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -771,9 +771,14 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { } func rpcSetCloudUrl(apiUrl string, appUrl string) error { + currentCloudURL := config.CloudURL config.CloudURL = apiUrl config.CloudAppURL = appUrl + if currentCloudURL != apiUrl { + disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl)) + } + if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } From fa1b11b228a2c432415eed57d5dd5708d81fa0e8 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:43:03 +0200 Subject: [PATCH 009/165] chore(ota): allow a longer timeout when downloading packages (#332) --- ota.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ota.go b/ota.go index f813c09..9c583b6 100644 --- a/ota.go +++ b/ota.go @@ -126,7 +126,15 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error creating request: %w", err) } - resp, err := http.DefaultClient.Do(req) + client := http.Client{ + // allow a longer timeout for the download but keep the TLS handshake short + Timeout: 10 * time.Minute, + Transport: &http.Transport{ + TLSHandshakeTimeout: 1 * time.Minute, + }, + } + + resp, err := client.Do(req) if err != nil { return fmt.Errorf("error downloading file: %w", err) } From 1a30977085ba1795a1e6827976f2346c722fdac4 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 9 Apr 2025 00:10:38 +0200 Subject: [PATCH 010/165] Feat/Trickle ice (#336) * feat(cloud): Use Websocket signaling in cloud mode * refactor: Enhance WebRTC signaling and connection handling * refactor: Improve WebRTC connection management and logging in KvmIdRoute * refactor: Update PeerConnectionDisconnectedOverlay to use Card component for better UI structure * refactor: Standardize metric naming and improve websocket logging * refactor: Rename WebRTC signaling functions and update deployment script for debug version * fix: Handle error when writing new ICE candidate to WebRTC signaling channel * refactor: Rename signaling handler function for clarity * refactor: Remove old http local http endpoint * refactor: Improve metric help text and standardize comparison operator in KvmIdRoute * chore(websocket): use MetricVec instead of Metric to store metrics * fix conflicts * fix: use wss when the page is served over https * feat: Add app version header and update WebRTC signaling endpoint * fix: Handle error when writing device metadata to WebRTC signaling channel --------- Co-authored-by: Siyuan Miao --- cloud.go | 179 +++++------ log.go | 1 + ui/package-lock.json | 6 + ui/package.json | 1 + ui/src/components/Header.tsx | 6 +- ui/src/components/VideoOverlay.tsx | 58 +++- ui/src/components/WebRTCVideo.tsx | 4 +- ui/src/routes/devices.$id.tsx | 463 ++++++++++++++++++++--------- web.go | 188 ++++++++++-- webrtc.go | 23 +- 10 files changed, 654 insertions(+), 275 deletions(-) diff --git a/cloud.go b/cloud.go index f91085a..7ad8b75 100644 --- a/cloud.go +++ b/cloud.go @@ -35,8 +35,8 @@ const ( // CloudOidcRequestTimeout is the timeout for OIDC token verification requests // should be lower than the websocket response timeout set in cloud-api CloudOidcRequestTimeout = 10 * time.Second - // CloudWebSocketPingInterval is the interval at which the websocket client sends ping messages to the cloud - CloudWebSocketPingInterval = 15 * time.Second + // WebsocketPingInterval is the interval at which the websocket client sends ping messages to the cloud + WebsocketPingInterval = 15 * time.Second ) var ( @@ -52,59 +52,67 @@ var ( Help: "The timestamp when the cloud connection was established", }, ) - metricCloudConnectionLastPingTimestamp = promauto.NewGauge( + metricConnectionLastPingTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_last_ping_timestamp", + Name: "jetkvm_connection_last_ping_timestamp", Help: "The timestamp when the last ping response was received", }, + []string{"type", "source"}, ) - metricCloudConnectionLastPingDuration = promauto.NewGauge( + metricConnectionLastPingDuration = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_last_ping_duration", + Name: "jetkvm_connection_last_ping_duration", Help: "The duration of the last ping response", }, + []string{"type", "source"}, ) - metricCloudConnectionPingDuration = promauto.NewHistogram( + metricConnectionPingDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "jetkvm_cloud_connection_ping_duration", + Name: "jetkvm_connection_ping_duration", Help: "The duration of the ping response", Buckets: []float64{ 0.1, 0.5, 1, 10, }, }, + []string{"type", "source"}, ) - metricCloudConnectionTotalPingCount = promauto.NewCounter( + metricConnectionTotalPingCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_cloud_connection_total_ping_count", - Help: "The total number of pings sent to the cloud", + Name: "jetkvm_connection_total_ping_count", + Help: "The total number of pings sent to the connection", }, + []string{"type", "source"}, ) - metricCloudConnectionSessionRequestCount = promauto.NewCounter( + metricConnectionSessionRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_cloud_connection_session_total_request_count", - Help: "The total number of session requests received from the cloud", + Name: "jetkvm_connection_session_total_request_count", + Help: "The total number of session requests received", }, + []string{"type", "source"}, ) - metricCloudConnectionSessionRequestDuration = promauto.NewHistogram( + metricConnectionSessionRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "jetkvm_cloud_connection_session_request_duration", + Name: "jetkvm_connection_session_request_duration", Help: "The duration of session requests", Buckets: []float64{ 0.1, 0.5, 1, 10, }, }, + []string{"type", "source"}, ) - metricCloudConnectionLastSessionRequestTimestamp = promauto.NewGauge( + metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_last_session_request_timestamp", + Name: "jetkvm_connection_last_session_request_timestamp", Help: "The timestamp of the last session request", }, + []string{"type", "source"}, ) - metricCloudConnectionLastSessionRequestDuration = promauto.NewGauge( + metricConnectionLastSessionRequestDuration = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_last_session_request_duration", + Name: "jetkvm_connection_last_session_request_duration", Help: "The duration of the last session request", }, + []string{"type", "source"}, ) metricCloudConnectionFailureCount = promauto.NewCounter( prometheus.CounterOpts{ @@ -119,12 +127,16 @@ var ( cloudDisconnectLock = &sync.Mutex{} ) -func cloudResetMetrics(established bool) { - metricCloudConnectionLastPingTimestamp.Set(-1) - metricCloudConnectionLastPingDuration.Set(-1) +func wsResetMetrics(established bool, sourceType string, source string) { + metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) - metricCloudConnectionLastSessionRequestTimestamp.Set(-1) - metricCloudConnectionLastSessionRequestDuration.Set(-1) + metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1) + + if sourceType != "cloud" { + return + } if established { metricCloudConnectionEstablishedTimestamp.SetToCurrentTime() @@ -256,6 +268,7 @@ func runWebsocketClient() error { header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) + header.Set("X-App-Version", builtAppVersion) header.Set("Authorization", "Bearer "+config.CloudToken) dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) @@ -270,88 +283,13 @@ func runWebsocketClient() error { cloudLogger.Infof("websocket connected to %s", wsURL) // set the metrics when we successfully connect to the cloud. - cloudResetMetrics(true) + wsResetMetrics(true, "cloud", "") - runCtx, cancelRun := context.WithCancel(context.Background()) - defer cancelRun() - go func() { - for { - time.Sleep(CloudWebSocketPingInterval) - - // set the timer for the ping duration - timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { - metricCloudConnectionLastPingDuration.Set(v) - metricCloudConnectionPingDuration.Observe(v) - })) - - err := c.Ping(runCtx) - - if err != nil { - cloudLogger.Warnf("websocket ping error: %v", err) - cancelRun() - return - } - - // dont use `defer` here because we want to observe the duration of the ping - timer.ObserveDuration() - - metricCloudConnectionTotalPingCount.Inc() - metricCloudConnectionLastPingTimestamp.SetToCurrentTime() - } - }() - - // create a channel to receive the disconnect event, once received, we cancelRun - cloudDisconnectChan = make(chan error) - defer func() { - close(cloudDisconnectChan) - cloudDisconnectChan = nil - }() - go func() { - for err := range cloudDisconnectChan { - if err == nil { - continue - } - cloudLogger.Infof("disconnecting from cloud due to: %v", err) - cancelRun() - } - }() - - for { - typ, msg, err := c.Read(runCtx) - if err != nil { - return err - } - if typ != websocket.MessageText { - // ignore non-text messages - continue - } - var req WebRTCSessionRequest - err = json.Unmarshal(msg, &req) - if err != nil { - cloudLogger.Warnf("unable to parse ws message: %v", string(msg)) - continue - } - - cloudLogger.Infof("new session request: %v", req.OidcGoogle) - cloudLogger.Tracef("session request info: %v", req) - - metricCloudConnectionSessionRequestCount.Inc() - metricCloudConnectionLastSessionRequestTimestamp.SetToCurrentTime() - err = handleSessionRequest(runCtx, c, req) - if err != nil { - cloudLogger.Infof("error starting new session: %v", err) - continue - } - } + // we don't have a source for the cloud connection + return handleWebRTCSignalWsMessages(c, true, "") } -func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { - timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { - metricCloudConnectionLastSessionRequestDuration.Set(v) - metricCloudConnectionSessionRequestDuration.Observe(v) - })) - defer timer.ObserveDuration() - +func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout) defer cancelOIDC() provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") @@ -379,10 +317,35 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess return fmt.Errorf("google identity mismatch") } + return nil +} + +func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest, isCloudConnection bool, source string) error { + var sourceType string + if isCloudConnection { + sourceType = "cloud" + } else { + sourceType = "local" + } + + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(v) + metricConnectionSessionRequestDuration.WithLabelValues(sourceType, source).Observe(v) + })) + defer timer.ObserveDuration() + + // If the message is from the cloud, we need to authenticate the session. + if isCloudConnection { + if err := authenticateSession(ctx, c, req); err != nil { + return err + } + } + session, err := newSession(SessionConfig{ - ICEServers: req.ICEServers, + ws: c, + IsCloud: isCloudConnection, LocalIP: req.IP, - IsCloud: true, + ICEServers: req.ICEServers, }) if err != nil { _ = wsjson.Write(context.Background(), c, gin.H{"error": err}) @@ -406,14 +369,14 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess cloudLogger.Info("new session accepted") cloudLogger.Tracef("new session accepted: %v", session) currentSession = session - _ = wsjson.Write(context.Background(), c, gin.H{"sd": sd}) + _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil } func RunWebsocketClient() { for { // reset the metrics when we start the websocket client. - cloudResetMetrics(false) + wsResetMetrics(false, "cloud", "") // If the cloud token is not set, we don't need to run the websocket client. if config.CloudToken == "" { diff --git a/log.go b/log.go index 7718a28..0d36c0d 100644 --- a/log.go +++ b/log.go @@ -6,3 +6,4 @@ import "github.com/pion/logging" // ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm") var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud") +var websocketLogger = logging.NewDefaultLoggerFactory().NewLogger("websocket") diff --git a/ui/package-lock.json b/ui/package-lock.json index e9caa20..ebce148 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -30,6 +30,7 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.7.112", + "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.9", "recharts": "^2.15.0", "tailwind-merge": "^2.5.5", @@ -5180,6 +5181,11 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-websocket": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==" + }, "node_modules/react-xtermjs": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz", diff --git a/ui/package.json b/ui/package.json index f8f1c7a..a248616 100644 --- a/ui/package.json +++ b/ui/package.json @@ -40,6 +40,7 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.22.3", "react-simple-keyboard": "^3.7.112", + "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.9", "recharts": "^2.15.0", "tailwind-merge": "^2.5.5", diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index 03a907e..19e9652 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -36,7 +36,7 @@ export default function DashboardNavbar({ picture, kvmName, }: NavbarProps) { - const peerConnection = useRTCStore(state => state.peerConnection); + const peerConnectionState = useRTCStore(state => state.peerConnectionState); const setUser = useUserStore(state => state.setUser); const navigate = useNavigate(); const onLogout = useCallback(async () => { @@ -82,14 +82,14 @@ export default function DashboardNavbar({
    diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 0620af4..e34cf10 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -6,7 +6,7 @@ import { LuPlay } from "react-icons/lu"; import { Button, LinkButton } from "@components/Button"; import LoadingSpinner from "@components/LoadingSpinner"; -import { GridCard } from "@components/Card"; +import Card, { GridCard } from "@components/Card"; interface OverlayContentProps { children: React.ReactNode; @@ -94,7 +94,7 @@ interface ConnectionErrorOverlayProps { setupPeerConnection: () => Promise; } -export function ConnectionErrorOverlay({ +export function ConnectionFailedOverlay({ show, setupPeerConnection, }: ConnectionErrorOverlayProps) { @@ -151,6 +151,60 @@ export function ConnectionErrorOverlay({ ); } +interface PeerConnectionDisconnectedOverlay { + show: boolean; +} + +export function PeerConnectionDisconnectedOverlay({ + show, +}: PeerConnectionDisconnectedOverlay) { + return ( + + {show && ( + + +
    + +
    +
    +
    +

    Connection Issue Detected

    +
      +
    • Verify that the device is powered on and properly connected
    • +
    • Check all cable connections for any loose or damaged wires
    • +
    • Ensure your network connection is stable and active
    • +
    • Try restarting both the device and your computer
    • +
    +
    +
    + +
    + +

    + Retrying connection... +

    +
    +
    +
    +
    +
    +
    +
    +
    + )} +
    + ); +} + interface HDMIErrorOverlayProps { show: boolean; hdmiState: string; diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 5d8fb55..99c0191 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -380,7 +380,7 @@ export default function WebRTCVideo() { (mediaStream: MediaStream) => { if (!videoElm.current) return; const videoElmRefValue = videoElm.current; - console.log("Adding stream to video element", videoElmRefValue); + // console.log("Adding stream to video element", videoElmRefValue); videoElmRefValue.srcObject = mediaStream; updateVideoSizeStore(videoElmRefValue); }, @@ -396,7 +396,7 @@ export default function WebRTCVideo() { peerConnection.addEventListener( "track", (e: RTCTrackEvent) => { - console.log("Adding stream to video element"); + // console.log("Adding stream to video element"); addStreamToVideoElm(e.streams[0]); }, { signal }, diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index d2662fc..fef1764 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LoaderFunctionArgs, Outlet, @@ -14,6 +14,7 @@ import { import { useInterval } from "usehooks-ts"; import FocusTrap from "focus-trap-react"; import { motion, AnimatePresence } from "framer-motion"; +import useWebSocket from "react-use-websocket"; import { cx } from "@/cva.config"; import { @@ -43,15 +44,16 @@ import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard import api from "../api"; import Modal from "../components/Modal"; import { useDeviceUiNavigation } from "../hooks/useAppNavigation"; +import { + ConnectionFailedOverlay, + LoadingConnectionOverlay, + PeerConnectionDisconnectedOverlay, +} from "../components/VideoOverlay"; import { FeatureFlagProvider } from "../providers/FeatureFlagProvider"; import notifications from "../notifications"; -import { - ConnectionErrorOverlay, - LoadingConnectionOverlay, -} from "../components/VideoOverlay"; -import { SystemVersionInfo } from "./devices.$id.settings.general.update"; import { DeviceStatus } from "./welcome-local"; +import { SystemVersionInfo } from "./devices.$id.settings.general.update"; interface LocalLoaderResp { authMode: "password" | "noPassword" | null; @@ -117,7 +119,6 @@ const loader = async ({ params }: LoaderFunctionArgs) => { export default function KvmIdRoute() { const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp; - // Depending on the mode, we set the appropriate variables const user = "user" in loaderResp ? loaderResp.user : null; const deviceName = "deviceName" in loaderResp ? loaderResp.deviceName : null; @@ -130,6 +131,8 @@ export default function KvmIdRoute() { const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse); const peerConnection = useRTCStore(state => state.peerConnection); + const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState); + const peerConnectionState = useRTCStore(state => state.peerConnectionState); const setMediaMediaStream = useRTCStore(state => state.setMediaStream); const setPeerConnection = useRTCStore(state => state.setPeerConnection); const setDiskChannel = useRTCStore(state => state.setDiskChannel); @@ -137,23 +140,28 @@ export default function KvmIdRoute() { const setTransceiver = useRTCStore(state => state.setTransceiver); const location = useLocation(); + const isLegacySignalingEnabled = useRef(false); + const [connectionFailed, setConnectionFailed] = useState(false); const navigate = useNavigate(); const { otaState, setOtaState, setModalView } = useUpdateStore(); const [loadingMessage, setLoadingMessage] = useState("Connecting to device..."); - const closePeerConnection = useCallback( - function closePeerConnection() { + const cleanupAndStopReconnecting = useCallback( + function cleanupAndStopReconnecting() { console.log("Closing peer connection"); setConnectionFailed(true); + if (peerConnection) { + setPeerConnectionState(peerConnection.connectionState); + } connectionFailedRef.current = true; peerConnection?.close(); signalingAttempts.current = 0; }, - [peerConnection], + [peerConnection, setPeerConnectionState], ); // We need to track connectionFailed in a ref to avoid stale closure issues @@ -171,95 +179,233 @@ export default function KvmIdRoute() { }, [connectionFailed]); const signalingAttempts = useRef(0); - const syncRemoteSessionDescription = useCallback( - async function syncRemoteSessionDescription(pc: RTCPeerConnection) { + const setRemoteSessionDescription = useCallback( + async function setRemoteSessionDescription( + pc: RTCPeerConnection, + remoteDescription: RTCSessionDescriptionInit, + ) { + setLoadingMessage("Setting remote description"); + try { - if (!pc) return; - - const sd = btoa(JSON.stringify(pc.localDescription)); - - const sessionUrl = isOnDevice - ? `${DEVICE_API}/webrtc/session` - : `${CLOUD_API}/webrtc/session`; - - console.log("Trying to get remote session description"); - setLoadingMessage( - `Getting remote session description... ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`, - ); - const res = await api.POST(sessionUrl, { - sd, - // When on device, we don't need to specify the device id, as it's already known - ...(isOnDevice ? {} : { id: params.id }), - }); - - const json = await res.json(); - if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login"); - if (!res.ok) { - console.error("Error getting SDP", { status: res.status, json }); - throw new Error("Error getting SDP"); - } - - console.log("Successfully got Remote Session Description. Setting."); - setLoadingMessage("Setting remote session description..."); - - const decodedSd = atob(json.sd); - const parsedSd = JSON.parse(decodedSd); - pc.setRemoteDescription(new RTCSessionDescription(parsedSd)); - - await new Promise((resolve, reject) => { - console.log("Waiting for remote description to be set"); - const maxAttempts = 10; - const interval = 1000; - let attempts = 0; - - const checkInterval = setInterval(() => { - attempts++; - // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects - if (pc.sctp?.state === "connected") { - console.log("Remote description set"); - clearInterval(checkInterval); - resolve(true); - } else if (attempts >= maxAttempts) { - console.log( - `Failed to get remote description after ${maxAttempts} attempts`, - ); - closePeerConnection(); - clearInterval(checkInterval); - reject( - new Error( - `Failed to get remote description after ${maxAttempts} attempts`, - ), - ); - } else { - console.log("Waiting for remote description to be set"); - } - }, interval); - }); + await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription)); + console.log("[setRemoteSessionDescription] Remote description set successfully"); + setLoadingMessage("Establishing secure connection..."); } catch (error) { - console.error("Error getting SDP", { error }); - console.log("Connection failed", connectionFailedRef.current); - if (connectionFailedRef.current) return; - if (signalingAttempts.current < 5) { - signalingAttempts.current++; - await new Promise(resolve => setTimeout(resolve, 500)); - console.log("Attempting to get SDP again", signalingAttempts.current); - syncRemoteSessionDescription(pc); - } else { - closePeerConnection(); - } + console.error( + "[setRemoteSessionDescription] Failed to set remote description:", + error, + ); + cleanupAndStopReconnecting(); + return; } + + // Replace the interval-based check with a more reliable approach + let attempts = 0; + const checkInterval = setInterval(() => { + attempts++; + + // When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects + if (pc.sctp?.state === "connected") { + console.log("[setRemoteSessionDescription] Remote description set"); + clearInterval(checkInterval); + setLoadingMessage("Connection established"); + } else if (attempts >= 10) { + console.log( + "[setRemoteSessionDescription] Failed to establish connection after 10 attempts", + { + connectionState: pc.connectionState, + iceConnectionState: pc.iceConnectionState, + }, + ); + cleanupAndStopReconnecting(); + clearInterval(checkInterval); + } else { + console.log("[setRemoteSessionDescription] Waiting for connection, state:", { + connectionState: pc.connectionState, + iceConnectionState: pc.iceConnectionState, + }); + } + }, 1000); }, - [closePeerConnection, navigate, params.id], + [cleanupAndStopReconnecting], + ); + + const ignoreOffer = useRef(false); + const isSettingRemoteAnswerPending = useRef(false); + const makingOffer = useRef(false); + + const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + + const { sendMessage, getWebSocket } = useWebSocket( + isOnDevice + ? `${wsProtocol}//${window.location.host}/webrtc/signaling/client` + : `${CLOUD_API.replace("http", "ws")}/webrtc/signaling/client?id=${params.id}`, + { + heartbeat: true, + retryOnError: true, + reconnectAttempts: 5, + reconnectInterval: 1000, + onReconnectStop: () => { + console.log("Reconnect stopped"); + cleanupAndStopReconnecting(); + }, + + shouldReconnect(event) { + console.log("[Websocket] shouldReconnect", event); + // TODO: Why true? + return true; + }, + + onClose(event) { + console.log("[Websocket] onClose", event); + // We don't want to close everything down, we wait for the reconnect to stop instead + }, + + onError(event) { + console.log("[Websocket] onError", event); + // We don't want to close everything down, we wait for the reconnect to stop instead + }, + onOpen() { + console.log("[Websocket] onOpen"); + }, + + onMessage: message => { + if (message.data === "pong") return; + + /* + Currently the signaling process is as follows: + After open, the other side will send a `device-metadata` message with the device version + If the device version is not set, we can assume the device is using the legacy signaling + Otherwise, we can assume the device is using the new signaling + + If the device is using the legacy signaling, we close the websocket connection + and use the legacy HTTPSignaling function to get the remote session description + + If the device is using the new signaling, we don't need to do anything special, but continue to use the websocket connection + to chat with the other peer about the connection + */ + + const parsedMessage = JSON.parse(message.data); + if (parsedMessage.type === "device-metadata") { + const { deviceVersion } = parsedMessage.data; + console.log("[Websocket] Received device-metadata message"); + console.log("[Websocket] Device version", deviceVersion); + // If the device version is not set, we can assume the device is using the legacy signaling + if (!deviceVersion) { + console.log("[Websocket] Device is using legacy signaling"); + + // Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling + // which does everything over HTTP(at least from the perspective of the client) + isLegacySignalingEnabled.current = true; + getWebSocket()?.close(); + } else { + console.log("[Websocket] Device is using new signaling"); + isLegacySignalingEnabled.current = false; + } + setupPeerConnection(); + } + + if (!peerConnection) return; + if (parsedMessage.type === "answer") { + console.log("[Websocket] Received answer"); + const readyForOffer = + // If we're making an offer, we don't want to accept an answer + !makingOffer && + // If the peer connection is stable or we're setting the remote answer pending, we're ready for an offer + (peerConnection?.signalingState === "stable" || + isSettingRemoteAnswerPending.current); + + // If we're not ready for an offer, we don't want to accept an offer + ignoreOffer.current = parsedMessage.type === "offer" && !readyForOffer; + if (ignoreOffer.current) return; + + // Set so we don't accept an answer while we're setting the remote description + isSettingRemoteAnswerPending.current = parsedMessage.type === "answer"; + console.log( + "[Websocket] Setting remote answer pending", + isSettingRemoteAnswerPending.current, + ); + + const sd = atob(parsedMessage.data); + const remoteSessionDescription = JSON.parse(sd); + + setRemoteSessionDescription( + peerConnection, + new RTCSessionDescription(remoteSessionDescription), + ); + + // Reset the remote answer pending flag + isSettingRemoteAnswerPending.current = false; + } else if (parsedMessage.type === "new-ice-candidate") { + console.log("[Websocket] Received new-ice-candidate"); + const candidate = parsedMessage.data; + peerConnection.addIceCandidate(candidate); + } + }, + }, + + // Don't even retry once we declare failure + !connectionFailed && isLegacySignalingEnabled.current === false, + ); + + const sendWebRTCSignal = useCallback( + (type: string, data: unknown) => { + // Second argument tells the library not to queue the message, and send it once the connection is established again. + // We have event handlers that handle the connection set up, so we don't need to queue the message. + sendMessage(JSON.stringify({ type, data }), false); + }, + [sendMessage], + ); + + const legacyHTTPSignaling = useCallback( + async (pc: RTCPeerConnection) => { + const sd = btoa(JSON.stringify(pc.localDescription)); + + // Legacy mode == UI in cloud with updated code connecting to older device version. + // In device mode, old devices wont server this JS, and on newer devices legacy mode wont be enabled + const sessionUrl = `${CLOUD_API}/webrtc/session`; + + console.log("Trying to get remote session description"); + setLoadingMessage( + `Getting remote session description... ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`, + ); + const res = await api.POST(sessionUrl, { + sd, + // When on device, we don't need to specify the device id, as it's already known + ...(isOnDevice ? {} : { id: params.id }), + }); + + const json = await res.json(); + if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login"); + if (!res.ok) { + console.error("Error getting SDP", { status: res.status, json }); + cleanupAndStopReconnecting(); + return; + } + + console.log("Successfully got Remote Session Description. Setting."); + setLoadingMessage("Setting remote session description..."); + + const decodedSd = atob(json.sd); + const parsedSd = JSON.parse(decodedSd); + setRemoteSessionDescription(pc, new RTCSessionDescription(parsedSd)); + }, + [cleanupAndStopReconnecting, navigate, params.id, setRemoteSessionDescription], ); const setupPeerConnection = useCallback(async () => { - console.log("Setting up peer connection"); + console.log("[setupPeerConnection] Setting up peer connection"); setConnectionFailed(false); setLoadingMessage("Connecting to device..."); + if (peerConnection?.signalingState === "stable") { + console.log("[setupPeerConnection] Peer connection already established"); + return; + } + let pc: RTCPeerConnection; try { - console.log("Creating peer connection"); + console.log("[setupPeerConnection] Creating peer connection"); setLoadingMessage("Creating peer connection..."); pc = new RTCPeerConnection({ // We only use STUN or TURN servers if we're in the cloud @@ -267,30 +413,65 @@ export default function KvmIdRoute() { ? { iceServers: [iceConfig?.iceServers] } : {}), }); - console.log("Peer connection created", pc); - setLoadingMessage("Peer connection created"); + + setPeerConnectionState(pc.connectionState); + console.log("[setupPeerConnection] Peer connection created", pc); + setLoadingMessage("Setting up connection to device..."); } catch (e) { - console.error(`Error creating peer connection: ${e}`); + console.error(`[setupPeerConnection] Error creating peer connection: ${e}`); setTimeout(() => { - closePeerConnection(); + cleanupAndStopReconnecting(); }, 1000); return; } // Set up event listeners and data channels pc.onconnectionstatechange = () => { - console.log("Connection state changed", pc.connectionState); + console.log("[setupPeerConnection] Connection state changed", pc.connectionState); + setPeerConnectionState(pc.connectionState); + }; + + pc.onnegotiationneeded = async () => { + try { + console.log("[setupPeerConnection] Creating offer"); + makingOffer.current = true; + + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + const sd = btoa(JSON.stringify(pc.localDescription)); + const isNewSignalingEnabled = isLegacySignalingEnabled.current === false; + if (isNewSignalingEnabled) { + sendWebRTCSignal("offer", { sd: sd }); + } else { + console.log("Legacy signanling. Waiting for ICE Gathering to complete..."); + } + } catch (e) { + console.error( + `[setupPeerConnection] Error creating offer: ${e}`, + new Date().toISOString(), + ); + cleanupAndStopReconnecting(); + } finally { + makingOffer.current = false; + } + }; + + pc.onicecandidate = async ({ candidate }) => { + if (!candidate) return; + if (candidate.candidate === "") return; + sendWebRTCSignal("new-ice-candidate", candidate); }; pc.onicegatheringstatechange = event => { const pc = event.currentTarget as RTCPeerConnection; - console.log("ICE Gathering State Changed", pc.iceGatheringState); if (pc.iceGatheringState === "complete") { console.log("ICE Gathering completed"); setLoadingMessage("ICE Gathering completed"); - // We can now start the https/ws connection to get the remote session description from the KVM device - syncRemoteSessionDescription(pc); + if (isLegacySignalingEnabled.current) { + // We can now start the https/ws connection to get the remote session description from the KVM device + legacyHTTPSignaling(pc); + } } else if (pc.iceGatheringState === "gathering") { console.log("ICE Gathering Started"); setLoadingMessage("Gathering ICE candidates..."); @@ -314,31 +495,26 @@ export default function KvmIdRoute() { }; setPeerConnection(pc); - - try { - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - } catch (e) { - console.error(`Error creating offer: ${e}`, new Date().toISOString()); - closePeerConnection(); - } }, [ - closePeerConnection, + cleanupAndStopReconnecting, iceConfig?.iceServers, + legacyHTTPSignaling, + peerConnection?.signalingState, + sendWebRTCSignal, setDiskChannel, setMediaMediaStream, setPeerConnection, + setPeerConnectionState, setRpcDataChannel, setTransceiver, - syncRemoteSessionDescription, ]); - // On boot, if the connection state is undefined, we connect to the WebRTC useEffect(() => { - if (peerConnection?.connectionState === undefined) { - setupPeerConnection(); + if (peerConnectionState === "failed") { + console.log("Connection failed, closing peer connection"); + cleanupAndStopReconnecting(); } - }, [setupPeerConnection, peerConnection?.connectionState]); + }, [peerConnectionState, cleanupAndStopReconnecting]); // Cleanup effect const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats); @@ -363,7 +539,7 @@ export default function KvmIdRoute() { // TURN server usage detection useEffect(() => { - if (peerConnection?.connectionState !== "connected") return; + if (peerConnectionState !== "connected") return; const { localCandidateStats, remoteCandidateStats } = useRTCStore.getState(); const lastLocalStat = Array.from(localCandidateStats).pop(); @@ -375,7 +551,7 @@ export default function KvmIdRoute() { const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here setIsTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn); - }, [peerConnection?.connectionState, setIsTurnServerInUse]); + }, [peerConnectionState, setIsTurnServerInUse]); // TURN server usage reporting const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); @@ -466,10 +642,6 @@ export default function KvmIdRoute() { }); }, [rpcDataChannel?.readyState, send, setHdmiState]); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - window.send = send; - // When the update is successful, we need to refresh the client javascript and show a success modal useEffect(() => { if (queryParams.get("updateSuccess")) { @@ -506,12 +678,12 @@ export default function KvmIdRoute() { useEffect(() => { if (!peerConnection) return; if (!kvmTerminal) { - console.log('Creating data channel "terminal"'); + // console.log('Creating data channel "terminal"'); setKvmTerminal(peerConnection.createDataChannel("terminal")); } if (!serialConsole) { - console.log('Creating data channel "serial"'); + // console.log('Creating data channel "serial"'); setSerialConsole(peerConnection.createDataChannel("serial")); } }, [kvmTerminal, peerConnection, serialConsole]); @@ -554,6 +726,43 @@ export default function KvmIdRoute() { [send, setScrollSensitivity], ); + const ConnectionStatusElement = useMemo(() => { + const hasConnectionFailed = + connectionFailed || ["failed", "closed"].includes(peerConnectionState || ""); + + const isPeerConnectionLoading = + ["connecting", "new"].includes(peerConnectionState || "") || + peerConnection === null; + + const isDisconnected = peerConnectionState === "disconnected"; + + const isOtherSession = location.pathname.includes("other-session"); + + if (isOtherSession) return null; + if (peerConnectionState === "connected") return null; + if (isDisconnected) { + return ; + } + + if (hasConnectionFailed) + return ( + + ); + + if (isPeerConnectionLoading) { + return ; + } + + return null; + }, [ + connectionFailed, + loadingMessage, + location.pathname, + peerConnection, + peerConnectionState, + setupPeerConnection, + ]); + return ( {!outlet && otaState.updating && ( @@ -593,27 +802,13 @@ export default function KvmIdRoute() { />
    -
    +
    - - + {!!ConnectionStatusElement && ConnectionStatusElement}
    - + {peerConnectionState === "connected" && }
    diff --git a/web.go b/web.go index 9201e7b..c3f6d8d 100644 --- a/web.go +++ b/web.go @@ -1,6 +1,7 @@ package kvm import ( + "context" "embed" "encoding/json" "fmt" @@ -10,8 +11,12 @@ import ( "strings" "time" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/pion/webrtc/v4" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/crypto/bcrypt" ) @@ -94,7 +99,7 @@ func setupRouter() *gin.Engine { protected := r.Group("/") protected.Use(protectedMiddleware()) { - protected.POST("/webrtc/session", handleWebRTCSession) + protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal) protected.POST("/cloud/register", handleCloudRegister) protected.GET("/cloud/state", handleCloudState) protected.GET("/device", handleDevice) @@ -121,35 +126,182 @@ func setupRouter() *gin.Engine { // TODO: support multiple sessions? var currentSession *Session -func handleWebRTCSession(c *gin.Context) { - var req WebRTCSessionRequest - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return +func handleLocalWebRTCSignal(c *gin.Context) { + cloudLogger.Infof("new websocket connection established") + // Create WebSocket options with InsecureSkipVerify to bypass origin check + wsOptions := &websocket.AcceptOptions{ + InsecureSkipVerify: true, // Allow connections from any origin } - session, err := newSession(SessionConfig{}) + wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - sd, err := session.ExchangeOffer(req.Sd) + // get the source from the request + source := c.ClientIP() + + // Now use conn for websocket operations + defer wsCon.Close(websocket.StatusNormalClosure, "") + + err = wsjson.Write(context.Background(), wsCon, gin.H{"type": "device-metadata", "data": gin.H{"deviceVersion": builtAppVersion}}) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - if currentSession != nil { - writeJSONRPCEvent("otherSessionConnected", nil, currentSession) - peerConn := currentSession.peerConnection + + err = handleWebRTCSignalWsMessages(wsCon, false, source) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } +} + +func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, source string) error { + runCtx, cancelRun := context.WithCancel(context.Background()) + defer cancelRun() + + // Add connection tracking to detect reconnections + connectionID := uuid.New().String() + cloudLogger.Infof("new websocket connection established with ID: %s", connectionID) + + // connection type + var sourceType string + if isCloudConnection { + sourceType = "cloud" + } else { + sourceType = "local" + } + + // probably we can use a better logging framework here + logInfof := func(format string, args ...interface{}) { + args = append(args, source, sourceType) + websocketLogger.Infof(format+", source: %s, sourceType: %s", args...) + } + logWarnf := func(format string, args ...interface{}) { + args = append(args, source, sourceType) + websocketLogger.Warnf(format+", source: %s, sourceType: %s", args...) + } + logTracef := func(format string, args ...interface{}) { + args = append(args, source, sourceType) + websocketLogger.Tracef(format+", source: %s, sourceType: %s", args...) + } + + go func() { + for { + time.Sleep(WebsocketPingInterval) + + // set the timer for the ping duration + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v) + metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v) + })) + + logInfof("pinging websocket") + err := wsCon.Ping(runCtx) + + if err != nil { + logWarnf("websocket ping error: %v", err) + cancelRun() + return + } + + // dont use `defer` here because we want to observe the duration of the ping + timer.ObserveDuration() + + metricConnectionTotalPingCount.WithLabelValues(sourceType, source).Inc() + metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() + } + }() + + if isCloudConnection { + // create a channel to receive the disconnect event, once received, we cancelRun + cloudDisconnectChan = make(chan error) + defer func() { + close(cloudDisconnectChan) + cloudDisconnectChan = nil + }() go func() { - time.Sleep(1 * time.Second) - _ = peerConn.Close() + for err := range cloudDisconnectChan { + if err == nil { + continue + } + cloudLogger.Infof("disconnecting from cloud due to: %v", err) + cancelRun() + } }() } - currentSession = session - c.JSON(http.StatusOK, gin.H{"sd": sd}) + + for { + typ, msg, err := wsCon.Read(runCtx) + if err != nil { + logWarnf("websocket read error: %v", err) + return err + } + if typ != websocket.MessageText { + // ignore non-text messages + continue + } + + var message struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` + } + + err = json.Unmarshal(msg, &message) + if err != nil { + logWarnf("unable to parse ws message: %v", err) + continue + } + + if message.Type == "offer" { + logInfof("new session request received") + var req WebRTCSessionRequest + err = json.Unmarshal(message.Data, &req) + if err != nil { + logWarnf("unable to parse session request data: %v", err) + continue + } + + logInfof("new session request: %v", req.OidcGoogle) + logTracef("session request info: %v", req) + + metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc() + metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() + err = handleSessionRequest(runCtx, wsCon, req, isCloudConnection, source) + if err != nil { + logWarnf("error starting new session: %v", err) + continue + } + } else if message.Type == "new-ice-candidate" { + logInfof("The client sent us a new ICE candidate: %v", string(message.Data)) + var candidate webrtc.ICECandidateInit + + // Attempt to unmarshal as a ICECandidateInit + if err := json.Unmarshal(message.Data, &candidate); err != nil { + logWarnf("unable to parse incoming ICE candidate data: %v", string(message.Data)) + continue + } + + if candidate.Candidate == "" { + logWarnf("empty incoming ICE candidate, skipping") + continue + } + + logInfof("unmarshalled incoming ICE candidate: %v", candidate) + + if currentSession == nil { + logInfof("no current session, skipping incoming ICE candidate") + continue + } + + logInfof("adding incoming ICE candidate to current session: %v", candidate) + if err = currentSession.peerConnection.AddICECandidate(candidate); err != nil { + logWarnf("failed to add incoming ICE candidate to our peer connection: %v", err) + } + } + } } func handleLogin(c *gin.Context) { diff --git a/webrtc.go b/webrtc.go index 12d4f95..a047ecc 100644 --- a/webrtc.go +++ b/webrtc.go @@ -1,11 +1,15 @@ package kvm import ( + "context" "encoding/base64" "encoding/json" "net" "strings" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" + "github.com/gin-gonic/gin" "github.com/pion/webrtc/v4" ) @@ -23,6 +27,7 @@ type SessionConfig struct { ICEServers []string LocalIP string IsCloud bool + ws *websocket.Conn } func (s *Session) ExchangeOffer(offerStr string) (string, error) { @@ -46,19 +51,11 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { return "", err } - // Create channel that is blocked until ICE Gathering is complete - gatherComplete := webrtc.GatheringCompletePromise(s.peerConnection) - // Sets the LocalDescription, and starts our UDP listeners if err = s.peerConnection.SetLocalDescription(answer); err != nil { return "", err } - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - <-gatherComplete - localDescription, err := json.Marshal(s.peerConnection.LocalDescription()) if err != nil { return "", err @@ -144,6 +141,16 @@ func newSession(config SessionConfig) (*Session, error) { }() var isConnected bool + peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { + logger.Infof("Our WebRTC peerConnection has a new ICE candidate: %v", candidate) + if candidate != nil { + err := wsjson.Write(context.Background(), config.ws, gin.H{"type": "new-ice-candidate", "data": candidate.ToJSON()}) + if err != nil { + logger.Errorf("failed to write new-ice-candidate to WebRTC signaling channel: %v", err) + } + } + }) + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { logger.Infof("Connection State has changed %s", connectionState) if connectionState == webrtc.ICEConnectionStateConnected { From 652e845d8317da0371c0efbfccd450118497946b Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Wed, 9 Apr 2025 20:25:26 +0200 Subject: [PATCH 011/165] fix(ota): certificate signed by unknown authority --- ota.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ota.go b/ota.go index 9c583b6..b28abbb 100644 --- a/ota.go +++ b/ota.go @@ -126,12 +126,10 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error creating request: %w", err) } + // TODO: set a separate timeout for the download but keep the TLS handshake short + // use Transport here will cause CA certificate validation failure so we temporarily removed it client := http.Client{ - // allow a longer timeout for the download but keep the TLS handshake short Timeout: 10 * time.Minute, - Transport: &http.Transport{ - TLSHandshakeTimeout: 1 * time.Minute, - }, } resp, err := client.Do(req) From 84b35d5deb68bbf85e7bd51ff2a756ea7c040a60 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 9 Apr 2025 22:23:05 +0200 Subject: [PATCH 012/165] re-add old signaling for when upgrading --- web.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/web.go b/web.go index c3f6d8d..3ab7780 100644 --- a/web.go +++ b/web.go @@ -99,6 +99,23 @@ func setupRouter() *gin.Engine { protected := r.Group("/") protected.Use(protectedMiddleware()) { + + /* + * Legacy WebRTC session endpoint + * + * This endpoint is maintained for backward compatibility when users upgrade from a version + * using the legacy HTTP-based signaling method to the new WebSocket-based signaling method. + * + * During the upgrade process, when the "Rebooting device after update..." message appears, + * the browser still runs the previous JavaScript code which polls this endpoint to establish + * a new WebRTC session. Once the session is established, the page will automatically reload + * with the updated code. + * + * Without this endpoint, the stale JavaScript would fail to establish a connection, + * causing users to see the "Rebooting device after update..." message indefinitely + * until they manually refresh the page, leading to a confusing user experience. + */ + protected.POST("/webrtc/session", handleWebRTCSession) protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal) protected.POST("/cloud/register", handleCloudRegister) protected.GET("/cloud/state", handleCloudState) @@ -126,6 +143,37 @@ func setupRouter() *gin.Engine { // TODO: support multiple sessions? var currentSession *Session +func handleWebRTCSession(c *gin.Context) { + var req WebRTCSessionRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + session, err := newSession(SessionConfig{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + + sd, err := session.ExchangeOffer(req.Sd) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + if currentSession != nil { + writeJSONRPCEvent("otherSessionConnected", nil, currentSession) + peerConn := currentSession.peerConnection + go func() { + time.Sleep(1 * time.Second) + _ = peerConn.Close() + }() + } + currentSession = session + c.JSON(http.StatusOK, gin.H{"sd": sd}) +} + func handleLocalWebRTCSignal(c *gin.Context) { cloudLogger.Infof("new websocket connection established") // Create WebSocket options with InsecureSkipVerify to bypass origin check From 98af8050898cab48ea0f610e9d4f4d0339879026 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 9 Apr 2025 22:25:47 +0200 Subject: [PATCH 013/165] refactor: remove unnecessary whitespace in setupRouter function --- web.go | 1 - 1 file changed, 1 deletion(-) diff --git a/web.go b/web.go index 3ab7780..51bcd98 100644 --- a/web.go +++ b/web.go @@ -99,7 +99,6 @@ func setupRouter() *gin.Engine { protected := r.Group("/") protected.Use(protectedMiddleware()) { - /* * Legacy WebRTC session endpoint * From 960ef230ba2cb9122559e86d2c2422cb9e61e083 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 9 Apr 2025 23:26:02 +0200 Subject: [PATCH 014/165] Don't block new PC if connection is stable. No need to (#340) --- ui/src/routes/devices.$id.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index fef1764..1b66bf5 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -243,7 +243,7 @@ export default function KvmIdRoute() { { heartbeat: true, retryOnError: true, - reconnectAttempts: 5, + reconnectAttempts: 15, reconnectInterval: 1000, onReconnectStop: () => { console.log("Reconnect stopped"); @@ -398,11 +398,6 @@ export default function KvmIdRoute() { setConnectionFailed(false); setLoadingMessage("Connecting to device..."); - if (peerConnection?.signalingState === "stable") { - console.log("[setupPeerConnection] Peer connection already established"); - return; - } - let pc: RTCPeerConnection; try { console.log("[setupPeerConnection] Creating peer connection"); @@ -499,7 +494,6 @@ export default function KvmIdRoute() { cleanupAndStopReconnecting, iceConfig?.iceServers, legacyHTTPSignaling, - peerConnection?.signalingState, sendWebRTCSignal, setDiskChannel, setMediaMediaStream, From 1505ca1bc169fc3c5cdc2bf1001c31aa076908d4 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 10 Apr 2025 00:05:41 +0200 Subject: [PATCH 015/165] fix(dev_device): update JETKVM_PROXY_URL to use WebSocket protocol (#342) --- ui/dev_device.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/dev_device.sh b/ui/dev_device.sh index 092b8c8..6b3a649 100755 --- a/ui/dev_device.sh +++ b/ui/dev_device.sh @@ -16,4 +16,4 @@ echo "└─────────────────────── # Set the environment variable and run Vite echo "Starting development server with JetKVM device at: $ip_address" sleep 1 -JETKVM_PROXY_URL="http://$ip_address" npx vite dev --mode=device +JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device From b94de385102d9fc778884c9130675e2b41d0f1ba Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 10 Apr 2025 00:05:51 +0200 Subject: [PATCH 016/165] fix(ui): increase z-index for Modal component to improve layering (#341) --- ui/src/components/Modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/components/Modal.tsx b/ui/src/components/Modal.tsx index 039b493..e395445 100644 --- a/ui/src/components/Modal.tsx +++ b/ui/src/components/Modal.tsx @@ -15,12 +15,12 @@ const Modal = React.memo(function Modal({ onClose: () => void; }) { return ( - + -
    +
    {/* TODO: This doesn't work well with other-sessions */}
    Date: Wed, 9 Apr 2025 23:30:33 +0100 Subject: [PATCH 017/165] fix: Shell linting (#328) Cleanup various shell linting issues * Use `/usr/bin/env` consistently for better platform compatibility. * SC2317 (info): Command appears to be unreachable. * SC2002 (style): Useless cat. Signed-off-by: SuperQ --- dev_deploy.sh | 9 +++++---- publish_source.sh | 4 ++-- ui/dev_device.sh | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dev_deploy.sh b/dev_deploy.sh index 7fbf29e..3f2ebb6 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash +# # Exit immediately if a command exits with a non-zero status set -e @@ -16,7 +18,6 @@ show_help() { echo "Example:" echo " $0 -r 192.168.0.17" echo " $0 -r 192.168.0.17 -u admin" - exit 0 } # Default values @@ -70,10 +71,10 @@ cd bin ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" # Copy the binary to the remote host -cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug" +ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app # Deploy and run the application on the remote host -ssh "${REMOTE_USER}@${REMOTE_HOST}" ash < Date: Thu, 10 Apr 2025 11:55:28 +0200 Subject: [PATCH 018/165] fix(ui): update WebRTCVideo component to properly animate on peer connection state (#343) --- ui/src/components/WebRTCVideo.tsx | 6 +++++- ui/src/routes/devices.$id.tsx | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 99c0191..970867a 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -28,6 +28,7 @@ export default function WebRTCVideo() { const videoElm = useRef(null); const mediaStream = useRTCStore(state => state.mediaStream); const [isPlaying, setIsPlaying] = useState(false); + const peerConnectionState = useRTCStore(state => state.peerConnectionState); // Store hooks const settings = useSettingsStore(); @@ -601,7 +602,10 @@ export default function WebRTCVideo() { "cursor-none": settings.mouseMode === "absolute" && settings.isCursorHidden, - "opacity-0": isVideoLoading || hdmiError, + "opacity-0": + isVideoLoading || + hdmiError || + peerConnectionState !== "connected", "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20": isPlaying, }, diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 1b66bf5..70132f4 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -795,14 +795,16 @@ export default function KvmIdRoute() { kvmName={deviceName || "JetKVM Device"} /> -
    -
    -
    +
    + +
    +
    {!!ConnectionStatusElement && ConnectionStatusElement}
    - - {peerConnectionState === "connected" && }
    From 9c758b6d5798e4dc99762082b1c0bf3cf97c5a91 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 10 Apr 2025 12:39:36 +0200 Subject: [PATCH 019/165] fix(ui): adjust layout and z-index for improved UI consistency in KvmIdRoute (#345) --- ui/src/routes/devices.$id.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 70132f4..82bb542 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -785,6 +785,7 @@ export default function KvmIdRoute() {
    +
    -
    +
    e.stopPropagation()} onKeyDown={e => { e.stopPropagation(); From 66a3352e5dbfe5a0be3849d3b70d7b5cdfeb12ee Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:10:22 +0200 Subject: [PATCH 020/165] feat(websocket): handle ping messages sent from react and add logging (#346) --- cloud.go | 4 ++++ go.mod | 2 +- go.sum | 2 ++ web.go | 53 +++++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/cloud.go b/cloud.go index 7ad8b75..89666a1 100644 --- a/cloud.go +++ b/cloud.go @@ -275,6 +275,10 @@ func runWebsocketClient() error { defer cancelDial() c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, + OnPingReceived: func(ctx context.Context, payload []byte) bool { + websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: cloud", payload, wsURL.Host) + return true + }, }) if err != nil { return err diff --git a/go.mod b/go.mod index 93fedab..fae1cbd 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.1 require ( github.com/Masterminds/semver/v3 v3.3.0 github.com/beevik/ntp v1.3.1 - github.com/coder/websocket v1.8.12 + github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 github.com/gin-gonic/gin v1.9.1 diff --git a/go.sum b/go.sum index b5769d8..1563130 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= diff --git a/web.go b/web.go index 51bcd98..0258dc6 100644 --- a/web.go +++ b/web.go @@ -1,6 +1,7 @@ package kvm import ( + "bytes" "context" "embed" "encoding/json" @@ -173,11 +174,24 @@ func handleWebRTCSession(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"sd": sd}) } +var ( + pingMessage = []byte("ping") + pongMessage = []byte("pong") +) + func handleLocalWebRTCSignal(c *gin.Context) { cloudLogger.Infof("new websocket connection established") + + // get the source from the request + source := c.ClientIP() + // Create WebSocket options with InsecureSkipVerify to bypass origin check wsOptions := &websocket.AcceptOptions{ InsecureSkipVerify: true, // Allow connections from any origin + OnPingReceived: func(ctx context.Context, payload []byte) bool { + websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: local", payload, source) + return true + }, } wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions) @@ -186,9 +200,6 @@ func handleLocalWebRTCSignal(c *gin.Context) { return } - // get the source from the request - source := c.ClientIP() - // Now use conn for websocket operations defer wsCon.Close(websocket.StatusNormalClosure, "") @@ -211,7 +222,6 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, // Add connection tracking to detect reconnections connectionID := uuid.New().String() - cloudLogger.Infof("new websocket connection established with ID: %s", connectionID) // connection type var sourceType string @@ -223,18 +233,20 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, // probably we can use a better logging framework here logInfof := func(format string, args ...interface{}) { - args = append(args, source, sourceType) - websocketLogger.Infof(format+", source: %s, sourceType: %s", args...) + args = append(args, source, sourceType, connectionID) + websocketLogger.Infof(format+", source: %s, sourceType: %s, id: %s", args...) } logWarnf := func(format string, args ...interface{}) { - args = append(args, source, sourceType) - websocketLogger.Warnf(format+", source: %s, sourceType: %s", args...) + args = append(args, source, sourceType, connectionID) + websocketLogger.Warnf(format+", source: %s, sourceType: %s, id: %s", args...) } logTracef := func(format string, args ...interface{}) { - args = append(args, source, sourceType) - websocketLogger.Tracef(format+", source: %s, sourceType: %s", args...) + args = append(args, source, sourceType, connectionID) + websocketLogger.Tracef(format+", source: %s, sourceType: %s, id: %s", args...) } + logInfof("new websocket connection established") + go func() { for { time.Sleep(WebsocketPingInterval) @@ -245,7 +257,7 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v) })) - logInfof("pinging websocket") + logTracef("sending ping frame") err := wsCon.Ping(runCtx) if err != nil { @@ -255,10 +267,12 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, } // dont use `defer` here because we want to observe the duration of the ping - timer.ObserveDuration() + duration := timer.ObserveDuration() metricConnectionTotalPingCount.WithLabelValues(sourceType, source).Inc() metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() + + logTracef("received pong frame, duration: %v", duration) } }() @@ -296,6 +310,16 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, Data json.RawMessage `json:"data"` } + if bytes.Equal(msg, pingMessage) { + logInfof("ping message received: %s", string(msg)) + err = wsCon.Write(context.Background(), websocket.MessageText, pongMessage) + if err != nil { + logWarnf("unable to write pong message: %v", err) + return err + } + continue + } + err = json.Unmarshal(msg, &message) if err != nil { logWarnf("unable to parse ws message: %v", err) @@ -311,8 +335,9 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, continue } - logInfof("new session request: %v", req.OidcGoogle) - logTracef("session request info: %v", req) + if req.OidcGoogle != "" { + logInfof("new session request with OIDC Google: %v", req.OidcGoogle) + } metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc() metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() From dc1ce0369763869550c9ea2d97b88b5c0417d06a Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:53:26 +0200 Subject: [PATCH 021/165] chore(websocket): logging and metrics improvement (#347) * chore(websocket): only show warning if websocket is closed abnormally * chore(websocket): add counter for ping requests received --- cloud.go | 39 +++++++++++++++++++++++++++++++-------- web.go | 20 +++++++++++++++++++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/cloud.go b/cloud.go index 89666a1..070db8d 100644 --- a/cloud.go +++ b/cloud.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -59,6 +60,13 @@ var ( }, []string{"type", "source"}, ) + metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_received_timestamp", + Help: "The timestamp when the last ping request was received", + }, + []string{"type", "source"}, + ) metricConnectionLastPingDuration = promauto.NewGaugeVec( prometheus.GaugeOpts{ Name: "jetkvm_connection_last_ping_duration", @@ -76,16 +84,23 @@ var ( }, []string{"type", "source"}, ) - metricConnectionTotalPingCount = promauto.NewCounterVec( + metricConnectionTotalPingSentCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_total_ping_count", + Name: "jetkvm_connection_total_ping_sent", Help: "The total number of pings sent to the connection", }, []string{"type", "source"}, ) + metricConnectionTotalPingReceivedCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_total_ping_received", + Help: "The total number of pings received from the connection", + }, + []string{"type", "source"}, + ) metricConnectionSessionRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_session_total_request_count", + Name: "jetkvm_connection_session_total_requests", Help: "The total number of session requests received", }, []string{"type", "source"}, @@ -131,6 +146,8 @@ func wsResetMetrics(established bool, sourceType string, source string) { metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1) metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1) @@ -277,20 +294,29 @@ func runWebsocketClient() error { HTTPHeader: header, OnPingReceived: func(ctx context.Context, payload []byte) bool { websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: cloud", payload, wsURL.Host) + + metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() + metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() + return true }, }) + // if the context is canceled, we don't want to return an error if err != nil { + if errors.Is(err, context.Canceled) { + cloudLogger.Infof("websocket connection canceled") + return nil + } return err } defer c.CloseNow() //nolint:errcheck cloudLogger.Infof("websocket connected to %s", wsURL) // set the metrics when we successfully connect to the cloud. - wsResetMetrics(true, "cloud", "") + wsResetMetrics(true, "cloud", wsURL.Host) // we don't have a source for the cloud connection - return handleWebRTCSignalWsMessages(c, true, "") + return handleWebRTCSignalWsMessages(c, true, wsURL.Host) } func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { @@ -379,9 +405,6 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess func RunWebsocketClient() { for { - // reset the metrics when we start the websocket client. - wsResetMetrics(false, "cloud", "") - // If the cloud token is not set, we don't need to run the websocket client. if config.CloudToken == "" { time.Sleep(5 * time.Second) diff --git a/web.go b/web.go index 0258dc6..6c35073 100644 --- a/web.go +++ b/web.go @@ -5,6 +5,7 @@ import ( "context" "embed" "encoding/json" + "errors" "fmt" "io/fs" "net/http" @@ -190,6 +191,10 @@ func handleLocalWebRTCSignal(c *gin.Context) { InsecureSkipVerify: true, // Allow connections from any origin OnPingReceived: func(ctx context.Context, payload []byte) bool { websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: local", payload, source) + + metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc() + metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime() + return true }, } @@ -251,6 +256,15 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, for { time.Sleep(WebsocketPingInterval) + if ctxErr := runCtx.Err(); ctxErr != nil { + if !errors.Is(ctxErr, context.Canceled) { + logWarnf("websocket connection closed: %v", ctxErr) + } else { + logTracef("websocket connection closed as the context was canceled: %v") + } + return + } + // set the timer for the ping duration timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v) @@ -269,7 +283,7 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, // dont use `defer` here because we want to observe the duration of the ping duration := timer.ObserveDuration() - metricConnectionTotalPingCount.WithLabelValues(sourceType, source).Inc() + metricConnectionTotalPingSentCount.WithLabelValues(sourceType, source).Inc() metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() logTracef("received pong frame, duration: %v", duration) @@ -317,6 +331,10 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, logWarnf("unable to write pong message: %v", err) return err } + + metricConnectionTotalPingReceivedCount.WithLabelValues(sourceType, source).Inc() + metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() + continue } From 76efa5608300ade85d7bb961951ea16d1ec09cbe Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Thu, 10 Apr 2025 16:09:37 +0200 Subject: [PATCH 022/165] chore(dev_deploy): update logging for websocket in deployment script (#348) --- dev_deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_deploy.sh b/dev_deploy.sh index 3f2ebb6..02bbb24 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -91,7 +91,7 @@ cd "${REMOTE_PATH}" chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=jetkvm,cloud ./jetkvm_app_debug +PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug EOF echo "Deployment complete." From 8f6e64fd9ccaa83f92bfbe73ac783d9c70c401d4 Mon Sep 17 00:00:00 2001 From: Andrew Davis <1709934+Savid@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:51:06 +1000 Subject: [PATCH 023/165] Add keyboard macros (#305) * add jsonrpc keyboard macro get/set * add ui keyboard macros settings and macro bar * use notifications component and handle jsonrpc errors * cleanup settings menu * return error rather than truncate steps in validation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(ui): add className prop to Checkbox component to allow custom styling * use existing components and CTA * extract display key mappings * create generic combobox component * remove macro description * cleanup styles and macro list * create sortable list component * split up macro routes * remove sortable list and simplify * cleanup macrobar * use and add info to fieldlabel * add useCallback optimizations * add confirm dialog component * cleanup delete buttons * revert info on field label * cleanup combobox focus * cleanup icons * set default label for delay --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- config.go | 60 ++++ jsonrpc.go | 95 ++++++ ui/src/components/Checkbox.tsx | 4 +- ui/src/components/Combobox.tsx | 119 +++++++ ui/src/components/ConfirmDialog.tsx | 106 ++++++ ui/src/components/FieldLabel.tsx | 2 +- ui/src/components/MacroBar.tsx | 48 +++ ui/src/components/MacroForm.tsx | 271 ++++++++++++++++ ui/src/components/MacroStepCard.tsx | 235 ++++++++++++++ ui/src/components/VirtualKeyboard.tsx | 133 +------- ui/src/components/WebRTCVideo.tsx | 24 +- ui/src/constants/macros.ts | 5 + ui/src/hooks/stores.ts | 156 +++++++++ ui/src/hooks/useKeyboard.ts | 26 +- ui/src/keyboardMappings.ts | 77 +++++ ui/src/main.tsx | 37 +++ .../devices.$id.settings.macros.add.tsx | 63 ++++ .../devices.$id.settings.macros.edit.tsx | 134 ++++++++ ui/src/routes/devices.$id.settings.macros.tsx | 306 ++++++++++++++++++ ui/src/routes/devices.$id.settings.tsx | 12 + 20 files changed, 1768 insertions(+), 145 deletions(-) create mode 100644 ui/src/components/Combobox.tsx create mode 100644 ui/src/components/ConfirmDialog.tsx create mode 100644 ui/src/components/MacroBar.tsx create mode 100644 ui/src/components/MacroForm.tsx create mode 100644 ui/src/components/MacroStepCard.tsx create mode 100644 ui/src/constants/macros.ts create mode 100644 ui/src/routes/devices.$id.settings.macros.add.tsx create mode 100644 ui/src/routes/devices.$id.settings.macros.edit.tsx create mode 100644 ui/src/routes/devices.$id.settings.macros.tsx diff --git a/config.go b/config.go index 642f113..1c1b98d 100644 --- a/config.go +++ b/config.go @@ -14,6 +14,64 @@ type WakeOnLanDevice struct { MacAddress string `json:"macAddress"` } +// Constants for keyboard macro limits +const ( + MaxMacrosPerDevice = 25 + MaxStepsPerMacro = 10 + MaxKeysPerStep = 10 + MinStepDelay = 50 + MaxStepDelay = 2000 +) + +type KeyboardMacroStep struct { + Keys []string `json:"keys"` + Modifiers []string `json:"modifiers"` + Delay int `json:"delay"` +} + +func (s *KeyboardMacroStep) Validate() error { + if len(s.Keys) > MaxKeysPerStep { + return fmt.Errorf("too many keys in step (max %d)", MaxKeysPerStep) + } + + if s.Delay < MinStepDelay { + s.Delay = MinStepDelay + } else if s.Delay > MaxStepDelay { + s.Delay = MaxStepDelay + } + + return nil +} + +type KeyboardMacro struct { + ID string `json:"id"` + Name string `json:"name"` + Steps []KeyboardMacroStep `json:"steps"` + SortOrder int `json:"sortOrder,omitempty"` +} + +func (m *KeyboardMacro) Validate() error { + if m.Name == "" { + return fmt.Errorf("macro name cannot be empty") + } + + if len(m.Steps) == 0 { + return fmt.Errorf("macro must have at least one step") + } + + if len(m.Steps) > MaxStepsPerMacro { + return fmt.Errorf("too many steps in macro (max %d)", MaxStepsPerMacro) + } + + for i := range m.Steps { + if err := m.Steps[i].Validate(); err != nil { + return fmt.Errorf("invalid step %d: %w", i+1, err) + } + } + + return nil +} + type Config struct { CloudURL string `json:"cloud_url"` CloudAppURL string `json:"cloud_app_url"` @@ -26,6 +84,7 @@ type Config struct { LocalAuthToken string `json:"local_auth_token"` LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` EdidString string `json:"hdmi_edid_string"` ActiveExtension string `json:"active_extension"` DisplayMaxBrightness int `json:"display_max_brightness"` @@ -43,6 +102,7 @@ var defaultConfig = &Config{ CloudAppURL: "https://app.jetkvm.com", AutoUpdateEnabled: true, // Set a default value ActiveExtension: "", + KeyboardMacros: []KeyboardMacro{}, DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes diff --git a/jsonrpc.go b/jsonrpc.go index 9ce1f1b..e5deb49 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -797,6 +797,99 @@ func rpcSetScrollSensitivity(sensitivity string) error { return nil } +func getKeyboardMacros() (interface{}, error) { + macros := make([]KeyboardMacro, len(config.KeyboardMacros)) + copy(macros, config.KeyboardMacros) + + return macros, nil +} + +type KeyboardMacrosParams struct { + Macros []interface{} `json:"macros"` +} + +func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { + if params.Macros == nil { + return nil, fmt.Errorf("missing or invalid macros parameter") + } + + newMacros := make([]KeyboardMacro, 0, len(params.Macros)) + + for i, item := range params.Macros { + macroMap, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid macro at index %d", i) + } + + id, _ := macroMap["id"].(string) + if id == "" { + id = fmt.Sprintf("macro-%d", time.Now().UnixNano()) + } + + name, _ := macroMap["name"].(string) + + sortOrder := i + 1 + if sortOrderFloat, ok := macroMap["sortOrder"].(float64); ok { + sortOrder = int(sortOrderFloat) + } + + steps := []KeyboardMacroStep{} + if stepsArray, ok := macroMap["steps"].([]interface{}); ok { + for _, stepItem := range stepsArray { + stepMap, ok := stepItem.(map[string]interface{}) + if !ok { + continue + } + + step := KeyboardMacroStep{} + + if keysArray, ok := stepMap["keys"].([]interface{}); ok { + for _, k := range keysArray { + if keyStr, ok := k.(string); ok { + step.Keys = append(step.Keys, keyStr) + } + } + } + + if modsArray, ok := stepMap["modifiers"].([]interface{}); ok { + for _, m := range modsArray { + if modStr, ok := m.(string); ok { + step.Modifiers = append(step.Modifiers, modStr) + } + } + } + + if delay, ok := stepMap["delay"].(float64); ok { + step.Delay = int(delay) + } + + steps = append(steps, step) + } + } + + macro := KeyboardMacro{ + ID: id, + Name: name, + Steps: steps, + SortOrder: sortOrder, + } + + if err := macro.Validate(); err != nil { + return nil, fmt.Errorf("invalid macro at index %d: %w", i, err) + } + + newMacros = append(newMacros, macro) + } + + config.KeyboardMacros = newMacros + + if err := SaveConfig(); err != nil { + return nil, err + } + + return nil, nil +} + var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, @@ -862,4 +955,6 @@ var rpcHandlers = map[string]RPCHandler{ "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, } diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/Checkbox.tsx index 261a425..e3237b1 100644 --- a/ui/src/components/Checkbox.tsx +++ b/ui/src/components/Checkbox.tsx @@ -37,11 +37,11 @@ type CheckBoxProps = { } & Omit; const Checkbox = forwardRef(function Checkbox( - { size = "MD", ...props }, + { size = "MD", className, ...props }, ref, ) { const classes = checkboxVariants({ size }); - return ; + return ; }); Checkbox.displayName = "Checkbox"; diff --git a/ui/src/components/Combobox.tsx b/ui/src/components/Combobox.tsx new file mode 100644 index 0000000..8055043 --- /dev/null +++ b/ui/src/components/Combobox.tsx @@ -0,0 +1,119 @@ +import { useRef } from "react"; +import clsx from "clsx"; +import { Combobox as HeadlessCombobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react"; +import { cva } from "@/cva.config"; +import Card from "./Card"; + +export interface ComboboxOption { + value: string; + label: string; +} + +const sizes = { + XS: "h-[24.5px] pl-3 pr-8 text-xs", + SM: "h-[32px] pl-3 pr-8 text-[13px]", + MD: "h-[40px] pl-4 pr-10 text-sm", + LG: "h-[48px] pl-4 pr-10 px-5 text-base", +} as const; + +const comboboxVariants = cva({ + variants: { size: sizes }, +}); + +type BaseProps = React.ComponentProps; + +interface ComboboxProps extends Omit { + displayValue: (option: ComboboxOption) => string; + onInputChange: (option: string) => void; + options: () => ComboboxOption[]; + placeholder?: string; + emptyMessage?: string; + size?: keyof typeof sizes; + disabledMessage?: string; +} + +export function Combobox({ + onInputChange, + displayValue, + options, + disabled = false, + placeholder = "Search...", + emptyMessage = "No results found", + size = "MD", + onChange, + disabledMessage = "Input disabled", + ...otherProps +}: ComboboxProps) { + const inputRef = useRef(null); + const classes = comboboxVariants({ size }); + + return ( + + {() => ( + <> + + onInputChange(event.target.value)} + disabled={disabled} + /> + + + {options().length > 0 && ( + + {options().map((option) => ( + + {option.label} + + ))} + + )} + + {options().length === 0 && inputRef.current?.value && ( +
    +
    + {emptyMessage} +
    +
    + )} + + )} +
    + ); +} \ No newline at end of file diff --git a/ui/src/components/ConfirmDialog.tsx b/ui/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..57391e2 --- /dev/null +++ b/ui/src/components/ConfirmDialog.tsx @@ -0,0 +1,106 @@ +import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from "@heroicons/react/24/outline"; +import { cx } from "@/cva.config"; +import { Button } from "@/components/Button"; +import Modal from "@/components/Modal"; + +type Variant = "danger" | "success" | "warning" | "info"; + +interface ConfirmDialogProps { + open: boolean; + onClose: () => void; + title: string; + description: string; + variant?: Variant; + confirmText?: string; + cancelText?: string | null; + onConfirm: () => void; + isConfirming?: boolean; +} + +const variantConfig = { + danger: { + icon: ExclamationTriangleIcon, + iconClass: "text-red-600", + iconBgClass: "bg-red-100", + buttonTheme: "danger", + }, + success: { + icon: CheckCircleIcon, + iconClass: "text-green-600", + iconBgClass: "bg-green-100", + buttonTheme: "primary", + }, + warning: { + icon: ExclamationTriangleIcon, + iconClass: "text-yellow-600", + iconBgClass: "bg-yellow-100", + buttonTheme: "lightDanger", + }, + info: { + icon: InformationCircleIcon, + iconClass: "text-blue-600", + iconBgClass: "bg-blue-100", + buttonTheme: "primary", + }, +} as Record; + +export function ConfirmDialog({ + open, + onClose, + title, + description, + variant = "info", + confirmText = "Confirm", + cancelText = "Cancel", + onConfirm, + isConfirming = false, +}: ConfirmDialogProps) { + const { icon: Icon, iconClass, iconBgClass, buttonTheme } = variantConfig[variant]; + + return ( + +
    +
    +
    +
    +
    +
    +
    +

    + {title} +

    +
    + {description} +
    +
    +
    + +
    + {cancelText && ( +
    +
    +
    +
    +
    + ); +} \ No newline at end of file diff --git a/ui/src/components/FieldLabel.tsx b/ui/src/components/FieldLabel.tsx index 42e6ede..f9065a1 100644 --- a/ui/src/components/FieldLabel.tsx +++ b/ui/src/components/FieldLabel.tsx @@ -49,4 +49,4 @@ export default function FieldLabel({ } else { return <>; } -} +} \ No newline at end of file diff --git a/ui/src/components/MacroBar.tsx b/ui/src/components/MacroBar.tsx new file mode 100644 index 0000000..066c21f --- /dev/null +++ b/ui/src/components/MacroBar.tsx @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import { LuCommand } from "react-icons/lu"; + +import { Button } from "@components/Button"; +import Container from "@components/Container"; +import { useMacrosStore } from "@/hooks/stores"; +import useKeyboard from "@/hooks/useKeyboard"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; + +export default function MacroBar() { + const { macros, initialized, loadMacros, setSendFn } = useMacrosStore(); + const { executeMacro } = useKeyboard(); + const [send] = useJsonRpc(); + + useEffect(() => { + setSendFn(send); + + if (!initialized) { + loadMacros(); + } + }, [initialized, loadMacros, setSendFn, send]); + + if (macros.length === 0) { + return null; + } + + return ( + +
    +
    + +
    +
    + {macros.map(macro => ( +
    +
    +
    + ); +} \ No newline at end of file diff --git a/ui/src/components/MacroForm.tsx b/ui/src/components/MacroForm.tsx new file mode 100644 index 0000000..135817c --- /dev/null +++ b/ui/src/components/MacroForm.tsx @@ -0,0 +1,271 @@ +import { useState } from "react"; + +import { LuPlus } from "react-icons/lu"; + +import { KeySequence } from "@/hooks/stores"; +import { Button } from "@/components/Button"; +import { InputFieldWithLabel, FieldError } from "@/components/InputField"; +import Fieldset from "@/components/Fieldset"; +import { MacroStepCard } from "@/components/MacroStepCard"; +import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros"; +import FieldLabel from "@/components/FieldLabel"; + +interface ValidationErrors { + name?: string; + steps?: Record; +} + +interface MacroFormProps { + initialData: Partial; + onSubmit: (macro: Partial) => Promise; + onCancel: () => void; + isSubmitting?: boolean; + submitText?: string; +} + +export function MacroForm({ + initialData, + onSubmit, + onCancel, + isSubmitting = false, + submitText = "Save Macro", +}: MacroFormProps) { + const [macro, setMacro] = useState>(initialData); + const [keyQueries, setKeyQueries] = useState>({}); + const [errors, setErrors] = useState({}); + const [errorMessage, setErrorMessage] = useState(null); + + const showTemporaryError = (message: string) => { + setErrorMessage(message); + setTimeout(() => setErrorMessage(null), 3000); + }; + + const validateForm = (): boolean => { + const newErrors: ValidationErrors = {}; + + // Name validation + if (!macro.name?.trim()) { + newErrors.name = "Name is required"; + } else if (macro.name.trim().length > 50) { + newErrors.name = "Name must be less than 50 characters"; + } + + if (!macro.steps?.length) { + newErrors.steps = { 0: { keys: "At least one step is required" } }; + } else { + const hasKeyOrModifier = macro.steps.some(step => + (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0 + ); + + if (!hasKeyOrModifier) { + newErrors.steps = { 0: { keys: "At least one step must have keys or modifiers" } }; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm()) { + showTemporaryError("Please fix the validation errors"); + return; + } + + try { + await onSubmit(macro); + } catch (error) { + if (error instanceof Error) { + showTemporaryError(error.message); + } else { + showTemporaryError("An error occurred while saving"); + } + } + }; + + const handleKeySelect = (stepIndex: number, option: { value: string | null; keys?: string[] }) => { + const newSteps = [...(macro.steps || [])]; + if (!newSteps[stepIndex]) return; + + if (option.keys) { + newSteps[stepIndex].keys = option.keys; + } else if (option.value) { + if (!newSteps[stepIndex].keys) { + newSteps[stepIndex].keys = []; + } + const keysArray = Array.isArray(newSteps[stepIndex].keys) ? newSteps[stepIndex].keys : []; + if (keysArray.length >= MAX_KEYS_PER_STEP) { + showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`); + return; + } + newSteps[stepIndex].keys = [...keysArray, option.value]; + } + setMacro({ ...macro, steps: newSteps }); + + if (errors.steps?.[stepIndex]?.keys) { + const newErrors = { ...errors }; + delete newErrors.steps?.[stepIndex].keys; + if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { + delete newErrors.steps?.[stepIndex]; + } + if (Object.keys(newErrors.steps || {}).length === 0) { + delete newErrors.steps; + } + setErrors(newErrors); + } + }; + + const handleKeyQueryChange = (stepIndex: number, query: string) => { + setKeyQueries(prev => ({ ...prev, [stepIndex]: query })); + }; + + const handleModifierChange = (stepIndex: number, modifiers: string[]) => { + const newSteps = [...(macro.steps || [])]; + newSteps[stepIndex].modifiers = modifiers; + setMacro({ ...macro, steps: newSteps }); + + // Clear step errors when modifiers are added + if (errors.steps?.[stepIndex]?.keys && modifiers.length > 0) { + const newErrors = { ...errors }; + delete newErrors.steps?.[stepIndex].keys; + if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { + delete newErrors.steps?.[stepIndex]; + } + if (Object.keys(newErrors.steps || {}).length === 0) { + delete newErrors.steps; + } + setErrors(newErrors); + } + }; + + const handleDelayChange = (stepIndex: number, delay: number) => { + const newSteps = [...(macro.steps || [])]; + newSteps[stepIndex].delay = delay; + setMacro({ ...macro, steps: newSteps }); + }; + + const handleStepMove = (stepIndex: number, direction: 'up' | 'down') => { + const newSteps = [...(macro.steps || [])]; + const newIndex = direction === 'up' ? stepIndex - 1 : stepIndex + 1; + [newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]]; + setMacro({ ...macro, steps: newSteps }); + }; + + const isMaxStepsReached = (macro.steps?.length || 0) >= MAX_STEPS_PER_MACRO; + + return ( + <> +
    +
    + { + setMacro(prev => ({ ...prev, name: e.target.value })); + if (errors.name) { + const newErrors = { ...errors }; + delete newErrors.name; + setErrors(newErrors); + } + }} + /> +
    + +
    +
    +
    + +
    + + {macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps + +
    + {errors.steps && errors.steps[0]?.keys && ( +
    + +
    + )} +
    +
    + {(macro.steps || []).map((step, stepIndex) => ( + 1 ? () => { + const newSteps = [...(macro.steps || [])]; + newSteps.splice(stepIndex, 1); + setMacro(prev => ({ ...prev, steps: newSteps })); + } : undefined} + onMoveUp={() => handleStepMove(stepIndex, 'up')} + onMoveDown={() => handleStepMove(stepIndex, 'down')} + onKeySelect={(option) => handleKeySelect(stepIndex, option)} + onKeyQueryChange={(query) => handleKeyQueryChange(stepIndex, query)} + keyQuery={keyQueries[stepIndex] || ''} + onModifierChange={(modifiers) => handleModifierChange(stepIndex, modifiers)} + onDelayChange={(delay) => handleDelayChange(stepIndex, delay)} + isLastStep={stepIndex === (macro.steps?.length || 0) - 1} + /> + ))} +
    +
    + +
    +
    + + {errorMessage && ( +
    + +
    + )} + +
    +
    +
    +
    + + ); +} \ No newline at end of file diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx new file mode 100644 index 0000000..8642c28 --- /dev/null +++ b/ui/src/components/MacroStepCard.tsx @@ -0,0 +1,235 @@ +import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; + +import { Button } from "@/components/Button"; +import { Combobox } from "@/components/Combobox"; +import { SelectMenuBasic } from "@/components/SelectMenuBasic"; +import Card from "@/components/Card"; +import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; +import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; +import FieldLabel from "@/components/FieldLabel"; + +// Filter out modifier keys since they're handled in the modifiers section +const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; + +const keyOptions = Object.keys(keys) + .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) + .map(key => ({ + value: key, + label: keyDisplayMap[key] || key, + })); + +const modifierOptions = Object.keys(modifiers).map(modifier => ({ + value: modifier, + label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), +})); + +const groupedModifiers: Record = { + Control: modifierOptions.filter(mod => mod.value.startsWith('Control')), + Shift: modifierOptions.filter(mod => mod.value.startsWith('Shift')), + Alt: modifierOptions.filter(mod => mod.value.startsWith('Alt')), + Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')), +}; + +const basePresetDelays = [ + { value: "50", label: "50ms" }, + { value: "100", label: "100ms" }, + { value: "200", label: "200ms" }, + { value: "300", label: "300ms" }, + { value: "500", label: "500ms" }, + { value: "750", label: "750ms" }, + { value: "1000", label: "1000ms" }, + { value: "1500", label: "1500ms" }, + { value: "2000", label: "2000ms" }, +]; + +const PRESET_DELAYS = basePresetDelays.map(delay => { + if (parseInt(delay.value, 10) === DEFAULT_DELAY) { + return { ...delay, label: "Default" }; + } + return delay; +}); + +interface MacroStep { + keys: string[]; + modifiers: string[]; + delay: number; +} + +interface MacroStepCardProps { + step: MacroStep; + stepIndex: number; + onDelete?: () => void; + onMoveUp?: () => void; + onMoveDown?: () => void; + onKeySelect: (option: { value: string | null; keys?: string[] }) => void; + onKeyQueryChange: (query: string) => void; + keyQuery: string; + onModifierChange: (modifiers: string[]) => void; + onDelayChange: (delay: number) => void; + isLastStep: boolean; +} + +const ensureArray = (arr: T[] | null | undefined): T[] => { + return Array.isArray(arr) ? arr : []; +}; + +export function MacroStepCard({ + step, + stepIndex, + onDelete, + onMoveUp, + onMoveDown, + onKeySelect, + onKeyQueryChange, + keyQuery, + onModifierChange, + onDelayChange, + isLastStep +}: MacroStepCardProps) { + const getFilteredKeys = () => { + const selectedKeys = ensureArray(step.keys); + const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value)); + + if (keyQuery === '') { + return availableKeys; + } else { + return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); + } + }; + + return ( + +
    +
    + + {stepIndex + 1} + +
    + +
    +
    +
    + {onDelete && ( +
    +
    + +
    +
    + +
    + {Object.entries(groupedModifiers).map(([group, mods]) => ( +
    + + {group} + +
    + {mods.map(option => ( +
    +
    + ))} +
    +
    + +
    +
    + +
    + {ensureArray(step.keys) && step.keys.length > 0 && ( +
    + {step.keys.map((key, keyIndex) => ( + + + {keyDisplayMap[key] || key} + +
    + )} +
    + { + onKeySelect(value); + onKeyQueryChange(''); + }} + displayValue={() => keyQuery} + onInputChange={onKeyQueryChange} + options={getFilteredKeys} + disabledMessage="Max keys reached" + size="SM" + immediate + disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP} + placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."} + emptyMessage="No matching keys found" + /> +
    +
    + +
    +
    + +
    +
    + onDelayChange(parseInt(e.target.value, 10))} + options={PRESET_DELAYS} + /> +
    +
    +
    +
    + ); +} \ No newline at end of file diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index ec5906c..09a94a6 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -11,7 +11,7 @@ import "react-simple-keyboard/build/css/index.css"; import { useHidStore, useUiStore } from "@/hooks/stores"; import { cx } from "@/cva.config"; -import { keys, modifiers } from "@/keyboardMappings"; +import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; import useKeyboard from "@/hooks/useKeyboard"; import DetachIconRaw from "@/assets/detach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg"; @@ -260,136 +260,7 @@ function KeyboardWrapper() { buttons: "CtrlAltDelete AltMetaEscape", }, ]} - display={{ - CtrlAltDelete: "Ctrl + Alt + Delete", - AltMetaEscape: "Alt + Meta + Escape", - Escape: "esc", - Tab: "tab", - Backspace: "backspace", - "(Backspace)": "backspace", - Enter: "enter", - CapsLock: "caps lock", - ShiftLeft: "shift", - ShiftRight: "shift", - ControlLeft: "ctrl", - AltLeft: "alt", - AltRight: "alt", - MetaLeft: "meta", - MetaRight: "meta", - KeyQ: "q", - KeyW: "w", - KeyE: "e", - KeyR: "r", - KeyT: "t", - KeyY: "y", - KeyU: "u", - KeyI: "i", - KeyO: "o", - KeyP: "p", - KeyA: "a", - KeyS: "s", - KeyD: "d", - KeyF: "f", - KeyG: "g", - KeyH: "h", - KeyJ: "j", - KeyK: "k", - KeyL: "l", - KeyZ: "z", - KeyX: "x", - KeyC: "c", - KeyV: "v", - KeyB: "b", - KeyN: "n", - KeyM: "m", - - "(KeyQ)": "Q", - "(KeyW)": "W", - "(KeyE)": "E", - "(KeyR)": "R", - "(KeyT)": "T", - "(KeyY)": "Y", - "(KeyU)": "U", - "(KeyI)": "I", - "(KeyO)": "O", - "(KeyP)": "P", - "(KeyA)": "A", - "(KeyS)": "S", - "(KeyD)": "D", - "(KeyF)": "F", - "(KeyG)": "G", - "(KeyH)": "H", - "(KeyJ)": "J", - "(KeyK)": "K", - "(KeyL)": "L", - "(KeyZ)": "Z", - "(KeyX)": "X", - "(KeyC)": "C", - "(KeyV)": "V", - "(KeyB)": "B", - "(KeyN)": "N", - "(KeyM)": "M", - Digit1: "1", - Digit2: "2", - Digit3: "3", - Digit4: "4", - Digit5: "5", - Digit6: "6", - Digit7: "7", - Digit8: "8", - Digit9: "9", - Digit0: "0", - - "(Digit1)": "!", - "(Digit2)": "@", - "(Digit3)": "#", - "(Digit4)": "$", - "(Digit5)": "%", - "(Digit6)": "^", - "(Digit7)": "&", - "(Digit8)": "*", - "(Digit9)": "(", - "(Digit0)": ")", - Minus: "-", - "(Minus)": "_", - - Equal: "=", - "(Equal)": "+", - BracketLeft: "[", - BracketRight: "]", - "(BracketLeft)": "{", - "(BracketRight)": "}", - Backslash: "\\", - "(Backslash)": "|", - - Semicolon: ";", - "(Semicolon)": ":", - Quote: "'", - "(Quote)": '"', - Comma: ",", - "(Comma)": "<", - Period: ".", - "(Period)": ">", - Slash: "/", - "(Slash)": "?", - Space: " ", - Backquote: "`", - "(Backquote)": "~", - IntlBackslash: "\\", - - F1: "F1", - F2: "F2", - F3: "F3", - F4: "F4", - F5: "F5", - F6: "F6", - F7: "F7", - F8: "F8", - F9: "F9", - F10: "F10", - F11: "F11", - F12: "F12", - }} + display={keyDisplayMap} layout={{ default: [ "CtrlAltDelete AltMetaEscape", diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 970867a..be69899 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -13,6 +13,7 @@ import { useResizeObserver } from "@/hooks/useResizeObserver"; import { cx } from "@/cva.config"; import VirtualKeyboard from "@components/VirtualKeyboard"; import Actionbar from "@components/ActionBar"; +import MacroBar from "@/components/MacroBar"; import InfoBar from "@components/InfoBar"; import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; @@ -553,16 +554,19 @@ export default function WebRTCVideo() { return (
    -
    -
    - - videoElm.current?.requestFullscreen({ - navigationUI: "show", - }) - } - /> -
    +
    +
    +
    + + videoElm.current?.requestFullscreen({ + navigationUI: "show", + }) + } + /> + +
    +
    ( @@ -649,3 +662,146 @@ export const useDeviceStore = create(set => ({ setAppVersion: version => set({ appVersion: version }), setSystemVersion: version => set({ systemVersion: version }), })); + +export interface KeySequenceStep { + keys: string[]; + modifiers: string[]; + delay: number; +} + +export interface KeySequence { + id: string; + name: string; + steps: KeySequenceStep[]; + sortOrder?: number; +} + +export interface MacrosState { + macros: KeySequence[]; + loading: boolean; + initialized: boolean; + loadMacros: () => Promise; + saveMacros: (macros: KeySequence[]) => Promise; + sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void) | null; + setSendFn: (sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void)) => void; +} + +export const generateMacroId = () => { + return Math.random().toString(36).substring(2, 9); +}; + +export const useMacrosStore = create((set, get) => ({ + macros: [], + loading: false, + initialized: false, + sendFn: null, + + setSendFn: (sendFn) => { + set({ sendFn }); + }, + + loadMacros: async () => { + if (get().initialized) return; + + const { sendFn } = get(); + if (!sendFn) { + console.warn("JSON-RPC send function not available."); + return; + } + + set({ loading: true }); + + try { + await new Promise((resolve, reject) => { + sendFn("getKeyboardMacros", {}, (response) => { + if (response.error) { + console.error("Error loading macros:", response.error); + reject(new Error(response.error.message)); + return; + } + + const macros = (response.result as KeySequence[]) || []; + + const sortedMacros = [...macros].sort((a, b) => { + if (a.sortOrder !== undefined && b.sortOrder !== undefined) { + return a.sortOrder - b.sortOrder; + } + if (a.sortOrder !== undefined) return -1; + if (b.sortOrder !== undefined) return 1; + return 0; + }); + + set({ + macros: sortedMacros, + initialized: true + }); + + resolve(); + }); + }); + } catch (error) { + console.error("Failed to load macros:", error); + } finally { + set({ loading: false }); + } + }, + + saveMacros: async (macros: KeySequence[]) => { + const { sendFn } = get(); + if (!sendFn) { + console.warn("JSON-RPC send function not available."); + throw new Error("JSON-RPC send function not available"); + } + + if (macros.length > MAX_TOTAL_MACROS) { + console.error(`Cannot save: exceeded maximum of ${MAX_TOTAL_MACROS} macros`); + throw new Error(`Cannot save: exceeded maximum of ${MAX_TOTAL_MACROS} macros`); + } + + for (const macro of macros) { + if (macro.steps.length > MAX_STEPS_PER_MACRO) { + console.error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`); + throw new Error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`); + } + + for (let i = 0; i < macro.steps.length; i++) { + const step = macro.steps[i]; + if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) { + console.error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + throw new Error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + } + } + } + + set({ loading: true }); + + try { + const macrosWithSortOrder = macros.map((macro, index) => ({ + ...macro, + sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index + })); + + const response = await new Promise((resolve) => { + sendFn("setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, (response) => { + resolve(response); + }); + }); + + if (response.error) { + console.error("Error saving macros:", response.error); + const errorMessage = typeof response.error.data === 'string' + ? response.error.data + : response.error.message || "Failed to save macros"; + throw new Error(errorMessage); + } + + // Only update the store if the request was successful + set({ macros: macrosWithSortOrder }); + } catch (error) { + console.error("Failed to save macros:", error); + throw error; + } finally { + set({ loading: false }); + } + } +})); \ No newline at end of file diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 137fc8b..0ce1eef 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -2,6 +2,7 @@ import { useCallback } from "react"; import { useHidStore, useRTCStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { keys, modifiers } from "@/keyboardMappings"; export default function useKeyboard() { const [send] = useJsonRpc(); @@ -28,5 +29,28 @@ export default function useKeyboard() { sendKeyboardEvent([], []); }, [sendKeyboardEvent]); - return { sendKeyboardEvent, resetKeyboardState }; + const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => { + for (const [index, step] of steps.entries()) { + const keyValues = step.keys?.map(key => keys[key]).filter(Boolean) || []; + const modifierValues = step.modifiers?.map(mod => modifiers[mod]).filter(Boolean) || []; + + // If the step has keys and/or modifiers, press them and hold for the delay + if (keyValues.length > 0 || modifierValues.length > 0) { + sendKeyboardEvent(keyValues, modifierValues); + await new Promise(resolve => setTimeout(resolve, step.delay || 50)); + + resetKeyboardState(); + } else { + // This is a delay-only step, just wait for the delay amount + await new Promise(resolve => setTimeout(resolve, step.delay || 50)); + } + + // Add a small pause between steps if not the last step + if (index < steps.length - 1) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + } + }; + + return { sendKeyboardEvent, resetKeyboardState, executeMacro }; } diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index ffc781c..347939a 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -212,3 +212,80 @@ export const modifiers = { MetaLeft: 0x08, MetaRight: 0x80, } as Record; + +export const modifierDisplayMap: Record = { + ControlLeft: "Left Ctrl", + ControlRight: "Right Ctrl", + ShiftLeft: "Left Shift", + ShiftRight: "Right Shift", + AltLeft: "Left Alt", + AltRight: "Right Alt", + MetaLeft: "Left Meta", + MetaRight: "Right Meta", +} as Record; + +export const keyDisplayMap: Record = { + CtrlAltDelete: "Ctrl + Alt + Delete", + AltMetaEscape: "Alt + Meta + Escape", + Escape: "esc", + Tab: "tab", + Backspace: "backspace", + Enter: "enter", + CapsLock: "caps lock", + ShiftLeft: "shift", + ShiftRight: "shift", + ControlLeft: "ctrl", + AltLeft: "alt", + AltRight: "alt", + MetaLeft: "meta", + MetaRight: "meta", + Space: " ", + Home: "home", + PageUp: "pageup", + Delete: "delete", + End: "end", + PageDown: "pagedown", + ArrowLeft: "←", + ArrowRight: "→", + ArrowUp: "↑", + ArrowDown: "↓", + + // Letters + KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e", + KeyF: "f", KeyG: "g", KeyH: "h", KeyI: "i", KeyJ: "j", + KeyK: "k", KeyL: "l", KeyM: "m", KeyN: "n", KeyO: "o", + KeyP: "p", KeyQ: "q", KeyR: "r", KeyS: "s", KeyT: "t", + KeyU: "u", KeyV: "v", KeyW: "w", KeyX: "x", KeyY: "y", + KeyZ: "z", + + // Numbers + Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4", Digit5: "5", + Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9", Digit0: "0", + + // Symbols + Minus: "-", + Equal: "=", + BracketLeft: "[", + BracketRight: "]", + Backslash: "\\", + Semicolon: ";", + Quote: "'", + Comma: ",", + Period: ".", + Slash: "/", + Backquote: "`", + IntlBackslash: "\\", + + // Function keys + F1: "F1", F2: "F2", F3: "F3", F4: "F4", + F5: "F5", F6: "F6", F7: "F7", F8: "F8", + F9: "F9", F10: "F10", F11: "F11", F12: "F12", + + // Numpad + Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2", + Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5", + Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8", + Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -", + NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .", + NumpadEnter: "Num Enter" +}; diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 066ee57..e09a2a9 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -43,6 +43,9 @@ import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index"; import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update"; import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth"; +import SettingsMacrosRoute from "./routes/devices.$id.settings.macros"; +import SettingsMacrosAddRoute from "./routes/devices.$id.settings.macros.add"; +import SettingsMacrosEditRoute from "./routes/devices.$id.settings.macros.edit"; export const isOnDevice = import.meta.env.MODE === "device"; export const isInCloud = !isOnDevice; @@ -175,6 +178,23 @@ if (isOnDevice) { path: "appearance", element: , }, + { + path: "macros", + children: [ + { + index: true, + element: , + }, + { + path: "add", + element: , + }, + { + path: ":macroId/edit", + element: , + }, + ], + }, ], }, ], @@ -283,6 +303,23 @@ if (isOnDevice) { path: "appearance", element: , }, + { + path: "macros", + children: [ + { + index: true, + element: , + }, + { + path: "add", + element: , + }, + { + path: ":macroId/edit", + element: , + }, + ], + }, ], }, ], diff --git a/ui/src/routes/devices.$id.settings.macros.add.tsx b/ui/src/routes/devices.$id.settings.macros.add.tsx new file mode 100644 index 0000000..1b3ce30 --- /dev/null +++ b/ui/src/routes/devices.$id.settings.macros.add.tsx @@ -0,0 +1,63 @@ +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; + +import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; +import { SettingsPageHeader } from "@/components/SettingsPageheader"; +import { MacroForm } from "@/components/MacroForm"; +import { DEFAULT_DELAY } from "@/constants/macros"; +import notifications from "@/notifications"; + +export default function SettingsMacrosAddRoute() { + const { macros, saveMacros } = useMacrosStore(); + const [isSaving, setIsSaving] = useState(false); + const navigate = useNavigate(); + + const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { + return macros.map((macro, index) => ({ + ...macro, + sortOrder: index + 1, + })); + }; + + const handleAddMacro = async (macro: Partial) => { + setIsSaving(true); + try { + const newMacro: KeySequence = { + id: generateMacroId(), + name: macro.name!.trim(), + steps: macro.steps || [], + sortOrder: macros.length + 1, + }; + + await saveMacros(normalizeSortOrders([...macros, newMacro])); + notifications.success(`Macro "${newMacro.name}" created successfully`); + navigate("../"); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to create macro: ${error.message}`); + } else { + notifications.error("Failed to create macro"); + } + } finally { + setIsSaving(false); + } + }; + + return ( +
    + + navigate("../")} + isSubmitting={isSaving} + /> +
    + ); +} \ No newline at end of file diff --git a/ui/src/routes/devices.$id.settings.macros.edit.tsx b/ui/src/routes/devices.$id.settings.macros.edit.tsx new file mode 100644 index 0000000..336fe85 --- /dev/null +++ b/ui/src/routes/devices.$id.settings.macros.edit.tsx @@ -0,0 +1,134 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { useState, useEffect } from "react"; +import { LuTrash2 } from "react-icons/lu"; + +import { KeySequence, useMacrosStore } from "@/hooks/stores"; +import { SettingsPageHeader } from "@/components/SettingsPageheader"; +import { MacroForm } from "@/components/MacroForm"; +import notifications from "@/notifications"; +import { Button } from "@/components/Button"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; + +const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { + return macros.map((macro, index) => ({ + ...macro, + sortOrder: index + 1, + })); +}; + +export default function SettingsMacrosEditRoute() { + const { macros, saveMacros } = useMacrosStore(); + const [isUpdating, setIsUpdating] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const navigate = useNavigate(); + const { macroId } = useParams<{ macroId: string }>(); + const [macro, setMacro] = useState(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + useEffect(() => { + const foundMacro = macros.find(m => m.id === macroId); + if (foundMacro) { + setMacro({ + ...foundMacro, + steps: foundMacro.steps.map(step => ({ + ...step, + keys: Array.isArray(step.keys) ? step.keys : [], + modifiers: Array.isArray(step.modifiers) ? step.modifiers : [], + delay: typeof step.delay === 'number' ? step.delay : 0 + })) + }); + } else { + navigate("../"); + } + }, [macroId, macros, navigate]); + + const handleUpdateMacro = async (updatedMacro: Partial) => { + if (!macro) return; + + setIsUpdating(true); + try { + const newMacros = macros.map(m => + m.id === macro.id ? { + ...macro, + name: updatedMacro.name!.trim(), + steps: updatedMacro.steps || [], + } : m + ); + + await saveMacros(normalizeSortOrders(newMacros)); + notifications.success(`Macro "${updatedMacro.name}" updated successfully`); + navigate("../"); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to update macro: ${error.message}`); + } else { + notifications.error("Failed to update macro"); + } + } finally { + setIsUpdating(false); + } + }; + + const handleDeleteMacro = async () => { + if (!macro) return; + + setIsDeleting(true); + try { + const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macro.id)); + await saveMacros(updatedMacros); + notifications.success(`Macro "${macro.name}" deleted successfully`); + navigate("../macros"); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to delete macro: ${error.message}`); + } else { + notifications.error("Failed to delete macro"); + } + } finally { + setIsDeleting(false); + } + }; + + if (!macro) return null; + + return ( +
    +
    + +
    + navigate("../")} + isSubmitting={isUpdating} + submitText="Save Changes" + /> + + setShowDeleteConfirm(false)} + title="Delete Macro" + description="Are you sure you want to delete this macro? This action cannot be undone." + variant="danger" + confirmText={isDeleting ? "Deleting" : "Delete"} + onConfirm={() => { + handleDeleteMacro(); + setShowDeleteConfirm(false); + }} + isConfirming={isDeleting} + /> +
    + ); +} \ No newline at end of file diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx new file mode 100644 index 0000000..f809f57 --- /dev/null +++ b/ui/src/routes/devices.$id.settings.macros.tsx @@ -0,0 +1,306 @@ +import { useEffect, Fragment, useMemo, useState, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu"; + +import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; +import { SettingsPageHeader } from "@/components/SettingsPageheader"; +import { Button } from "@/components/Button"; +import EmptyCard from "@/components/EmptyCard"; +import Card from "@/components/Card"; +import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros"; +import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings"; +import notifications from "@/notifications"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; +import LoadingSpinner from "@/components/LoadingSpinner"; + +const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { + return macros.map((macro, index) => ({ + ...macro, + sortOrder: index + 1, + })); +}; + +export default function SettingsMacrosRoute() { + const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore(); + const navigate = useNavigate(); + const [actionLoadingId, setActionLoadingId] = useState(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [macroToDelete, setMacroToDelete] = useState(null); + + const isMaxMacrosReached = useMemo(() => + macros.length >= MAX_TOTAL_MACROS, + [macros.length] + ); + + useEffect(() => { + if (!initialized) { + loadMacros(); + } + }, [initialized, loadMacros]); + + const handleDuplicateMacro = useCallback(async (macro: KeySequence) => { + if (!macro?.id || !macro?.name) { + notifications.error("Invalid macro data"); + return; + } + + if (isMaxMacrosReached) { + notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`); + return; + } + + setActionLoadingId(macro.id); + + const newMacroCopy: KeySequence = { + ...JSON.parse(JSON.stringify(macro)), + id: generateMacroId(), + name: `${macro.name} ${COPY_SUFFIX}`, + sortOrder: macros.length + 1, + }; + + try { + await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); + notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to duplicate macro: ${error.message}`); + } else { + notifications.error("Failed to duplicate macro"); + } + } finally { + setActionLoadingId(null); + } + }, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]); + + const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => { + if (!Array.isArray(macros) || macros.length === 0) { + notifications.error("No macros available"); + return; + } + + const newIndex = direction === 'up' ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= macros.length) return; + + setActionLoadingId(macroId); + + try { + const newMacros = [...macros]; + [newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]]; + const updatedMacros = normalizeSortOrders(newMacros); + + await saveMacros(updatedMacros); + notifications.success("Macro order updated successfully"); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to reorder macros: ${error.message}`); + } else { + notifications.error("Failed to reorder macros"); + } + } finally { + setActionLoadingId(null); + } + }, [macros, saveMacros, setActionLoadingId]); + + const handleDeleteMacro = useCallback(async () => { + if (!macroToDelete?.id) return; + + setActionLoadingId(macroToDelete.id); + try { + const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id)); + await saveMacros(updatedMacros); + notifications.success(`Macro "${macroToDelete.name}" deleted successfully`); + setShowDeleteConfirm(false); + setMacroToDelete(null); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to delete macro: ${error.message}`); + } else { + notifications.error("Failed to delete macro"); + } + } finally { + setActionLoadingId(null); + } + }, [macroToDelete, macros, saveMacros]); + + const MacroList = useMemo(() => ( +
    + {macros.map((macro, index) => ( + +
    +
    +
    + +
    +

    + {macro.name} +

    +

    + + {macro.steps.map((step, stepIndex) => { + const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight; + + return ( + + + + {(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? ( + <> + {Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => ( + + + {modifierDisplayMap[modifier] || modifier} + + {idx < step.modifiers.length - 1 && ( + + + )} + + ))} + + {Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && ( + + + )} + + {Array.isArray(step.keys) && step.keys.map((key, idx) => ( + + + {keyDisplayMap[key] || key} + + {idx < step.keys.length - 1 && ( + + + )} + + ))} + + ) : ( + Delay only + )} + {step.delay !== DEFAULT_DELAY && ( + ({step.delay}ms) + )} + + + ); + })} + +

    +
    + +
    +
    +
    +
    + ))} + + { + setShowDeleteConfirm(false); + setMacroToDelete(null); + }} + title="Delete Macro" + description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`} + variant="danger" + confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"} + onConfirm={handleDeleteMacro} + isConfirming={actionLoadingId === macroToDelete?.id} + /> +
    + ), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]); + + return ( +
    +
    + + { macros.length > 0 && ( +
    +
    + )} +
    + +
    + {loading && macros.length === 0 ? ( + + +
    + } + /> + ) : macros.length === 0 ? ( + navigate("add")} + disabled={isMaxMacrosReached} + aria-label="Add new macro" + /> + } + /> + ) : MacroList} +
    +
    + ); +} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index 4742445..db7d6b0 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -8,6 +8,7 @@ import { LuWrench, LuArrowLeft, LuPalette, + LuCommand, } from "react-icons/lu"; import React, { useEffect, useRef, useState } from "react"; @@ -195,6 +196,17 @@ export default function SettingsRoute() {
    +
    + (isActive ? "active" : "")} + > +
    + +

    Keyboard Macros

    +
    +
    +
    Date: Thu, 10 Apr 2025 17:05:34 +0200 Subject: [PATCH 024/165] refactor: use structured logging --- block_device.go | 12 +- cloud.go | 31 ++-- config.go | 6 +- display.go | 28 ++-- fuse.go | 2 +- go.mod | 40 +++--- go.sum | 84 ++++++----- hw.go | 8 +- internal/usbgadget/config.go | 28 ++-- internal/usbgadget/hid_keyboard.go | 2 +- internal/usbgadget/hid_mouse_absolute.go | 2 +- internal/usbgadget/hid_mouse_relative.go | 2 +- internal/usbgadget/udc.go | 6 +- internal/usbgadget/usbgadget.go | 12 +- internal/usbgadget/utils.go | 6 +- jiggler.go | 4 +- jsonrpc.go | 46 +++--- log.go | 172 ++++++++++++++++++++++- main.go | 16 +-- native.go | 84 +++++++---- network.go | 29 ++-- ntp.go | 28 ++-- ota.go | 27 ++-- remote_mount.go | 2 +- serial.go | 27 ++-- terminal.go | 10 +- usb.go | 6 +- usb_mass_storage.go | 50 +++---- video.go | 2 +- web.go | 86 ++++++------ web_tls.go | 14 +- webrtc.go | 22 +-- 32 files changed, 553 insertions(+), 341 deletions(-) diff --git a/block_device.go b/block_device.go index 3a44135..4a8769e 100644 --- a/block_device.go +++ b/block_device.go @@ -16,8 +16,8 @@ type remoteImageBackend struct { func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { virtualMediaStateMutex.RLock() - logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) - logger.Debugf("read size: %d, off: %d", len(p), off) + logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState") + logger.Debug().Int64("read size", int64(len(p))).Int64("off", off).Msg("read size and off") if currentVirtualMediaState == nil { return 0, errors.New("image not mounted") } @@ -93,7 +93,7 @@ func (d *NBDDevice) Start() error { // Remove the socket file if it already exists if _, err := os.Stat(nbdSocketPath); err == nil { if err := os.Remove(nbdSocketPath); err != nil { - logger.Errorf("Failed to remove existing socket file %s: %v", nbdSocketPath, err) + nativeLogger.Warn().Err(err).Str("socket_path", nbdSocketPath).Msg("Failed to remove existing socket file") os.Exit(1) } } @@ -134,7 +134,7 @@ func (d *NBDDevice) runServerConn() { MaximumBlockSize: uint32(16 * 1024), SupportsMultiConn: false, }) - logger.Infof("nbd server exited: %v", err) + nativeLogger.Info().Err(err).Msg("nbd server exited") } func (d *NBDDevice) runClientConn() { @@ -142,14 +142,14 @@ func (d *NBDDevice) runClientConn() { ExportName: "jetkvm", BlockSize: uint32(4 * 1024), }) - logger.Infof("nbd client exited: %v", err) + nativeLogger.Info().Err(err).Msg("nbd client exited") } func (d *NBDDevice) Close() { if d.dev != nil { err := client.Disconnect(d.dev) if err != nil { - logger.Warnf("error disconnecting nbd client: %v", err) + nativeLogger.Warn().Err(err).Msg("error disconnecting nbd client") } _ = d.dev.Close() } diff --git a/cloud.go b/cloud.go index 070db8d..a999fc8 100644 --- a/cloud.go +++ b/cloud.go @@ -253,14 +253,14 @@ func disconnectCloud(reason error) { defer cloudDisconnectLock.Unlock() if cloudDisconnectChan == nil { - cloudLogger.Tracef("cloud disconnect channel is not set, no need to disconnect") + cloudLogger.Trace().Msg("cloud disconnect channel is not set, no need to disconnect") return } // just in case the channel is closed, we don't want to panic defer func() { if r := recover(); r != nil { - cloudLogger.Infof("cloud disconnect channel is closed, no need to disconnect: %v", r) + cloudLogger.Warn().Interface("reason", r).Msg("cloud disconnect channel is closed, no need to disconnect") } }() cloudDisconnectChan <- reason @@ -289,11 +289,16 @@ func runWebsocketClient() error { header.Set("Authorization", "Bearer "+config.CloudToken) dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) + scopedLogger := websocketLogger.With(). + Str("source", wsURL.Host). + Str("sourceType", "cloud"). + Logger() + defer cancelDial() c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, OnPingReceived: func(ctx context.Context, payload []byte) bool { - websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: cloud", payload, wsURL.Host) + scopedLogger.Info().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() @@ -304,19 +309,19 @@ func runWebsocketClient() error { // if the context is canceled, we don't want to return an error if err != nil { if errors.Is(err, context.Canceled) { - cloudLogger.Infof("websocket connection canceled") + cloudLogger.Info().Msg("websocket connection canceled") return nil } return err } defer c.CloseNow() //nolint:errcheck - cloudLogger.Infof("websocket connected to %s", wsURL) + cloudLogger.Info().Str("url", wsURL.String()).Msg("websocket connected") // set the metrics when we successfully connect to the cloud. wsResetMetrics(true, "cloud", wsURL.Host) // we don't have a source for the cloud connection - return handleWebRTCSignalWsMessages(c, true, wsURL.Host) + return handleWebRTCSignalWsMessages(c, true, wsURL.Host, &scopedLogger) } func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { @@ -327,7 +332,7 @@ func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessi _ = wsjson.Write(context.Background(), c, gin.H{ "error": fmt.Sprintf("failed to initialize OIDC provider: %v", err), }) - cloudLogger.Errorf("failed to initialize OIDC provider: %v", err) + cloudLogger.Warn().Err(err).Msg("failed to initialize OIDC provider") return err } @@ -396,8 +401,8 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess }() } - cloudLogger.Info("new session accepted") - cloudLogger.Tracef("new session accepted: %v", session) + cloudLogger.Info().Interface("session", session).Msg("new session accepted") + cloudLogger.Trace().Interface("session", session).Msg("new session accepted") currentSession = session _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil @@ -413,21 +418,21 @@ func RunWebsocketClient() { // If the network is not up, well, we can't connect to the cloud. if !networkState.Up { - cloudLogger.Warn("waiting for network to be up, will retry in 3 seconds") + cloudLogger.Warn().Msg("waiting for network to be up, will retry in 3 seconds") time.Sleep(3 * time.Second) continue } // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. if isTimeSyncNeeded() && !timeSyncSuccess { - cloudLogger.Warn("system time is not synced, will retry in 3 seconds") + cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds") time.Sleep(3 * time.Second) continue } err := runWebsocketClient() if err != nil { - cloudLogger.Errorf("websocket client error: %v", err) + cloudLogger.Warn().Err(err).Msg("websocket client error") metricCloudConnectionStatus.Set(0) metricCloudConnectionFailureCount.Inc() time.Sleep(5 * time.Second) @@ -479,7 +484,7 @@ func rpcDeregisterDevice() error { return fmt.Errorf("failed to save configuration after deregistering: %w", err) } - cloudLogger.Infof("device deregistered, disconnecting from cloud") + cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") disconnectCloud(fmt.Errorf("device deregistered")) return nil diff --git a/config.go b/config.go index 1c1b98d..c38f1ed 100644 --- a/config.go +++ b/config.go @@ -132,7 +132,7 @@ func LoadConfig() { defer configLock.Unlock() if config != nil { - logger.Info("config already loaded, skipping") + logger.Info().Msg("config already loaded, skipping") return } @@ -141,7 +141,7 @@ func LoadConfig() { file, err := os.Open(configPath) if err != nil { - logger.Debug("default config file doesn't exist, using default") + logger.Debug().Msg("default config file doesn't exist, using default") return } defer file.Close() @@ -149,7 +149,7 @@ func LoadConfig() { // load and merge the default config with the user config loadedConfig := *defaultConfig if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil { - logger.Errorf("config file JSON parsing failed, %v", err) + logger.Warn().Err(err).Msg("config file JSON parsing failed") return } diff --git a/display.go b/display.go index f4e8cf7..a5528a3 100644 --- a/display.go +++ b/display.go @@ -24,7 +24,7 @@ const ( func switchToScreen(screen string) { _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) if err != nil { - logger.Warnf("failed to switch to screen %s: %v", screen, err) + displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen") return } currentScreen = screen @@ -40,7 +40,7 @@ func updateLabelIfChanged(objName string, newText string) { } func switchToScreenIfDifferent(screenName string) { - logger.Infof("switching screen from %s to %s", currentScreen, screenName) + displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") if currentScreen != screenName { switchToScreen(screenName) } @@ -74,12 +74,12 @@ var displayInited = false func requestDisplayUpdate() { if !displayInited { - logger.Info("display not inited, skipping updates") + displayLogger.Info().Msg("display not inited, skipping updates") return } go func() { wakeDisplay(false) - logger.Info("display updating") + displayLogger.Info().Msg("display updating") //TODO: only run once regardless how many pending updates updateDisplay() }() @@ -118,7 +118,7 @@ func setDisplayBrightness(brightness int) error { return err } - logger.Infof("display: set brightness to %v", brightness) + displayLogger.Info().Int("brightness", brightness).Msg("set brightness") return nil } @@ -127,7 +127,7 @@ func setDisplayBrightness(brightness int) error { func tick_displayDim() { err := setDisplayBrightness(config.DisplayMaxBrightness / 2) if err != nil { - logger.Warnf("display: failed to dim display: %s", err) + displayLogger.Warn().Err(err).Msg("failed to dim display") } dimTicker.Stop() @@ -140,7 +140,7 @@ func tick_displayDim() { func tick_displayOff() { err := setDisplayBrightness(0) if err != nil { - logger.Warnf("display: failed to turn off display: %s", err) + displayLogger.Warn().Err(err).Msg("failed to turn off display") } offTicker.Stop() @@ -163,7 +163,7 @@ func wakeDisplay(force bool) { err := setDisplayBrightness(config.DisplayMaxBrightness) if err != nil { - logger.Warnf("display wake failed, %s", err) + displayLogger.Warn().Err(err).Msg("failed to wake display") } if config.DisplayDimAfterSec != 0 { @@ -183,7 +183,7 @@ func wakeDisplay(force bool) { func watchTsEvents() { ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666) if err != nil { - logger.Warnf("display: failed to open touchscreen device: %s", err) + displayLogger.Warn().Err(err).Msg("failed to open touchscreen device") return } @@ -196,7 +196,7 @@ func watchTsEvents() { for { _, err := ts.Read(buf) if err != nil { - logger.Warnf("display: failed to read from touchscreen device: %s", err) + displayLogger.Warn().Err(err).Msg("failed to read from touchscreen device") return } @@ -216,7 +216,7 @@ func startBacklightTickers() { } if dimTicker == nil && config.DisplayDimAfterSec != 0 { - logger.Info("display: dim_ticker has started") + displayLogger.Info().Msg("dim_ticker has started") dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second) defer dimTicker.Stop() @@ -231,7 +231,7 @@ func startBacklightTickers() { } if offTicker == nil && config.DisplayOffAfterSec != 0 { - logger.Info("display: off_ticker has started") + displayLogger.Info().Msg("off_ticker has started") offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second) defer offTicker.Stop() @@ -251,11 +251,11 @@ func init() { go func() { waitCtrlClientConnected() - logger.Info("setting initial display contents") + displayLogger.Info().Msg("setting initial display contents") time.Sleep(500 * time.Millisecond) updateStaticContents() displayInited = true - logger.Info("display inited") + displayLogger.Info().Msg("display inited") startBacklightTickers() wakeDisplay(true) requestDisplayUpdate() diff --git a/fuse.go b/fuse.go index 29b11f7..ea50bfd 100644 --- a/fuse.go +++ b/fuse.go @@ -103,7 +103,7 @@ func RunFuseServer() { var err error fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts) if err != nil { - logger.Warnf("failed to mount fuse: %v", err) + logger.Warn().Err(err).Msg("failed to mount fuse") } fuseServer.Wait() } diff --git a/go.mod b/go.mod index fae1cbd..1311a33 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/jetkvm/kvm -go 1.21.0 - -toolchain go1.21.1 +go 1.23.0 require ( github.com/Masterminds/semver/v3 v3.3.0 @@ -10,7 +8,8 @@ require ( github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 - github.com/gin-gonic/gin v1.9.1 + github.com/gin-contrib/logger v1.2.5 + github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf github.com/hanwen/go-fuse/v2 v2.5.1 @@ -22,38 +21,39 @@ require ( github.com/prometheus/client_golang v1.21.0 github.com/prometheus/common v0.62.0 github.com/psanford/httpreadat v0.1.0 + github.com/rs/zerolog v1.34.0 github.com/vishvananda/netlink v1.3.0 go.bug.st/serial v1.6.2 - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.33.0 + golang.org/x/crypto v0.36.0 + golang.org/x/net v0.38.0 ) replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/creack/goselect v0.1.2 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pilebones/go-udev v0.9.0 // indirect github.com/pion/datachannel v1.5.9 // indirect github.com/pion/dtls/v3 v3.0.3 // indirect @@ -74,10 +74,10 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/arch v0.8.0 // indirect + golang.org/x/arch v0.15.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1563130..565c0cc 100644 --- a/go.sum +++ b/go.sum @@ -4,24 +4,23 @@ github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM= github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= @@ -29,12 +28,14 @@ github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= +github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -43,10 +44,11 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -63,8 +65,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -78,6 +80,11 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= @@ -89,8 +96,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= @@ -125,6 +132,7 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8= github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= @@ -139,6 +147,9 @@ github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIw github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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= @@ -164,27 +175,27 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -192,4 +203,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/hw.go b/hw.go index 03e9d4b..02e4815 100644 --- a/hw.go +++ b/hw.go @@ -42,7 +42,7 @@ func GetDeviceID() string { deviceIDOnce.Do(func() { serial, err := extractSerialNumber() if err != nil { - logger.Warn("unknown serial number, the program likely not running on RV1106") + logger.Warn().Msg("unknown serial number, the program likely not running on RV1106") deviceID = "unknown_device_id" } else { deviceID = serial @@ -54,7 +54,7 @@ func GetDeviceID() string { func runWatchdog() { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) if err != nil { - logger.Warnf("unable to open /dev/watchdog: %v, skipping watchdog reset", err) + logger.Warn().Err(err).Msg("unable to open /dev/watchdog, skipping watchdog reset") return } defer file.Close() @@ -65,13 +65,13 @@ func runWatchdog() { case <-ticker.C: _, err = file.Write([]byte{0}) if err != nil { - logger.Errorf("error writing to /dev/watchdog, system may reboot: %v", err) + logger.Warn().Err(err).Msg("error writing to /dev/watchdog, system may reboot") } case <-appCtx.Done(): //disarm watchdog with magic value _, err := file.Write([]byte("V")) if err != nil { - logger.Errorf("failed to disarm watchdog, system may reboot: %v", err) + logger.Warn().Err(err).Msg("failed to disarm watchdog, system may reboot") } return } diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 5cc3ed2..b73d392 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -84,7 +84,7 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { func (u *UsbGadget) loadGadgetConfig() { if u.customConfig.isEmpty { - u.log.Trace("using default gadget config") + u.log.Trace().Msg("using default gadget config") return } @@ -163,26 +163,26 @@ func (u *UsbGadget) Init() error { udcs := getUdcs() if len(udcs) < 1 { - u.log.Error("no udc found, skipping USB stack init") + u.log.Error().Msg("no udc found, skipping USB stack init") return nil } u.udc = udcs[0] _, err := os.Stat(u.kvmGadgetPath) if err == nil { - u.log.Info("usb gadget already exists") + u.log.Info().Msg("usb gadget already exists") } if err := mountConfigFS(); err != nil { - u.log.Errorf("failed to mount configfs: %v, usb stack might not function properly", err) + u.log.Error().Err(err).Msg("failed to mount configfs, usb stack might not function properly") } if err := os.MkdirAll(u.configC1Path, 0755); err != nil { - u.log.Errorf("failed to create config path: %v", err) + u.log.Error().Err(err).Msg("failed to create config path") } if err := u.writeGadgetConfig(); err != nil { - u.log.Errorf("failed to start gadget: %v", err) + u.log.Error().Err(err).Msg("failed to start gadget") } return nil @@ -195,7 +195,7 @@ func (u *UsbGadget) UpdateGadgetConfig() error { u.loadGadgetConfig() if err := u.writeGadgetConfig(); err != nil { - u.log.Errorf("failed to update gadget: %v", err) + u.log.Error().Err(err).Msg("failed to update gadget") } return nil @@ -221,21 +221,21 @@ func (u *UsbGadget) writeGadgetConfig() error { return err } - u.log.Tracef("writing gadget config") + u.log.Trace().Msg("writing gadget config") for _, val := range u.getOrderedConfigItems() { key := val.key item := val.item // check if the item is enabled in the config if !u.isGadgetConfigItemEnabled(key) { - u.log.Tracef("disabling gadget config: %s", key) + u.log.Trace().Str("key", key).Msg("disabling gadget config") err = u.disableGadgetItemConfig(item) if err != nil { return err } continue } - u.log.Tracef("writing gadget config: %s", key) + u.log.Trace().Str("key", key).Msg("writing gadget config") err = u.writeGadgetItemConfig(item) if err != nil { return err @@ -243,12 +243,12 @@ func (u *UsbGadget) writeGadgetConfig() error { } if err = u.writeUDC(); err != nil { - u.log.Errorf("failed to write UDC: %v", err) + u.log.Error().Err(err).Msg("failed to write UDC") return err } if err = u.rebindUsb(true); err != nil { - u.log.Infof("failed to rebind usb: %v", err) + u.log.Info().Err(err).Msg("failed to rebind usb") } return nil @@ -263,7 +263,7 @@ func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error { configPath := joinPath(u.configC1Path, item.configPath) if _, err := os.Lstat(configPath); os.IsNotExist(err) { - u.log.Tracef("symlink %s does not exist", item.configPath) + u.log.Trace().Str("path", configPath).Msg("symlink does not exist") return nil } @@ -315,7 +315,7 @@ func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error { // create symlink if configPath is set if item.configPath != nil && item.configAttrs == nil { configPath := joinPath(u.configC1Path, item.configPath) - u.log.Tracef("Creating symlink from %s to %s", configPath, gadgetItemPath) + u.log.Trace().Str("source", configPath).Str("target", gadgetItemPath).Msg("creating symlink") if err := ensureSymlink(configPath, gadgetItemPath); err != nil { return err } diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 030f7af..de007e4 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -65,7 +65,7 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { _, err := u.keyboardHidFile.Write(data) if err != nil { - u.log.Errorf("failed to write to hidg0: %w", err) + u.log.Error().Err(err).Msg("failed to write to hidg0") u.keyboardHidFile.Close() u.keyboardHidFile = nil return err diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index c59b591..de77b1e 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -73,7 +73,7 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { _, err := u.absMouseHidFile.Write(data) if err != nil { - u.log.Errorf("failed to write to hidg1: %w", err) + u.log.Error().Err(err).Msg("failed to write to hidg1") u.absMouseHidFile.Close() u.absMouseHidFile = nil return err diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index df844dc..af2d028 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -65,7 +65,7 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { _, err := u.relMouseHidFile.Write(data) if err != nil { - u.log.Errorf("failed to write to hidg2: %w", err) + u.log.Error().Err(err).Msg("failed to write to hidg2") u.relMouseHidFile.Close() u.relMouseHidFile = nil return err diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go index 6316b83..84dfbe4 100644 --- a/internal/usbgadget/udc.go +++ b/internal/usbgadget/udc.go @@ -38,7 +38,7 @@ func rebindUsb(udc string, ignoreUnbindError bool) error { } func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error { - u.log.Infof("rebinding USB gadget to UDC %s", u.udc) + u.log.Info().Str("udc", u.udc).Msg("rebinding USB gadget to UDC") return rebindUsb(u.udc, ignoreUnbindError) } @@ -53,7 +53,7 @@ func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error { func (u *UsbGadget) writeUDC() error { path := path.Join(u.kvmGadgetPath, "UDC") - u.log.Tracef("writing UDC %s to %s", u.udc, path) + u.log.Trace().Str("udc", u.udc).Str("path", path).Msg("writing UDC") err := u.writeIfDifferent(path, []byte(u.udc), 0644) if err != nil { return fmt.Errorf("failed to write UDC: %w", err) @@ -70,7 +70,7 @@ func (u *UsbGadget) GetUsbState() (state string) { if os.IsNotExist(err) { return "not attached" } else { - u.log.Tracef("failed to read usb state: %v", err) + u.log.Trace().Err(err).Msg("failed to read usb state") } return "unknown" } diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index 9fc34d5..1dff2f3 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/pion/logging" + "github.com/rs/zerolog" ) // Devices is a struct that represents the USB devices that can be enabled on a USB gadget. @@ -63,16 +63,16 @@ type UsbGadget struct { lastUserInput time.Time - log logging.LeveledLogger + log *zerolog.Logger } const configFSPath = "/sys/kernel/config" const gadgetPath = "/sys/kernel/config/usb_gadget" -var defaultLogger = logging.NewDefaultLoggerFactory().NewLogger("usbgadget") +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) // NewUsbGadget creates a new UsbGadget. -func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *logging.LeveledLogger) *UsbGadget { +func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget { if logger == nil { logger = &defaultLogger } @@ -97,12 +97,12 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger * relMouseLock: sync.Mutex{}, enabledDevices: *enabledDevices, lastUserInput: time.Now(), - log: *logger, + log: logger, absMouseAccumulatedWheelY: 0, } if err := g.Init(); err != nil { - g.log.Errorf("failed to init USB gadget: %v", err) + logger.Error().Err(err).Msg("failed to init USB gadget") return nil } diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go index 3f0adda..0e796c8 100644 --- a/internal/usbgadget/utils.go +++ b/internal/usbgadget/utils.go @@ -45,18 +45,18 @@ func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode o oldContent, err := os.ReadFile(filePath) if err == nil { if bytes.Equal(oldContent, content) { - u.log.Tracef("skipping writing to %s as it already has the correct content", filePath) + u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content") return nil } if len(oldContent) == len(content)+1 && bytes.Equal(oldContent[:len(content)], content) && oldContent[len(content)] == 10 { - u.log.Tracef("skipping writing to %s as it already has the correct content", filePath) + u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content") return nil } - u.log.Tracef("writing to %s as it has different content old%v new%v", filePath, oldContent, content) + u.log.Trace().Str("path", filePath).Bytes("old", oldContent).Bytes("new", content).Msg("writing to as it has different content") } } return os.WriteFile(filePath, content, permMode) diff --git a/jiggler.go b/jiggler.go index daec192..94f7d15 100644 --- a/jiggler.go +++ b/jiggler.go @@ -28,11 +28,11 @@ func runJiggler() { //TODO: change to rel mouse err := rpcAbsMouseReport(1, 1, 0) if err != nil { - logger.Warnf("Failed to jiggle mouse: %v", err) + logger.Warn().Err(err).Msg("Failed to jiggle mouse") } err = rpcAbsMouseReport(0, 0, 0) if err != nil { - logger.Warnf("Failed to reset mouse position: %v", err) + logger.Warn().Err(err).Msg("Failed to reset mouse position") } } } diff --git a/jsonrpc.go b/jsonrpc.go index e5deb49..de29e08 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -47,12 +47,12 @@ type BacklightSettings struct { func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { responseBytes, err := json.Marshal(response) if err != nil { - logger.Warnf("Error marshalling JSONRPC response: %v", err) + logger.Warn().Err(err).Msg("Error marshalling JSONRPC response") return } err = session.RPCChannel.SendText(string(responseBytes)) if err != nil { - logger.Warnf("Error sending JSONRPC response: %v", err) + logger.Warn().Err(err).Msg("Error sending JSONRPC response") return } } @@ -65,16 +65,16 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) { } requestBytes, err := json.Marshal(request) if err != nil { - logger.Warnf("Error marshalling JSONRPC event: %v", err) + logger.Warn().Err(err).Msg("Error marshalling JSONRPC event") return } if session == nil || session.RPCChannel == nil { - logger.Info("RPC channel not available") + logger.Info().Msg("RPC channel not available") return } err = session.RPCChannel.SendText(string(requestBytes)) if err != nil { - logger.Warnf("Error sending JSONRPC event: %v", err) + logger.Warn().Err(err).Msg("Error sending JSONRPC event") return } } @@ -148,7 +148,7 @@ func rpcGetStreamQualityFactor() (float64, error) { } func rpcSetStreamQualityFactor(factor float64) error { - logger.Infof("Setting stream quality factor to: %f", factor) + logger.Info().Float64("factor", factor).Msg("Setting stream quality factor") var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor}) if err != nil { return err @@ -184,10 +184,10 @@ func rpcGetEDID() (string, error) { func rpcSetEDID(edid string) error { if edid == "" { - logger.Info("Restoring EDID to default") + logger.Info().Msg("Restoring EDID to default") edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" } else { - logger.Infof("Setting EDID to: %s", edid) + logger.Info().Str("edid", edid).Msg("Setting EDID") } _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid}) if err != nil { @@ -227,7 +227,7 @@ func rpcTryUpdate() error { go func() { err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Warnf("failed to try update: %v", err) + logger.Warn().Err(err).Msg("failed to try update") } }() return nil @@ -257,7 +257,7 @@ func rpcSetBacklightSettings(params BacklightSettings) error { return fmt.Errorf("failed to save config: %w", err) } - logger.Infof("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec) + logger.Info().Int("max_brightness", config.DisplayMaxBrightness).Int("dim_after", config.DisplayDimAfterSec).Int("off_after", config.DisplayOffAfterSec).Msg("rpc: display: settings applied") // If the device started up with auto-dim and/or auto-off set to zero, the display init // method will not have started the tickers. So in case that has changed, attempt to start the tickers now. @@ -318,7 +318,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to create devmode file: %w", err) } } else { - logger.Debug("dev mode already enabled") + logger.Debug().Msg("dev mode already enabled") return nil } } else { @@ -327,7 +327,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to remove devmode file: %w", err) } } else if os.IsNotExist(err) { - logger.Debug("dev mode already disabled") + logger.Debug().Msg("dev mode already disabled") return nil } else { return fmt.Errorf("error checking dev mode file: %w", err) @@ -337,7 +337,7 @@ func rpcSetDevModeState(enabled bool) error { cmd := exec.Command("dropbear.sh") output, err := cmd.CombinedOutput() if err != nil { - logger.Warnf("Failed to start/stop SSH: %v, %v", err, output) + logger.Warn().Err(err).Bytes("output", output).Msg("Failed to start/stop SSH") return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect") } @@ -478,23 +478,23 @@ type RPCHandler struct { } func rpcSetMassStorageMode(mode string) (string, error) { - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Info().Str("mode", mode).Msg("Setting mass storage mode") var cdrom bool if mode == "cdrom" { cdrom = true } else if mode != "file" { - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode) + logger.Info().Str("mode", mode).Msg("Invalid mode provided") return "", fmt.Errorf("invalid mode: %s", mode) } - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Info().Str("mode", mode).Msg("Setting mass storage mode") err := setMassStorageMode(cdrom) if err != nil { return "", fmt.Errorf("failed to set mass storage mode: %w", err) } - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode) + logger.Info().Str("mode", mode).Msg("Mass storage mode set") // Get the updated mode after setting return rpcGetMassStorageMode() @@ -563,7 +563,7 @@ func rpcResetConfig() error { return fmt.Errorf("failed to reset config: %w", err) } - logger.Info("Configuration reset to default") + logger.Info().Msg("Configuration reset to default") return nil } @@ -579,7 +579,7 @@ func rpcGetDCPowerState() (DCPowerState, error) { } func rpcSetDCPowerState(enabled bool) error { - logger.Infof("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled) + logger.Info().Bool("enabled", enabled).Msg("Setting DC power state") err := setDCPowerState(enabled) if err != nil { return fmt.Errorf("failed to set DC power state: %w", err) @@ -613,16 +613,16 @@ func rpcSetActiveExtension(extensionId string) error { } func rpcSetATXPowerAction(action string) error { - logger.Debugf("[jsonrpc.go:rpcSetATXPowerAction] Executing ATX power action: %s", action) + logger.Debug().Str("action", action).Msg("Executing ATX power action") switch action { case "power-short": - logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating short power button press") + logger.Debug().Msg("Simulating short power button press") return pressATXPowerButton(200 * time.Millisecond) case "power-long": - logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating long power button press") + logger.Debug().Msg("Simulating long power button press") return pressATXPowerButton(5 * time.Second) case "reset": - logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating reset button press") + logger.Debug().Msg("Simulating reset button press") return pressATXResetButton(200 * time.Millisecond) default: return fmt.Errorf("invalid action: %s", action) diff --git a/log.go b/log.go index 0d36c0d..6824a3f 100644 --- a/log.go +++ b/log.go @@ -1,9 +1,169 @@ package kvm -import "github.com/pion/logging" +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "time" -// we use logging framework from pion -// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC -var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm") -var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud") -var websocketLogger = logging.NewDefaultLoggerFactory().NewLogger("websocket") + "github.com/pion/logging" + "github.com/rs/zerolog" +) + +var ( + defaultLogOutput io.Writer = zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + PartsOrder: []string{"time", "level", "scope", "component", "message"}, + FieldsExclude: []string{"scope", "component"}, + FormatPartValueByName: func(value interface{}, name string) string { + val := fmt.Sprintf("%s", value) + if name == "component" { + if value == nil { + return "-" + } + } + return val + }, + } + defaultLogLevel = zerolog.ErrorLevel + rootLogger = zerolog.New(defaultLogOutput).With(). + Str("scope", "jetkvm"). + Timestamp(). + Stack(). + Logger() +) + +var ( + scopeLevels map[string]zerolog.Level + scopeLevelMutex = sync.Mutex{} +) + +var ( + logger = getLogger("jetkvm") + cloudLogger = getLogger("cloud") + websocketLogger = getLogger("websocket") + nativeLogger = getLogger("native") + ntpLogger = getLogger("ntp") + displayLogger = getLogger("display") + usbLogger = getLogger("usb") + ginLogger = getLogger("gin") +) + +func updateLogLevel() { + scopeLevelMutex.Lock() + defer scopeLevelMutex.Unlock() + + logLevels := map[string]zerolog.Level{ + "DISABLE": zerolog.Disabled, + "NOLEVEL": zerolog.NoLevel, + "PANIC": zerolog.PanicLevel, + "FATAL": zerolog.FatalLevel, + "ERROR": zerolog.ErrorLevel, + "WARN": zerolog.WarnLevel, + "INFO": zerolog.InfoLevel, + "DEBUG": zerolog.DebugLevel, + "TRACE": zerolog.TraceLevel, + } + + scopeLevels = make(map[string]zerolog.Level) + + for name, level := range logLevels { + env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) + + if env == "" { + env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) + } + + if env == "" { + env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) + } + + if env == "" { + continue + } + + if strings.ToLower(env) == "all" { + if defaultLogLevel > level { + defaultLogLevel = level + } + + continue + } + + scopes := strings.Split(strings.ToLower(env), ",") + for _, scope := range scopes { + scopeLevels[scope] = level + } + } +} + +func getLogger(scope string) zerolog.Logger { + if scopeLevels == nil { + updateLogLevel() + } + + l := rootLogger.With().Str("component", scope).Logger() + + // if the scope is not in the map, use the default level from the root logger + if level, ok := scopeLevels[scope]; ok { + return l.Level(level) + } + + return l.Level(defaultLogLevel) +} + +type pionLogger struct { + logger *zerolog.Logger +} + +// Print all messages except trace. +func (c pionLogger) Trace(msg string) { + c.logger.Trace().Msg(msg) +} +func (c pionLogger) Tracef(format string, args ...interface{}) { + c.logger.Trace().Msgf(format, args...) +} + +func (c pionLogger) Debug(msg string) { + c.logger.Debug().Msg(msg) +} +func (c pionLogger) Debugf(format string, args ...interface{}) { + c.logger.Debug().Msgf(format, args...) +} +func (c pionLogger) Info(msg string) { + c.logger.Info().Msg(msg) +} +func (c pionLogger) Infof(format string, args ...interface{}) { + c.logger.Info().Msgf(format, args...) +} +func (c pionLogger) Warn(msg string) { + c.logger.Warn().Msg(msg) +} +func (c pionLogger) Warnf(format string, args ...interface{}) { + c.logger.Warn().Msgf(format, args...) +} +func (c pionLogger) Error(msg string) { + c.logger.Error().Msg(msg) +} +func (c pionLogger) Errorf(format string, args ...interface{}) { + c.logger.Error().Msgf(format, args...) +} + +// customLoggerFactory satisfies the interface logging.LoggerFactory +// This allows us to create different loggers per subsystem. So we can +// add custom behavior. +type pionLoggerFactory struct{} + +func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { + logger := getLogger(subsystem).With(). + Str("scope", "pion"). + Str("component", subsystem). + Logger() + + return pionLogger{logger: &logger} +} + +var defaultLoggerFactory = &pionLoggerFactory{} diff --git a/main.go b/main.go index aeb3d85..98748c7 100644 --- a/main.go +++ b/main.go @@ -17,17 +17,17 @@ func Main() { var cancel context.CancelFunc appCtx, cancel = context.WithCancel(context.Background()) defer cancel() - logger.Info("Starting JetKvm") + logger.Info().Msg("Starting JetKvm") go runWatchdog() go confirmCurrentSystem() http.DefaultClient.Timeout = 1 * time.Minute LoadConfig() - logger.Debug("config loaded") + logger.Debug().Msg("config loaded") err := rootcerts.UpdateDefaultTransport() if err != nil { - logger.Errorf("failed to load CA certs: %v", err) + logger.Warn().Err(err).Msg("failed to load CA certs") } go TimeSyncLoop() @@ -40,7 +40,7 @@ func Main() { go func() { err = ExtractAndRunNativeBin() if err != nil { - logger.Errorf("failed to extract and run native bin: %v", err) + logger.Warn().Err(err).Msg("failed to extract and run native bin") //TODO: prepare an error message screen buffer to show on kvm screen } }() @@ -50,19 +50,19 @@ func Main() { go func() { time.Sleep(15 * time.Minute) for { - logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled) + logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING") if !config.AutoUpdateEnabled { return } if currentSession != nil { - logger.Debugf("skipping update since a session is active") + logger.Debug().Msg("skipping update since a session is active") time.Sleep(1 * time.Minute) continue } includePreRelease := config.IncludePreRelease err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Errorf("failed to auto update: %v", err) + logger.Warn().Err(err).Msg("failed to auto update") } time.Sleep(1 * time.Hour) } @@ -79,7 +79,7 @@ func Main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - logger.Info("JetKVM Shutting Down") + logger.Info().Msg("JetKVM Shutting Down") //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { diff --git a/native.go b/native.go index 8960304..630093b 100644 --- a/native.go +++ b/native.go @@ -13,6 +13,7 @@ import ( "time" "github.com/jetkvm/kvm/resource" + "github.com/rs/zerolog" "github.com/pion/webrtc/v4/pkg/media" ) @@ -34,6 +35,19 @@ type CtrlResponse struct { Data json.RawMessage `json:"data,omitempty"` } +type nativeOutput struct { + mu *sync.Mutex + logger *zerolog.Event +} + +func (w *nativeOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.logger.Msg(string(p)) + return len(p), nil +} + type EventHandler func(event CtrlResponse) var seq int32 = 1 @@ -61,7 +75,7 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse return nil, fmt.Errorf("error marshaling ctrl action: %w", err) } - logger.Infof("sending ctrl action: %s", string(jsonData)) + nativeLogger.Info().Str("action", ctrlAction.Action).Msg("sending ctrl action") err = WriteCtrlMessage(jsonData) if err != nil { @@ -104,28 +118,28 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC // Remove the socket file if it already exists if _, err := os.Stat(socketPath); err == nil { if err := os.Remove(socketPath); err != nil { - logger.Errorf("Failed to remove existing socket file %s: %v", socketPath, err) + nativeLogger.Warn().Err(err).Str("socket_path", socketPath).Msg("Failed to remove existing socket file") os.Exit(1) } } listener, err := net.Listen("unixpacket", socketPath) if err != nil { - logger.Errorf("Failed to start server on %s: %v", socketPath, err) + nativeLogger.Warn().Err(err).Str("socket_path", socketPath).Msg("Failed to start server") os.Exit(1) } - logger.Infof("Server listening on %s", socketPath) + nativeLogger.Info().Str("socket_path", socketPath).Msg("Server listening") go func() { conn, err := listener.Accept() listener.Close() if err != nil { - logger.Errorf("failed to accept sock: %v", err) + nativeLogger.Warn().Err(err).Str("socket_path", socketPath).Msg("failed to accept sock") } if isCtrl { close(ctrlClientConnected) - logger.Debug("first native ctrl socket client connected") + nativeLogger.Debug().Msg("first native ctrl socket client connected") } handleClient(conn) }() @@ -135,20 +149,20 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC func StartNativeCtrlSocketServer() { nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) - logger.Debug("native app ctrl sock started") + nativeLogger.Debug().Msg("native app ctrl sock started") } func StartNativeVideoSocketServer() { nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) - logger.Debug("native app video sock started") + nativeLogger.Debug().Msg("native app video sock started") } func handleCtrlClient(conn net.Conn) { defer conn.Close() - logger.Debug("native socket client connected") + nativeLogger.Debug().Msg("native socket client connected") if ctrlSocketConn != nil { - logger.Debugf("closing existing native socket connection") + nativeLogger.Debug().Msg("closing existing native socket connection") ctrlSocketConn.Close() } @@ -161,17 +175,19 @@ func handleCtrlClient(conn net.Conn) { for { n, err := conn.Read(readBuf) if err != nil { - logger.Errorf("error reading from ctrl sock: %v", err) + nativeLogger.Warn().Err(err).Msg("error reading from ctrl sock") break } readMsg := string(readBuf[:n]) - logger.Tracef("ctrl sock msg: %v", readMsg) + ctrlResp := CtrlResponse{} err = json.Unmarshal([]byte(readMsg), &ctrlResp) if err != nil { - logger.Warnf("error parsing ctrl sock msg: %v", err) + nativeLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg") continue } + nativeLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg") + if ctrlResp.Seq != 0 { responseChan, ok := ongoingRequests[ctrlResp.Seq] if ok { @@ -184,20 +200,20 @@ func handleCtrlClient(conn net.Conn) { } } - logger.Debug("ctrl sock disconnected") + nativeLogger.Debug().Msg("ctrl sock disconnected") } func handleVideoClient(conn net.Conn) { defer conn.Close() - logger.Infof("Native video socket client connected: %v", conn.RemoteAddr()) + nativeLogger.Info().Str("addr", conn.RemoteAddr().String()).Msg("Native video socket client connected") inboundPacket := make([]byte, maxFrameSize) lastFrame := time.Now() for { n, err := conn.Read(inboundPacket) if err != nil { - logger.Warnf("error during read: %v", err) + nativeLogger.Warn().Err(err).Msg("error during read") return } now := time.Now() @@ -206,7 +222,7 @@ func handleVideoClient(conn net.Conn) { if currentSession != nil { err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) if err != nil { - logger.Warnf("error writing sample: %v", err) + nativeLogger.Warn().Err(err).Msg("error writing sample") } } } @@ -225,9 +241,19 @@ func ExtractAndRunNativeBin() error { // Run the binary in the background cmd := exec.Command(binaryPath) + nativeOutputLock := sync.Mutex{} + nativeStdout := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stdout"), + } + nativeStderr := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stderr"), + } + // Redirect stdout and stderr to the current process - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + cmd.Stdout = nativeStdout + cmd.Stderr = nativeStderr // Set the process group ID so we can kill the process and its children when this process exits cmd.SysProcAttr = &syscall.SysProcAttr{ @@ -243,28 +269,28 @@ func ExtractAndRunNativeBin() error { //TODO: add auto restart go func() { <-appCtx.Done() - logger.Infof("killing process PID: %d", cmd.Process.Pid) + nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") err := cmd.Process.Kill() if err != nil { - logger.Errorf("failed to kill process: %v", err) + nativeLogger.Warn().Err(err).Msg("failed to kill process") return } }() - logger.Infof("Binary started with PID: %d", cmd.Process.Pid) + nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("Binary started") return nil } func shouldOverwrite(destPath string, srcHash []byte) bool { if srcHash == nil { - logger.Debug("error reading embedded jetkvm_native.sha256, doing overwriting") + nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting") return true } dstHash, err := os.ReadFile(destPath + ".sha256") if err != nil { - logger.Debug("error reading existing jetkvm_native.sha256, doing overwriting") + nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting") return true } @@ -280,13 +306,13 @@ func ensureBinaryUpdated(destPath string) error { srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") if err != nil { - logger.Debug("error reading embedded jetkvm_native.sha256, proceeding with update") + nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update") srcHash = nil } _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - logger.Info("writing jetkvm_native") + nativeLogger.Info().Msg("writing jetkvm_native") _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { @@ -303,7 +329,7 @@ func ensureBinaryUpdated(destPath string) error { return err } } - logger.Info("jetkvm_native updated") + nativeLogger.Info().Msg("jetkvm_native updated") } return nil @@ -313,10 +339,10 @@ func ensureBinaryUpdated(destPath string) error { // Called after successful connection to jetkvm_native. func restoreHdmiEdid() { if config.EdidString != "" { - logger.Infof("Restoring HDMI EDID to %v", config.EdidString) + nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) if err != nil { - logger.Errorf("Failed to restore HDMI EDID: %v", err) + nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") } } } diff --git a/network.go b/network.go index 66b8616..4051c06 100644 --- a/network.go +++ b/network.go @@ -56,14 +56,14 @@ func setDhcpClientState(active bool) { cmd := exec.Command("/usr/bin/killall", signal, "udhcpc") if err := cmd.Run(); err != nil { - logger.Warnf("network: setDhcpClientState: failed to change udhcpc state: %s", err) + logger.Warn().Err(err).Msg("network: setDhcpClientState: failed to change udhcpc state") } } func checkNetworkState() { iface, err := netlink.LinkByName(NetIfName) if err != nil { - logger.Warnf("failed to get [%s] interface: %v", NetIfName, err) + logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get interface") return } @@ -76,7 +76,7 @@ func checkNetworkState() { addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL) if err != nil { - logger.Warnf("failed to get addresses for [%s]: %v", NetIfName, err) + logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get addresses") } // If the link is going down, put udhcpc into idle mode. @@ -89,10 +89,10 @@ func checkNetworkState() { if addr.IP.To4() != nil { if !newState.Up && networkState.Up { // If the network is going down, remove all IPv4 addresses from the interface. - logger.Infof("network: state transitioned to down, removing IPv4 address %s", addr.IP.String()) + logger.Info().Str("address", addr.IP.String()).Msg("network: state transitioned to down, removing IPv4 address") err := netlink.AddrDel(iface, &addr) if err != nil { - logger.Warnf("network: failed to delete %s", addr.IP.String()) + logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("network: failed to delete address") } newState.IPv4 = "..." @@ -105,7 +105,7 @@ func checkNetworkState() { } if newState != networkState { - logger.Info("network state changed") + logger.Info().Msg("network state changed") // restart MDNS _ = startMDNS() networkState = newState @@ -116,15 +116,15 @@ func checkNetworkState() { func startMDNS() error { // If server was previously running, stop it if mDNSConn != nil { - logger.Info("Stopping mDNS server") + logger.Info().Msg("Stopping mDNS server") err := mDNSConn.Close() if err != nil { - logger.Warnf("failed to stop mDNS server: %v", err) + logger.Warn().Err(err).Msg("failed to stop mDNS server") } } // Start a new server - logger.Info("Starting mDNS server on jetkvm.local") + logger.Info().Msg("Starting mDNS server on jetkvm.local") addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { return err @@ -146,7 +146,8 @@ func startMDNS() error { } mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ - LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable + LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable + LoggerFactory: defaultLoggerFactory, }) if err != nil { mDNSConn = nil @@ -181,7 +182,7 @@ func getNTPServersFromDHCPInfo() ([]string, error) { for _, server := range strings.Fields(val) { if net.ParseIP(server) == nil { - logger.Infof("invalid NTP server IP: %s, ignoring", server) + logger.Info().Str("server", server).Msg("invalid NTP server IP, ignoring") } servers = append(servers, server) } @@ -196,7 +197,7 @@ func init() { done := make(chan struct{}) if err := netlink.LinkSubscribe(updates, done); err != nil { - logger.Warnf("failed to subscribe to link updates: %v", err) + logger.Warn().Err(err).Msg("failed to subscribe to link updates") return } @@ -210,7 +211,7 @@ func init() { select { case update := <-updates: if update.Link.Attrs().Name == NetIfName { - logger.Infof("link update: %+v", update) + logger.Info().Interface("update", update).Msg("link update") checkNetworkState() } case <-ticker.C: @@ -222,6 +223,6 @@ func init() { }() err := startMDNS() if err != nil { - logger.Warnf("failed to run mDNS: %v", err) + logger.Warn().Err(err).Msg("failed to run mDNS") } } diff --git a/ntp.go b/ntp.go index 27ec100..aa7d17a 100644 --- a/ntp.go +++ b/ntp.go @@ -32,13 +32,13 @@ var ( func isTimeSyncNeeded() bool { if builtTimestamp == "" { - logger.Warnf("Built timestamp is not set, time sync is needed") + ntpLogger.Warn().Msg("Built timestamp is not set, time sync is needed") return true } ts, err := strconv.Atoi(builtTimestamp) if err != nil { - logger.Warnf("Failed to parse built timestamp: %v", err) + ntpLogger.Warn().Str("error", err.Error()).Msg("Failed to parse built timestamp") return true } @@ -46,10 +46,10 @@ func isTimeSyncNeeded() bool { builtTime := time.Unix(int64(ts), 0) now := time.Now() - logger.Tracef("Built time: %v, now: %v", builtTime, now) + ntpLogger.Debug().Str("built_time", builtTime.Format(time.RFC3339)).Str("now", now.Format(time.RFC3339)).Msg("Built time and now") if now.Sub(builtTime) < 0 { - logger.Warnf("System time is behind the built time, time sync is needed") + ntpLogger.Warn().Msg("System time is behind the built time, time sync is needed") return true } @@ -64,7 +64,7 @@ func TimeSyncLoop() { } if !networkState.Up { - logger.Infof("Waiting for network to come up") + ntpLogger.Info().Msg("Waiting for network to come up") time.Sleep(timeSyncWaitNetUpInt) continue } @@ -72,11 +72,11 @@ func TimeSyncLoop() { // check if time sync is needed, but do nothing for now isTimeSyncNeeded() - logger.Infof("Syncing system time") + ntpLogger.Info().Msg("Syncing system time") start := time.Now() err := SyncSystemTime() if err != nil { - logger.Warnf("Failed to sync system time: %v", err) + ntpLogger.Error().Str("error", err.Error()).Msg("Failed to sync system time") // retry after a delay timeSyncRetryInterval += timeSyncRetryStep @@ -89,7 +89,9 @@ func TimeSyncLoop() { continue } timeSyncSuccess = true - logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start)) + ntpLogger.Info().Str("now", time.Now().Format(time.RFC3339)). + Str("time_taken", time.Since(start).String()). + Msg("Time sync successful") time.Sleep(timeSyncInterval) // after the first sync is done } } @@ -109,20 +111,20 @@ func SyncSystemTime() (err error) { func queryNetworkTime() (*time.Time, error) { ntpServers, err := getNTPServersFromDHCPInfo() if err != nil { - logger.Warnf("failed to get NTP servers from DHCP info: %v\n", err) + ntpLogger.Error().Str("error", err.Error()).Msg("failed to get NTP servers from DHCP info") } if ntpServers == nil { ntpServers = defaultNTPServers - logger.Infof("Using default NTP servers: %v\n", ntpServers) + ntpLogger.Info().Str("ntp_servers", fmt.Sprintf("%v", ntpServers)).Msg("Using default NTP servers") } else { - logger.Infof("Using NTP servers from DHCP: %v\n", ntpServers) + ntpLogger.Info().Str("ntp_servers", fmt.Sprintf("%v", ntpServers)).Msg("Using NTP servers from DHCP") } for _, server := range ntpServers { now, err := queryNtpServer(server, timeSyncTimeout) if err == nil { - logger.Infof("NTP server [%s] returned time: %v\n", server, now) + ntpLogger.Info().Str("ntp_server", server).Str("time", now.Format(time.RFC3339)).Msg("NTP server returned time") return now, nil } } @@ -133,9 +135,11 @@ func queryNetworkTime() (*time.Time, error) { for _, url := range httpUrls { now, err := queryHttpTime(url, timeSyncTimeout) if err == nil { + ntpLogger.Info().Str("http_url", url).Str("time", now.Format(time.RFC3339)).Msg("HTTP server returned time") return now, nil } } + ntpLogger.Error().Msg("failed to query network time") return nil, errors.New("failed to query network time") } diff --git a/ota.go b/ota.go index b28abbb..64d7a26 100644 --- a/ota.go +++ b/ota.go @@ -76,7 +76,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) updateUrl.RawQuery = query.Encode() - logger.Infof("Checking for updates at: %s", updateUrl) + logger.Info().Str("url", updateUrl.String()).Msg("Checking for updates") req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil) if err != nil { @@ -235,7 +235,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error } hashSum := hash.Sum(nil) - logger.Infof("SHA256 hash of %s: %x", path, hashSum) + logger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") if hex.EncodeToString(hashSum) != expectedHash { return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) @@ -277,7 +277,7 @@ var otaState = OTAState{} func triggerOTAStateUpdate() { go func() { if currentSession == nil { - logger.Info("No active RPC session, skipping update state update") + logger.Info().Msg("No active RPC session, skipping update state update") return } writeJSONRPCEvent("otaState", otaState, currentSession) @@ -285,7 +285,7 @@ func triggerOTAStateUpdate() { } func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { - logger.Info("Trying to update...") + logger.Info().Msg("Trying to update...") if otaState.Updating { return fmt.Errorf("update already in progress") } @@ -320,7 +320,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err rebootNeeded := false if appUpdateAvailable { - logger.Infof("App update available: %s -> %s", local.AppVersion, remote.AppVersion) + logger.Info().Str("local", local.AppVersion).Str("remote", remote.AppVersion).Msg("App update available") err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) if err != nil { @@ -346,14 +346,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.AppUpdateProgress = 1 triggerOTAStateUpdate() - logger.Info("App update downloaded") + logger.Info().Msg("App update downloaded") rebootNeeded = true } else { - logger.Info("App is up to date") + logger.Info().Msg("App is up to date") } if systemUpdateAvailable { - logger.Infof("System update available: %s -> %s", local.SystemVersion, remote.SystemVersion) + logger.Info().Str("local", local.SystemVersion).Str("remote", remote.SystemVersion).Msg("System update available") + err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) @@ -371,7 +372,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err triggerOTAStateUpdate() return err } - logger.Info("System update downloaded") + logger.Info().Msg("System update downloaded") verifyFinished := time.Now() otaState.SystemVerifiedAt = &verifyFinished otaState.SystemVerificationProgress = 1 @@ -418,17 +419,17 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) } - logger.Infof("rk_ota success, output: %s", output) + logger.Info().Str("output", output).Msg("rk_ota success") otaState.SystemUpdateProgress = 1 otaState.SystemUpdatedAt = &verifyFinished triggerOTAStateUpdate() rebootNeeded = true } else { - logger.Info("System is up to date") + logger.Info().Msg("System is up to date") } if rebootNeeded { - logger.Info("System Rebooting in 10s") + logger.Info().Msg("System Rebooting in 10s") time.Sleep(10 * time.Second) cmd := exec.Command("reboot") err := cmd.Start() @@ -503,6 +504,6 @@ func IsUpdatePending() bool { func confirmCurrentSystem() { output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() if err != nil { - logger.Warnf("failed to set current partition in A/B setup: %s", string(output)) + logger.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup") } } diff --git a/remote_mount.go b/remote_mount.go index 5b10695..befffcb 100644 --- a/remote_mount.go +++ b/remote_mount.go @@ -44,7 +44,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( return nil, errors.New("not active session") } - logger.Debugf("reading from webrtc %v", string(jsonBytes)) + logger.Debug().Str("request", string(jsonBytes)).Msg("reading from webrtc") err = currentSession.DiskChannel.SendText(string(jsonBytes)) if err != nil { return nil, err diff --git a/serial.go b/serial.go index 31fd553..732c022 100644 --- a/serial.go +++ b/serial.go @@ -39,13 +39,13 @@ func runATXControl() { for { line, err := reader.ReadString('\n') if err != nil { - logger.Errorf("Error reading from serial port: %v", err) + logger.Warn().Err(err).Msg("Error reading from serial port") return } // Each line should be 4 binary digits + newline if len(line) != 5 { - logger.Warnf("Invalid line length: %d", len(line)) + logger.Warn().Int("length", len(line)).Msg("Invalid line length") continue } @@ -66,8 +66,7 @@ func runATXControl() { newLedPWRState != ledPWRState || newBtnRSTState != btnRSTState || newBtnPWRState != btnPWRState { - logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v", - newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState) + logger.Debug().Bool("hdd", newLedHDDState).Bool("pwr", newLedPWRState).Bool("rst", newBtnRSTState).Bool("pwr", newBtnPWRState).Msg("Status changed") // Update states ledHDDState = newLedHDDState @@ -138,41 +137,41 @@ func runDCControl() { for { line, err := reader.ReadString('\n') if err != nil { - logger.Errorf("Error reading from serial port: %v", err) + logger.Warn().Err(err).Msg("Error reading from serial port") return } // Split the line by semicolon parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) != 4 { - logger.Warnf("Invalid line: %s", line) + logger.Warn().Str("line", line).Msg("Invalid line") continue } // Parse new states powerState, err := strconv.Atoi(parts[0]) if err != nil { - logger.Warnf("Invalid power state: %v", err) + logger.Warn().Err(err).Msg("Invalid power state") continue } dcState.IsOn = powerState == 1 milliVolts, err := strconv.ParseFloat(parts[1], 64) if err != nil { - logger.Warnf("Invalid voltage: %v", err) + logger.Warn().Err(err).Msg("Invalid voltage") continue } volts := milliVolts / 1000 // Convert mV to V milliAmps, err := strconv.ParseFloat(parts[2], 64) if err != nil { - logger.Warnf("Invalid current: %v", err) + logger.Warn().Err(err).Msg("Invalid current") continue } amps := milliAmps / 1000 // Convert mA to A milliWatts, err := strconv.ParseFloat(parts[3], 64) if err != nil { - logger.Warnf("Invalid power: %v", err) + logger.Warn().Err(err).Msg("Invalid power") continue } watts := milliWatts / 1000 // Convert mW to W @@ -226,7 +225,7 @@ func reopenSerialPort() error { var err error port, err = serial.Open(serialPortPath, defaultMode) if err != nil { - logger.Errorf("Error opening serial port: %v", err) + logger.Warn().Err(err).Msg("Error opening serial port") } return nil } @@ -239,13 +238,13 @@ func handleSerialChannel(d *webrtc.DataChannel) { n, err := port.Read(buf) if err != nil { if err != io.EOF { - logger.Errorf("Failed to read from serial port: %v", err) + logger.Warn().Err(err).Msg("Failed to read from serial port") } break } err = d.Send(buf[:n]) if err != nil { - logger.Errorf("Failed to send serial output: %v", err) + logger.Warn().Err(err).Msg("Failed to send serial output") break } } @@ -258,7 +257,7 @@ func handleSerialChannel(d *webrtc.DataChannel) { } _, err := port.Write(msg.Data) if err != nil { - logger.Errorf("Failed to write to serial: %v", err) + logger.Warn().Err(err).Msg("Failed to write to serial") } }) diff --git a/terminal.go b/terminal.go index 3e64020..2200064 100644 --- a/terminal.go +++ b/terminal.go @@ -23,7 +23,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) { var err error ptmx, err = pty.Start(cmd) if err != nil { - logger.Errorf("Failed to start pty: %v", err) + logger.Warn().Err(err).Msg("Failed to start pty") d.Close() return } @@ -34,13 +34,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) { n, err := ptmx.Read(buf) if err != nil { if err != io.EOF { - logger.Errorf("Failed to read from pty: %v", err) + logger.Warn().Err(err).Msg("Failed to read from pty") } break } err = d.Send(buf[:n]) if err != nil { - logger.Errorf("Failed to send pty output: %v", err) + logger.Warn().Err(err).Msg("Failed to send pty output") break } } @@ -63,11 +63,11 @@ func handleTerminalChannel(d *webrtc.DataChannel) { return } } - logger.Errorf("Failed to parse terminal size: %v", err) + logger.Warn().Err(err).Msg("Failed to parse terminal size") } _, err := ptmx.Write(msg.Data) if err != nil { - logger.Errorf("Failed to write to pty: %v", err) + logger.Warn().Err(err).Msg("Failed to write to pty") } }) diff --git a/usb.go b/usb.go index 8a3538b..03ea8a3 100644 --- a/usb.go +++ b/usb.go @@ -15,7 +15,7 @@ func initUsbGadget() { "jetkvm", config.UsbDevices, config.UsbConfig, - &logger, + &usbLogger, ) go func() { @@ -51,7 +51,7 @@ func rpcGetUSBState() (state string) { func triggerUSBStateUpdate() { go func() { if currentSession == nil { - logger.Info("No active RPC session, skipping update state update") + usbLogger.Info().Msg("No active RPC session, skipping update state update") return } writeJSONRPCEvent("usbState", usbState, currentSession) @@ -65,7 +65,7 @@ func checkUSBState() { } usbState = newState - logger.Infof("USB state changed from %s to %s", usbState, newState) + usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed") requestDisplayUpdate() triggerUSBStateUpdate() } diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 6578069..e70a353 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -55,7 +55,7 @@ func setMassStorageMode(cdrom bool) error { } func onDiskMessage(msg webrtc.DataChannelMessage) { - logger.Infof("Disk Message, len: %d", len(msg.Data)) + logger.Info().Int("len", len(msg.Data)).Msg("Disk Message") diskReadChan <- msg.Data } @@ -76,7 +76,7 @@ var nbdDevice *NBDDevice const imagesFolder = "/userdata/jetkvm/images" func rpcMountBuiltInImage(filename string) error { - logger.Infof("Mount Built-In Image: %s", filename) + logger.Info().Str("filename", filename).Msg("Mount Built-In Image") _ = os.MkdirAll(imagesFolder, 0755) imagePath := filepath.Join(imagesFolder, filename) @@ -173,7 +173,7 @@ func rpcUnmountImage() error { defer virtualMediaStateMutex.Unlock() err := setMassStorageImage("\n") if err != nil { - logger.Warnf("Remove Mass Storage Image Error: %v", err) + logger.Warn().Err(err).Msg("Remove Mass Storage Image Error") } //TODO: check if we still need it time.Sleep(500 * time.Millisecond) @@ -199,7 +199,7 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { virtualMediaStateMutex.Unlock() return fmt.Errorf("failed to use http url: %w", err) } - logger.Infof("using remote url %s with size %d", url, n) + logger.Info().Str("url", url).Int64("size", n).Msg("using remote url") currentVirtualMediaState = &VirtualMediaState{ Source: HTTP, Mode: mode, @@ -208,21 +208,21 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { } virtualMediaStateMutex.Unlock() - logger.Debug("Starting nbd device") + logger.Debug().Msg("Starting nbd device") nbdDevice = NewNBDDevice() err = nbdDevice.Start() if err != nil { - logger.Errorf("failed to start nbd device: %v", err) + logger.Warn().Err(err).Msg("failed to start nbd device") return err } - logger.Debug("nbd device started") + logger.Debug().Msg("nbd device started") //TODO: replace by polling on block device having right size time.Sleep(1 * time.Second) err = setMassStorageImage("/dev/nbd0") if err != nil { return err } - logger.Info("usb mass storage mounted") + logger.Info().Msg("usb mass storage mounted") return nil } @@ -239,22 +239,22 @@ func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) erro Size: size, } virtualMediaStateMutex.Unlock() - logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) - logger.Debug("Starting nbd device") + logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState") + logger.Debug().Msg("Starting nbd device") nbdDevice = NewNBDDevice() err := nbdDevice.Start() if err != nil { - logger.Errorf("failed to start nbd device: %v", err) + logger.Warn().Err(err).Msg("failed to start nbd device") return err } - logger.Debug("nbd device started") + logger.Debug().Msg("nbd device started") //TODO: replace by polling on block device having right size time.Sleep(1 * time.Second) err = setMassStorageImage("/dev/nbd0") if err != nil { return err } - logger.Info("usb mass storage mounted") + logger.Info().Msg("usb mass storage mounted") return nil } @@ -444,7 +444,7 @@ func handleUploadChannel(d *webrtc.DataChannel) { pendingUpload, ok := pendingUploads[uploadId] pendingUploadsMutex.Unlock() if !ok { - logger.Warnf("upload channel opened for unknown upload: %s", uploadId) + logger.Warn().Str("uploadId", uploadId).Msg("upload channel opened for unknown upload") return } totalBytesWritten := pendingUpload.AlreadyUploadedBytes @@ -454,12 +454,12 @@ func handleUploadChannel(d *webrtc.DataChannel) { newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete") err := os.Rename(pendingUpload.File.Name(), newName) if err != nil { - logger.Errorf("failed to rename uploaded file: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to rename uploaded file") } else { - logger.Debugf("successfully renamed uploaded file to: %s", newName) + logger.Debug().Str("uploadId", uploadId).Str("newName", newName).Msg("successfully renamed uploaded file") } } else { - logger.Warnf("uploaded ended before the complete file received") + logger.Warn().Str("uploadId", uploadId).Msg("uploaded ended before the complete file received") } pendingUploadsMutex.Lock() delete(pendingUploads, uploadId) @@ -470,7 +470,7 @@ func handleUploadChannel(d *webrtc.DataChannel) { d.OnMessage(func(msg webrtc.DataChannelMessage) { bytesWritten, err := pendingUpload.File.Write(msg.Data) if err != nil { - logger.Errorf("failed to write to file: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to write to file") close(uploadComplete) return } @@ -492,11 +492,11 @@ func handleUploadChannel(d *webrtc.DataChannel) { } progressJSON, err := json.Marshal(progress) if err != nil { - logger.Errorf("failed to marshal upload progress: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to marshal upload progress") } else { err = d.SendText(string(progressJSON)) if err != nil { - logger.Errorf("failed to send upload progress: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to send upload progress") } } lastProgressTime = time.Now() @@ -524,12 +524,12 @@ func handleUploadHttp(c *gin.Context) { newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete") err := os.Rename(pendingUpload.File.Name(), newName) if err != nil { - logger.Errorf("failed to rename uploaded file: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to rename uploaded file") } else { - logger.Debugf("successfully renamed uploaded file to: %s", newName) + logger.Debug().Str("uploadId", uploadId).Str("newName", newName).Msg("successfully renamed uploaded file") } } else { - logger.Warnf("uploaded ended before the complete file received") + logger.Warn().Str("uploadId", uploadId).Msg("uploaded ended before the complete file received") } pendingUploadsMutex.Lock() delete(pendingUploads, uploadId) @@ -541,7 +541,7 @@ func handleUploadHttp(c *gin.Context) { for { n, err := reader.Read(buffer) if err != nil && err != io.EOF { - logger.Errorf("failed to read from request body: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to read from request body") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read upload data"}) return } @@ -549,7 +549,7 @@ func handleUploadHttp(c *gin.Context) { if n > 0 { bytesWritten, err := pendingUpload.File.Write(buffer[:n]) if err != nil { - logger.Errorf("failed to write to file: %v", err) + logger.Warn().Err(err).Str("uploadId", uploadId).Msg("failed to write to file") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write upload data"}) return } diff --git a/video.go b/video.go index ade9353..d74add8 100644 --- a/video.go +++ b/video.go @@ -38,7 +38,7 @@ func HandleVideoStateMessage(event CtrlResponse) { videoState := VideoInputState{} err := json.Unmarshal(event.Data, &videoState) if err != nil { - logger.Warnf("Error parsing video state json: %v", err) + logger.Warn().Err(err).Msg("Error parsing video state json") return } lastVideoState = videoState diff --git a/web.go b/web.go index 6c35073..8bf34af 100644 --- a/web.go +++ b/web.go @@ -15,11 +15,13 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" + gin_logger "github.com/gin-contrib/logger" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/pion/webrtc/v4" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" "golang.org/x/crypto/bcrypt" ) @@ -65,7 +67,11 @@ func setupRouter() *gin.Engine { gin.SetMode(gin.ReleaseMode) gin.DisableConsoleColor() r := gin.Default() - + r.Use(gin_logger.SetLogger( + gin_logger.WithLogger(func(*gin.Context, zerolog.Logger) zerolog.Logger { + return ginLogger + }), + )) staticFS, _ := fs.Sub(staticFiles, "static") // Add a custom middleware to set cache headers for images @@ -181,16 +187,22 @@ var ( ) func handleLocalWebRTCSignal(c *gin.Context) { - cloudLogger.Infof("new websocket connection established") - // get the source from the request source := c.ClientIP() + scopedLogger := websocketLogger.With(). + Str("component", "websocket"). + Str("source", source). + Str("sourceType", "local"). + Logger() + + scopedLogger.Info().Msg("new websocket connection established") + // Create WebSocket options with InsecureSkipVerify to bypass origin check wsOptions := &websocket.AcceptOptions{ InsecureSkipVerify: true, // Allow connections from any origin OnPingReceived: func(ctx context.Context, payload []byte) bool { - websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: local", payload, source) + scopedLogger.Info().Bytes("payload", payload).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime() @@ -214,14 +226,14 @@ func handleLocalWebRTCSignal(c *gin.Context) { return } - err = handleWebRTCSignalWsMessages(wsCon, false, source) + err = handleWebRTCSignalWsMessages(wsCon, false, source, &scopedLogger) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } -func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, source string) error { +func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, source string, scopedLogger *zerolog.Logger) error { runCtx, cancelRun := context.WithCancel(context.Background()) defer cancelRun() @@ -236,21 +248,13 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, sourceType = "local" } - // probably we can use a better logging framework here - logInfof := func(format string, args ...interface{}) { - args = append(args, source, sourceType, connectionID) - websocketLogger.Infof(format+", source: %s, sourceType: %s, id: %s", args...) - } - logWarnf := func(format string, args ...interface{}) { - args = append(args, source, sourceType, connectionID) - websocketLogger.Warnf(format+", source: %s, sourceType: %s, id: %s", args...) - } - logTracef := func(format string, args ...interface{}) { - args = append(args, source, sourceType, connectionID) - websocketLogger.Tracef(format+", source: %s, sourceType: %s, id: %s", args...) - } + l := scopedLogger.With(). + Str("source", source). + Str("sourceType", sourceType). + Str("connectionID", connectionID). + Logger() - logInfof("new websocket connection established") + l.Info().Msg("new websocket connection established") go func() { for { @@ -258,9 +262,9 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, if ctxErr := runCtx.Err(); ctxErr != nil { if !errors.Is(ctxErr, context.Canceled) { - logWarnf("websocket connection closed: %v", ctxErr) + l.Warn().Str("error", ctxErr.Error()).Msg("websocket connection closed") } else { - logTracef("websocket connection closed as the context was canceled: %v") + l.Trace().Str("error", ctxErr.Error()).Msg("websocket connection closed as the context was canceled") } return } @@ -271,11 +275,11 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v) })) - logTracef("sending ping frame") + l.Trace().Msg("sending ping frame") err := wsCon.Ping(runCtx) if err != nil { - logWarnf("websocket ping error: %v", err) + l.Warn().Str("error", err.Error()).Msg("websocket ping error") cancelRun() return } @@ -286,7 +290,7 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, metricConnectionTotalPingSentCount.WithLabelValues(sourceType, source).Inc() metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() - logTracef("received pong frame, duration: %v", duration) + l.Trace().Str("duration", duration.String()).Msg("received pong frame") } }() @@ -302,7 +306,7 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, if err == nil { continue } - cloudLogger.Infof("disconnecting from cloud due to: %v", err) + cloudLogger.Info().Err(err).Msg("disconnecting from cloud due to") cancelRun() } }() @@ -311,7 +315,7 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, for { typ, msg, err := wsCon.Read(runCtx) if err != nil { - logWarnf("websocket read error: %v", err) + l.Warn().Str("error", err.Error()).Msg("websocket read error") return err } if typ != websocket.MessageText { @@ -325,10 +329,10 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, } if bytes.Equal(msg, pingMessage) { - logInfof("ping message received: %s", string(msg)) + l.Info().Str("message", string(msg)).Msg("ping message received") err = wsCon.Write(context.Background(), websocket.MessageText, pongMessage) if err != nil { - logWarnf("unable to write pong message: %v", err) + l.Warn().Str("error", err.Error()).Msg("unable to write pong message") return err } @@ -340,55 +344,55 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool, err = json.Unmarshal(msg, &message) if err != nil { - logWarnf("unable to parse ws message: %v", err) + l.Warn().Str("error", err.Error()).Msg("unable to parse ws message") continue } if message.Type == "offer" { - logInfof("new session request received") + l.Info().Msg("new session request received") var req WebRTCSessionRequest err = json.Unmarshal(message.Data, &req) if err != nil { - logWarnf("unable to parse session request data: %v", err) + l.Warn().Str("error", err.Error()).Msg("unable to parse session request data") continue } if req.OidcGoogle != "" { - logInfof("new session request with OIDC Google: %v", req.OidcGoogle) + l.Info().Str("oidcGoogle", req.OidcGoogle).Msg("new session request with OIDC Google") } metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc() metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() err = handleSessionRequest(runCtx, wsCon, req, isCloudConnection, source) if err != nil { - logWarnf("error starting new session: %v", err) + l.Warn().Str("error", err.Error()).Msg("error starting new session") continue } } else if message.Type == "new-ice-candidate" { - logInfof("The client sent us a new ICE candidate: %v", string(message.Data)) + l.Info().Str("data", string(message.Data)).Msg("The client sent us a new ICE candidate") var candidate webrtc.ICECandidateInit // Attempt to unmarshal as a ICECandidateInit if err := json.Unmarshal(message.Data, &candidate); err != nil { - logWarnf("unable to parse incoming ICE candidate data: %v", string(message.Data)) + l.Warn().Str("error", err.Error()).Msg("unable to parse incoming ICE candidate data") continue } if candidate.Candidate == "" { - logWarnf("empty incoming ICE candidate, skipping") + l.Warn().Msg("empty incoming ICE candidate, skipping") continue } - logInfof("unmarshalled incoming ICE candidate: %v", candidate) + l.Info().Str("data", fmt.Sprintf("%v", candidate)).Msg("unmarshalled incoming ICE candidate") if currentSession == nil { - logInfof("no current session, skipping incoming ICE candidate") + l.Warn().Msg("no current session, skipping incoming ICE candidate") continue } - logInfof("adding incoming ICE candidate to current session: %v", candidate) + l.Info().Str("data", fmt.Sprintf("%v", candidate)).Msg("adding incoming ICE candidate to current session") if err = currentSession.peerConnection.AddICECandidate(candidate); err != nil { - logWarnf("failed to add incoming ICE candidate to our peer connection: %v", err) + l.Warn().Str("error", err.Error()).Msg("failed to add incoming ICE candidate to our peer connection") } } } diff --git a/web_tls.go b/web_tls.go index 976cff6..1ef4d31 100644 --- a/web_tls.go +++ b/web_tls.go @@ -45,7 +45,7 @@ func RunWebSecureServer() { hostname = strings.Split(info.Conn.LocalAddr().String(), ":")[0] } - logger.Infof("TLS handshake for %s, SupportedProtos: %v", hostname, info.SupportedProtos) + logger.Info().Str("hostname", hostname).Interface("SupportedProtos", info.SupportedProtos).Msg("TLS handshake") cert := createSelfSignedCert(hostname) @@ -53,7 +53,7 @@ func RunWebSecureServer() { }, }, } - logger.Infof("Starting websecure server on %s", RunWebSecureServer) + logger.Info().Str("listen", WebSecureListen).Msg("Starting websecure server") err := server.ListenAndServeTLS("", "") if err != nil { panic(err) @@ -67,11 +67,11 @@ func createSelfSignedCert(hostname string) *tls.Certificate { tlsCertLock.Lock() defer tlsCertLock.Unlock() - logger.Infof("Creating self-signed certificate for %s", hostname) + logger.Info().Str("hostname", hostname).Msg("Creating self-signed certificate") priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - logger.Errorf("Failed to generate private key: %v", err) + logger.Warn().Err(err).Msg("Failed to generate private key") os.Exit(1) } keyUsage := x509.KeyUsageDigitalSignature @@ -82,7 +82,7 @@ func createSelfSignedCert(hostname string) *tls.Certificate { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - logger.Errorf("Failed to generate serial number: %v", err) + logger.Warn().Err(err).Msg("Failed to generate serial number") } dnsName := hostname @@ -114,12 +114,12 @@ func createSelfSignedCert(hostname string) *tls.Certificate { derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { - logger.Errorf("Failed to create certificate: %v", err) + logger.Warn().Err(err).Msg("Failed to create certificate") } cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) if cert == nil { - logger.Errorf("Failed to encode certificate") + logger.Warn().Msg("Failed to encode certificate") } tlsCert := &tls.Certificate{ diff --git a/webrtc.go b/webrtc.go index a047ecc..d01b4b6 100644 --- a/webrtc.go +++ b/webrtc.go @@ -65,22 +65,24 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { } func newSession(config SessionConfig) (*Session, error) { - webrtcSettingEngine := webrtc.SettingEngine{} + webrtcSettingEngine := webrtc.SettingEngine{ + LoggerFactory: defaultLoggerFactory, + } iceServer := webrtc.ICEServer{} if config.IsCloud { if config.ICEServers == nil { - logger.Info("ICE Servers not provided by cloud") + logger.Info().Msg("ICE Servers not provided by cloud") } else { iceServer.URLs = config.ICEServers - logger.Infof("Using ICE Servers provided by cloud: %v", iceServer.URLs) + logger.Info().Interface("iceServers", iceServer.URLs).Msg("Using ICE Servers provided by cloud") } if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil { - logger.Infof("Local IP address %v not provided or invalid, won't set NAT1To1IPs", config.LocalIP) + logger.Info().Str("localIP", config.LocalIP).Msg("Local IP address not provided or invalid, won't set NAT1To1IPs") } else { webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx) - logger.Infof("Setting NAT1To1IPs to %s", config.LocalIP) + logger.Info().Str("localIP", config.LocalIP).Msg("Setting NAT1To1IPs") } } @@ -94,7 +96,7 @@ func newSession(config SessionConfig) (*Session, error) { session := &Session{peerConnection: peerConnection} peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { - logger.Infof("New DataChannel %s %d", d.Label(), d.ID()) + logger.Info().Str("label", d.Label()).Uint16("id", *d.ID()).Msg("New DataChannel") switch d.Label() { case "rpc": session.RPCChannel = d @@ -142,17 +144,17 @@ func newSession(config SessionConfig) (*Session, error) { var isConnected bool peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { - logger.Infof("Our WebRTC peerConnection has a new ICE candidate: %v", candidate) + logger.Info().Interface("candidate", candidate).Msg("Our WebRTC peerConnection has a new ICE candidate") if candidate != nil { err := wsjson.Write(context.Background(), config.ws, gin.H{"type": "new-ice-candidate", "data": candidate.ToJSON()}) if err != nil { - logger.Errorf("failed to write new-ice-candidate to WebRTC signaling channel: %v", err) + logger.Warn().Err(err).Msg("failed to write new-ice-candidate to WebRTC signaling channel") } } }) peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { - logger.Infof("Connection State has changed %s", connectionState) + logger.Info().Str("connectionState", connectionState.String()).Msg("Connection State has changed") if connectionState == webrtc.ICEConnectionStateConnected { if !isConnected { isConnected = true @@ -173,7 +175,7 @@ func newSession(config SessionConfig) (*Session, error) { } if session.shouldUmountVirtualMedia { err := rpcUnmountImage() - logger.Debugf("unmount image failed on connection close %v", err) + logger.Debug().Err(err).Msg("unmount image failed on connection close") } if isConnected { isConnected = false From 82c018a2f6c2922ce240af77a4b259acb0158945 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Tue, 18 Mar 2025 17:25:03 +0100 Subject: [PATCH 025/165] feat(tls): #330 --- config.go | 4 +- internal/websecure/log.go | 9 + internal/websecure/selfsign.go | 191 ++++++++++++ internal/websecure/store.go | 175 +++++++++++ internal/websecure/utils.go | 80 +++++ jsonrpc.go | 48 ++- log.go | 1 + main.go | 6 +- .../devices.$id.settings.access._index.tsx | 177 +++++++++-- ui/src/routes/devices.$id.settings.tsx | 13 +- web_tls.go | 283 +++++++++++------- 11 files changed, 858 insertions(+), 129 deletions(-) create mode 100644 internal/websecure/log.go create mode 100644 internal/websecure/selfsign.go create mode 100644 internal/websecure/store.go create mode 100644 internal/websecure/utils.go diff --git a/config.go b/config.go index c38f1ed..f19b6e0 100644 --- a/config.go +++ b/config.go @@ -90,7 +90,7 @@ type Config struct { DisplayMaxBrightness int `json:"display_max_brightness"` DisplayDimAfterSec int `json:"display_dim_after_sec"` DisplayOffAfterSec int `json:"display_off_after_sec"` - TLSMode string `json:"tls_mode"` + TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" UsbConfig *usbgadget.Config `json:"usb_config"` UsbDevices *usbgadget.Devices `json:"usb_devices"` } @@ -169,6 +169,8 @@ func SaveConfig() error { configLock.Lock() defer configLock.Unlock() + logger.Trace().Str("path", configPath).Msg("Saving config") + file, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create config file: %w", err) diff --git a/internal/websecure/log.go b/internal/websecure/log.go new file mode 100644 index 0000000..f45767e --- /dev/null +++ b/internal/websecure/log.go @@ -0,0 +1,9 @@ +package websecure + +import ( + "os" + + "github.com/rs/zerolog" +) + +var defaultLogger = zerolog.New(os.Stdout).With().Str("component", "websecure").Logger() diff --git a/internal/websecure/selfsign.go b/internal/websecure/selfsign.go new file mode 100644 index 0000000..77efa37 --- /dev/null +++ b/internal/websecure/selfsign.go @@ -0,0 +1,191 @@ +package websecure + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "net" + "strings" + "time" + + "github.com/rs/zerolog" + "golang.org/x/net/idna" +) + +const selfSignerCAMagicName = "__ca__" + +type SelfSigner struct { + store *CertStore + log *zerolog.Logger + + caInfo pkix.Name + + DefaultDomain string + DefaultOrg string + DefaultOU string +} + +func NewSelfSigner( + store *CertStore, + log *zerolog.Logger, + defaultDomain, + defaultOrg, + defaultOU, + caName string, +) *SelfSigner { + return &SelfSigner{ + store: store, + log: log, + DefaultDomain: defaultDomain, + DefaultOrg: defaultOrg, + DefaultOU: defaultOU, + caInfo: pkix.Name{ + CommonName: caName, + Organization: []string{defaultOrg}, + OrganizationalUnit: []string{defaultOU}, + }, + } +} + +func (s *SelfSigner) getCA() *tls.Certificate { + return s.createSelfSignedCert(selfSignerCAMagicName) +} + +func (s *SelfSigner) createSelfSignedCert(hostname string) *tls.Certificate { + if tlsCert := s.store.certificates[hostname]; tlsCert != nil { + return tlsCert + } + + // check if hostname is the CA magic name + var ca *tls.Certificate + if hostname != selfSignerCAMagicName { + ca = s.getCA() + if ca == nil { + s.log.Error().Msg("Failed to get CA certificate") + return nil + } + } + + s.log.Info().Str("hostname", hostname).Msg("Creating self-signed certificate") + + // lock the store while creating the certificate (do not move upwards) + s.store.certLock.Lock() + defer s.store.certLock.Unlock() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + s.log.Error().Err(err).Msg("Failed to generate private key") + return nil + } + + notBefore := time.Now() + notAfter := notBefore.AddDate(1, 0, 0) + + serialNumber, err := generateSerialNumber() + if err != nil { + s.log.Error().Err(err).Msg("Failed to generate serial number") + return nil + } + + dnsName := hostname + ip := net.ParseIP(hostname) + if ip != nil { + dnsName = s.DefaultDomain + } + + // set up CSR + isCA := hostname == selfSignerCAMagicName + subject := pkix.Name{ + CommonName: hostname, + Organization: []string{s.DefaultOrg}, + OrganizationalUnit: []string{s.DefaultOU}, + } + keyUsage := x509.KeyUsageDigitalSignature + extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + + // check if hostname is the CA magic name, and if so, set the subject to the CA info + if isCA { + subject = s.caInfo + keyUsage |= x509.KeyUsageCertSign + extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth) + notAfter = notBefore.AddDate(10, 0, 0) + } + + cert := x509.Certificate{ + SerialNumber: serialNumber, + Subject: subject, + NotBefore: notBefore, + NotAfter: notAfter, + IsCA: isCA, + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + BasicConstraintsValid: true, + } + + // set up DNS names and IP addresses + if !isCA { + cert.DNSNames = []string{dnsName} + if ip != nil { + cert.IPAddresses = []net.IP{ip} + } + } + + // set up parent certificate + parent := &cert + parentPriv := priv + if ca != nil { + parent, err = x509.ParseCertificate(ca.Certificate[0]) + if err != nil { + s.log.Error().Err(err).Msg("Failed to parse parent certificate") + return nil + } + parentPriv = ca.PrivateKey.(*ecdsa.PrivateKey) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &cert, parent, &priv.PublicKey, parentPriv) + if err != nil { + s.log.Error().Err(err).Msg("Failed to create certificate") + return nil + } + + tlsCert := &tls.Certificate{ + Certificate: [][]byte{certBytes}, + PrivateKey: priv, + } + if ca != nil { + tlsCert.Certificate = append(tlsCert.Certificate, ca.Certificate...) + } + + s.store.certificates[hostname] = tlsCert + s.store.saveCertificate(hostname) + + return tlsCert +} + +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *SelfSigner) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + var hostname string + if info.ServerName != "" && info.ServerName != selfSignerCAMagicName { + hostname = info.ServerName + } else { + hostname = strings.Split(info.Conn.LocalAddr().String(), ":")[0] + } + + s.log.Info().Str("hostname", hostname).Strs("supported_protos", info.SupportedProtos).Msg("TLS handshake") + + // convert hostname to punycode + h, err := idna.Lookup.ToASCII(hostname) + if err != nil { + s.log.Warn().Str("hostname", hostname).Err(err).Str("remote_addr", info.Conn.RemoteAddr().String()).Msg("Hostname is not valid") + hostname = s.DefaultDomain + } else { + hostname = h + } + + cert := s.createSelfSignedCert(hostname) + return cert, nil +} diff --git a/internal/websecure/store.go b/internal/websecure/store.go new file mode 100644 index 0000000..7da2dee --- /dev/null +++ b/internal/websecure/store.go @@ -0,0 +1,175 @@ +package websecure + +import ( + "crypto/tls" + "fmt" + "os" + "path" + "strings" + "sync" + + "github.com/rs/zerolog" +) + +type CertStore struct { + certificates map[string]*tls.Certificate + certLock *sync.Mutex + + storePath string + + log *zerolog.Logger +} + +func NewCertStore(storePath string, log *zerolog.Logger) *CertStore { + if log == nil { + log = &defaultLogger + } + + return &CertStore{ + certificates: make(map[string]*tls.Certificate), + certLock: &sync.Mutex{}, + + storePath: storePath, + log: log, + } +} + +func (s *CertStore) ensureStorePath() error { + // check if directory exists + stat, err := os.Stat(s.storePath) + if err == nil { + if stat.IsDir() { + return nil + } + + return fmt.Errorf("TLS store path exists but is not a directory: %s", s.storePath) + } + + if os.IsNotExist(err) { + s.log.Trace().Str("path", s.storePath).Msg("TLS store directory does not exist, creating directory") + err = os.MkdirAll(s.storePath, 0755) + if err != nil { + return fmt.Errorf("Failed to create TLS store path: %w", err) + } + return nil + } + + return fmt.Errorf("Failed to check TLS store path: %w", err) +} + +func (s *CertStore) LoadCertificates() { + err := s.ensureStorePath() + if err != nil { + s.log.Error().Err(err).Msg("Failed to ensure store path") + return + } + + files, err := os.ReadDir(s.storePath) + if err != nil { + s.log.Error().Err(err).Msg("Failed to read TLS directory") + return + } + + for _, file := range files { + if file.IsDir() { + continue + } + + if strings.HasSuffix(file.Name(), ".crt") { + s.loadCertificate(strings.TrimSuffix(file.Name(), ".crt")) + } + } +} + +func (s *CertStore) loadCertificate(hostname string) { + s.certLock.Lock() + defer s.certLock.Unlock() + + keyFile := path.Join(s.storePath, hostname+".key") + crtFile := path.Join(s.storePath, hostname+".crt") + + cert, err := tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + s.log.Error().Err(err).Str("hostname", hostname).Msg("Failed to load certificate") + return + } + + s.certificates[hostname] = &cert + + s.log.Info().Str("hostname", hostname).Msg("Loaded certificate") +} + +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *CertStore) GetCertificate(hostname string) *tls.Certificate { + s.certLock.Lock() + defer s.certLock.Unlock() + + return s.certificates[hostname] +} + +// ValidateAndSaveCertificate validates the certificate and saves it to the store +// returns are: +// - error: if the certificate is invalid or if there's any error during saving the certificate +// - error: if there's any warning or error during saving the certificate +func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key string, ignoreWarning bool) (error, error) { + tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key)) + if err != nil { + return fmt.Errorf("Failed to parse certificate: %w", err), nil + } + + // this can be skipped as current implementation supports one custom certificate only + if tlsCert.Leaf != nil { + // add recover to avoid panic + defer func() { + if r := recover(); r != nil { + s.log.Error().Interface("recovered", r).Msg("Failed to verify hostname") + } + }() + + if err = tlsCert.Leaf.VerifyHostname(hostname); err != nil { + if !ignoreWarning { + return nil, fmt.Errorf("Certificate does not match hostname: %w", err) + } + s.log.Warn().Err(err).Msg("Certificate does not match hostname") + } + } + + s.certLock.Lock() + s.certificates[hostname] = &tlsCert + s.certLock.Unlock() + + s.saveCertificate(hostname) + + return nil, nil +} + +func (s *CertStore) saveCertificate(hostname string) { + // check if certificate already exists + tlsCert := s.certificates[hostname] + if tlsCert == nil { + s.log.Error().Str("hostname", hostname).Msg("Certificate for hostname does not exist, skipping saving certificate") + return + } + + err := s.ensureStorePath() + if err != nil { + s.log.Error().Err(err).Msg("Failed to ensure store path") + return + } + + keyFile := path.Join(s.storePath, hostname+".key") + crtFile := path.Join(s.storePath, hostname+".crt") + + if err := keyToFile(tlsCert, keyFile); err != nil { + s.log.Error().Err(err).Msg("Failed to save key file") + return + } + + if err := certToFile(tlsCert, crtFile); err != nil { + s.log.Error().Err(err).Msg("Failed to save certificate") + return + } + + s.log.Info().Str("hostname", hostname).Msg("Saved certificate") +} diff --git a/internal/websecure/utils.go b/internal/websecure/utils.go new file mode 100644 index 0000000..de29c73 --- /dev/null +++ b/internal/websecure/utils.go @@ -0,0 +1,80 @@ +package websecure + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + "os" +) + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 4096) + +func withSecretFile(filename string, f func(*os.File) error) error { + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer file.Close() + + return f(file) +} + +func keyToFile(cert *tls.Certificate, filename string) error { + var keyBlock pem.Block + switch k := cert.PrivateKey.(type) { + case *rsa.PrivateKey: + keyBlock = pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + } + case *ecdsa.PrivateKey: + b, e := x509.MarshalECPrivateKey(k) + if e != nil { + return fmt.Errorf("Failed to marshal EC private key: %v", e) + } + + keyBlock = pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: b, + } + default: + return fmt.Errorf("Unknown private key type: %T", k) + } + + err := withSecretFile(filename, func(file *os.File) error { + return pem.Encode(file, &keyBlock) + }) + + if err != nil { + return fmt.Errorf("Failed to save private key: %w", err) + } + + return nil +} + +func certToFile(cert *tls.Certificate, filename string) error { + return withSecretFile(filename, func(file *os.File) error { + for _, c := range cert.Certificate { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: c, + } + + err := pem.Encode(file, &block) + if err != nil { + return fmt.Errorf("Failed to save certificate: %w", err) + } + } + + return nil + }) +} + +func generateSerialNumber() (*big.Int, error) { + return rand.Int(rand.Reader, serialNumberLimit) +} diff --git a/jsonrpc.go b/jsonrpc.go index de29e08..d56b8ea 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -95,7 +95,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } - //logger.Infof("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID) + logger.Trace().Str("method", request.Method).Interface("params", request.Params).Interface("id", request.ID).Msg("Received RPC request") handler, ok := rpcHandlers[request.Method] if !ok { errorResponse := JSONRPCResponse{ @@ -110,6 +110,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } + logger.Trace().Str("method", request.Method).Interface("id", request.ID).Msg("Calling RPC handler") result, err := callRPCHandler(handler, request.Params) if err != nil { errorResponse := JSONRPCResponse{ @@ -125,6 +126,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } + logger.Trace().Interface("result", result).Interface("id", request.ID).Msg("RPC handler returned") response := JSONRPCResponse{ JSONRPC: "2.0", Result: result, @@ -141,6 +143,30 @@ func rpcGetDeviceID() (string, error) { return GetDeviceID(), nil } +func rpcReboot(force bool) error { + logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") + + args := []string{} + if force { + args = append(args, "-f") + } + + cmd := exec.Command("reboot", args...) + err := cmd.Start() + if err != nil { + logger.Error().Err(err).Msg("failed to reboot") + return fmt.Errorf("failed to reboot: %w", err) + } + + // If the reboot command is successful, exit the program after 5 seconds + go func() { + time.Sleep(5 * time.Second) + os.Exit(0) + }() + + return nil +} + var streamFactor = 1.0 func rpcGetStreamQualityFactor() (float64, error) { @@ -375,6 +401,23 @@ func rpcSetSSHKeyState(sshKey string) error { return nil } +func rpcGetTLSState() TLSState { + return getTLSState() +} + +func rpcSetTLSState(state TLSState) error { + err := setTLSState(state) + if err != nil { + return fmt.Errorf("failed to set TLS state: %w", err) + } + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) { handlerValue := reflect.ValueOf(handler.Func) handlerType := handlerValue.Type() @@ -892,6 +935,7 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, "getDeviceID": {Func: rpcGetDeviceID}, "deregisterDevice": {Func: rpcDeregisterDevice}, "getCloudState": {Func: rpcGetCloudState}, @@ -920,6 +964,8 @@ var rpcHandlers = map[string]RPCHandler{ "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "getSSHKeyState": {Func: rpcGetSSHKeyState}, "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, "getMassStorageMode": {Func: rpcGetMassStorageMode}, "isUpdatePending": {Func: rpcIsUpdatePending}, diff --git a/log.go b/log.go index 6824a3f..5dac1f6 100644 --- a/log.go +++ b/log.go @@ -50,6 +50,7 @@ var ( displayLogger = getLogger("display") usbLogger = getLogger("usb") ginLogger = getLogger("gin") + websecureLogger = getLogger("websecure") ) func updateLogLevel() { diff --git a/main.go b/main.go index 98748c7..d74b1ef 100644 --- a/main.go +++ b/main.go @@ -69,9 +69,13 @@ func Main() { }() //go RunFuseServer() go RunWebServer() + + go RunWebSecureServer() + // Web secure server is started only if TLS mode is enabled if config.TLSMode != "" { - go RunWebSecureServer() + startWebSecureServer() } + // As websocket client already checks if the cloud token is set, we can start it here. go RunWebsocketClient() diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx index 0ed5862..d8eebf9 100644 --- a/ui/src/routes/devices.$id.settings.access._index.tsx +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -14,11 +14,18 @@ import notifications from "@/notifications"; import { DEVICE_API } from "@/ui.config"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { isOnDevice } from "@/main"; +import { TextAreaWithLabel } from "@components/TextArea"; import { LocalDevice } from "./devices.$id"; import { SettingsItem } from "./devices.$id.settings"; import { CloudState } from "./adopt"; +export interface TLSState { + mode: "self-signed" | "custom" | "disabled"; + certificate?: string; + privateKey?: string; +} + export const loader = async () => { if (isOnDevice) { const status = await api @@ -44,6 +51,9 @@ export default function SettingsAccessIndexRoute() { // Use a simple string identifier for the selected provider const [selectedProvider, setSelectedProvider] = useState("jetkvm"); + const [tlsMode, setTlsMode] = useState("unknown"); + const [tlsCert, setTlsCert] = useState(""); + const [tlsKey, setTlsKey] = useState(""); const getCloudState = useCallback(() => { send("getCloudState", {}, resp => { @@ -66,6 +76,17 @@ export default function SettingsAccessIndexRoute() { }); }, [send]); + const getTLSState = useCallback(() => { + send("getTLSState", {}, resp => { + if ("error" in resp) return console.error(resp.error); + const tlsState = resp.result as TLSState; + + setTlsMode(tlsState.mode); + if (tlsState.certificate) setTlsCert(tlsState.certificate); + if (tlsState.privateKey) setTlsKey(tlsState.privateKey); + }); + }, [send]); + const deregisterDevice = async () => { send("deregisterDevice", {}, resp => { if ("error" in resp) { @@ -126,15 +147,62 @@ export default function SettingsAccessIndexRoute() { } }; + // Function to update TLS state - accepts a mode parameter + const updateTlsState = useCallback( + (mode: string, cert?: string, key?: string) => { + const state = { mode } as TLSState; + if (cert && key) { + state.certificate = cert; + state.privateKey = key; + } + + send("setTLSState", { state }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to update TLS settings: ${resp.error.data || "Unknown error"}`, + ); + return; + } + + notifications.success("TLS settings updated successfully"); + }); + }, + [send], + ); + + // Handle TLS mode change + const handleTlsModeChange = (value: string) => { + setTlsMode(value); + + // For "disabled" and "self-signed" modes, immediately apply the settings + if (value !== "custom") { + updateTlsState(value); + } + }; + + const handleTlsCertChange = (value: string) => { + setTlsCert(value); + }; + + const handleTlsKeyChange = (value: string) => { + setTlsKey(value); + }; + + // Update the custom TLS settings button click handler + const handleCustomTlsUpdate = () => { + updateTlsState(tlsMode, tlsCert, tlsKey); + }; + // Fetch device ID and cloud state on component mount useEffect(() => { getCloudState(); + getTLSState(); send("getDeviceID", {}, async resp => { if ("error" in resp) return console.error(resp.error); setDeviceId(resp.result as string); }); - }, [send, getCloudState]); + }, [send, getCloudState, getTLSState]); return (
    @@ -150,30 +218,95 @@ export default function SettingsAccessIndexRoute() { title="Local" description="Manage the mode of local access to the device" /> - - {loaderData.authMode === "password" ? ( -
    +
    )} - + + + {loaderData.authMode === "password" ? ( +
    From 2b2a14204d969c03b1caee9114ab1aa25cb30cac Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:34:53 +0200 Subject: [PATCH 043/165] feat: implement pointer-lock and keyboard-lock (#352) * feat: implement pointer-lock and keyboard-lock * feat: Add Pointer lock functionality and SSL support in dev mode - Introduced @vitejs/plugin-basic-ssl for enabling SSL in development. - Added a new script `dev:ssl` to run the development server with SSL. - Implemented pointer lock feature in the WebRTCVideo component, enhancing user interaction. - Added a PointerLockBar component to guide users on enabling mouse control. - Cleaned up the VideoOverlay and WebRTCVideo components for better readability and functionality. --------- Co-authored-by: Adam Shiervani --- ui/package-lock.json | 56 ++----- ui/package.json | 2 + ui/src/components/VideoOverlay.tsx | 34 ++++ ui/src/components/WebRTCVideo.tsx | 245 ++++++++++++++++++++--------- ui/vite.config.ts | 13 +- 5 files changed, 230 insertions(+), 120 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index ebce148..b51a2ea 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -11,6 +11,7 @@ "@headlessui/react": "^2.2.0", "@headlessui/tailwindcss": "^0.2.1", "@heroicons/react": "^2.2.0", + "@vitejs/plugin-basic-ssl": "^1.2.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", @@ -105,7 +106,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -121,7 +121,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -137,7 +136,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -153,7 +151,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -169,7 +166,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -185,7 +181,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -201,7 +196,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -217,7 +211,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -233,7 +226,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -249,7 +241,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -265,7 +256,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -281,7 +271,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -297,7 +286,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -313,7 +301,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -329,7 +316,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -345,7 +331,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -361,7 +346,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -377,7 +361,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -393,7 +376,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -409,7 +391,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -425,7 +406,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -441,7 +421,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -457,7 +436,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -890,7 +868,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -903,7 +880,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -916,7 +892,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -929,7 +904,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -942,7 +916,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -955,7 +928,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -968,7 +940,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -981,7 +952,6 @@ "cpu": [ "ppc64le" ], - "dev": true, "optional": true, "os": [ "linux" @@ -994,7 +964,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1007,7 +976,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1020,7 +988,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1033,7 +1000,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1046,7 +1012,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1059,7 +1024,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1072,7 +1036,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1426,8 +1389,7 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -1675,6 +1637,17 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", @@ -2740,7 +2713,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -5345,7 +5317,6 @@ "version": "4.14.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==", - "dev": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -6250,7 +6221,6 @@ "version": "5.2.8", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", - "dev": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", diff --git a/ui/package.json b/ui/package.json index a248616..3160297 100644 --- a/ui/package.json +++ b/ui/package.json @@ -8,6 +8,7 @@ }, "scripts": { "dev": "./dev_device.sh", + "dev:ssl": "USE_SSL=true ./dev_device.sh", "dev:cloud": "vite dev --mode=cloud-development", "build": "npm run build:prod", "build:device": "tsc && vite build --mode=device --emptyOutDir", @@ -21,6 +22,7 @@ "@headlessui/react": "^2.2.0", "@headlessui/tailwindcss": "^0.2.1", "@heroicons/react": "^2.2.0", + "@vitejs/plugin-basic-ssl": "^1.2.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 3d520d2..0d71dcd 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -7,6 +7,7 @@ import { LuPlay } from "react-icons/lu"; import { Button, LinkButton } from "@components/Button"; import LoadingSpinner from "@components/LoadingSpinner"; import Card, { GridCard } from "@components/Card"; +import { BsMouseFill } from "react-icons/bs"; interface OverlayContentProps { children: React.ReactNode; @@ -358,3 +359,36 @@ export function NoAutoplayPermissionsOverlay({ ); } + +interface PointerLockBarProps { + show: boolean; +} + +export function PointerLockBar({ show }: PointerLockBarProps) { + return ( + + {show ? ( + +
    + +
    +
    + + + Click on the video to enable mouse control + +
    +
    +
    +
    +
    + ) : null} +
    + ); +} diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index be69899..b73135b 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -6,6 +6,7 @@ import { useMouseStore, useRTCStore, useSettingsStore, + useUiStore, useVideoStore, } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; @@ -17,11 +18,13 @@ import MacroBar from "@/components/MacroBar"; import InfoBar from "@components/InfoBar"; import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; +import notifications from "@/notifications"; import { HDMIErrorOverlay, LoadingVideoOverlay, NoAutoplayPermissionsOverlay, + PointerLockBar, } from "./VideoOverlay"; export default function WebRTCVideo() { @@ -30,7 +33,7 @@ export default function WebRTCVideo() { const mediaStream = useRTCStore(state => state.mediaStream); const [isPlaying, setIsPlaying] = useState(false); const peerConnectionState = useRTCStore(state => state.peerConnectionState); - + const [isPointerLockActive, setIsPointerLockActive] = useState(false); // Store hooks const settings = useSettingsStore(); const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); @@ -53,14 +56,13 @@ export default function WebRTCVideo() { const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); const isVideoLoading = !isPlaying; - // console.log("peerConnection?.connectionState", peerConnection?.connectionState); - // Keyboard related states const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } = useHidStore(); // Misc states and hooks const [blockWheelEvent, setBlockWheelEvent] = useState(false); + const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); const [send] = useJsonRpc(); // Video-related @@ -97,6 +99,64 @@ export default function WebRTCVideo() { [setVideoClientSize, updateVideoSizeStore, setVideoSize], ); + // Pointer lock and keyboard lock related + const isPointerLockPossible = window.location.protocol === "https:"; + + const checkNavigatorPermissions = useCallback(async (permissionName: string) => { + const name = permissionName as PermissionName; + const { state } = await navigator.permissions.query({ name }); + return state === "granted"; + }, []); + + const requestPointerLock = useCallback(async () => { + if (document.pointerLockElement) return; + + const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock"); + if (isPointerLockGranted && settings.mouseMode === "relative") { + videoElm.current?.requestPointerLock(); + } + }, [checkNavigatorPermissions, settings.mouseMode]); + + useEffect(() => { + if (!isPointerLockPossible || !videoElm.current) return; + + const handlePointerLockChange = () => { + if (document.pointerLockElement) { + notifications.success("Pointer lock Enabled, hold escape to exit"); + setIsPointerLockActive(true); + } else { + notifications.success("Pointer lock disabled"); + setIsPointerLockActive(false); + } + }; + + const abortController = new AbortController(); + const signal = abortController.signal; + + document.addEventListener("pointerlockchange", handlePointerLockChange, { signal }); + + return () => { + abortController.abort(); + }; + }, [isPointerLockPossible, videoElm]); + + const requestFullscreen = useCallback(async () => { + videoElm.current?.requestFullscreen({ + navigationUI: "show", + }); + + // we do not care about pointer lock if it's for fullscreen + await requestPointerLock(); + + const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock"); + if (isKeyboardLockGranted) { + if ("keyboard" in navigator) { + // @ts-ignore + await navigator.keyboard.lock(); + } + } + }, [requestPointerLock, checkNavigatorPermissions]); + // Mouse-related const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos); const sendRelMouseMovement = useCallback( @@ -113,12 +173,18 @@ export default function WebRTCVideo() { const relMouseMoveHandler = useCallback( (e: MouseEvent) => { if (settings.mouseMode !== "relative") return; + if (isPointerLockActive === false && isPointerLockPossible === true) return; // Send mouse movement const { buttons } = e; sendRelMouseMovement(e.movementX, e.movementY, buttons); }, - [sendRelMouseMovement, settings.mouseMode], + [ + isPointerLockActive, + isPointerLockPossible, + sendRelMouseMovement, + settings.mouseMode, + ], ); const sendAbsMouseMovement = useCallback( @@ -294,7 +360,8 @@ export default function WebRTCVideo() { // console.log("KEYUP: Not focusing on the video", document.activeElement); // return; // } - console.log(document.activeElement); + + // console.log(document.activeElement); setIsNumLockActive(e.getModifierState("NumLock")); setIsCapsLockActive(e.getModifierState("CapsLock")); @@ -512,36 +579,51 @@ export default function WebRTCVideo() { // Setup Relative Mouse Events const containerRef = useRef(null); + useEffect( function setupRelativeMouseEventListeners() { if (settings.mouseMode !== "relative") return; + // Relative mouse mode should only be active if the pointer lock is active and Pointer Lock is possible + + const videoElmRefValue = videoElm.current; + if (!videoElmRefValue) return; const abortController = new AbortController(); const signal = abortController.signal; - // We bind to the larger container in relative mode because of delta between the acceleration of the local - // mouse and the mouse movement of the remote mouse. This simply makes it a bit less painful to use. - // When we get Pointer Lock support, we can remove this. - const containerElm = containerRef.current; - if (!containerElm) return; - - containerElm.addEventListener("mousemove", relMouseMoveHandler, { signal }); - containerElm.addEventListener("pointerdown", relMouseMoveHandler, { signal }); - containerElm.addEventListener("pointerup", relMouseMoveHandler, { signal }); - - containerElm.addEventListener("wheel", mouseWheelHandler, { + videoElmRefValue.addEventListener("mousemove", relMouseMoveHandler, { signal }); + videoElmRefValue.addEventListener("pointerdown", relMouseMoveHandler, { signal }); + videoElmRefValue.addEventListener("pointerup", relMouseMoveHandler, { signal }); + videoElmRefValue.addEventListener( + "click", + () => { + if (isPointerLockPossible && !document.pointerLockElement) { + requestPointerLock(); + } + }, + { signal }, + ); + videoElmRefValue.addEventListener("wheel", mouseWheelHandler, { signal, passive: true, }); const preventContextMenu = (e: MouseEvent) => e.preventDefault(); - containerElm.addEventListener("contextmenu", preventContextMenu, { signal }); + videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal }); return () => { abortController.abort(); }; }, - [settings.mouseMode, relMouseMoveHandler, mouseWheelHandler], + [ + settings.mouseMode, + relMouseMoveHandler, + mouseWheelHandler, + disableVideoFocusTrap, + requestPointerLock, + isPointerLockPossible, + isPointerLockActive, + ], ); const hasNoAutoPlayPermissions = useMemo(() => { @@ -552,33 +634,43 @@ export default function WebRTCVideo() { return true; }, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]); + const showPointerLockBar = useMemo(() => { + if (settings.mouseMode !== "relative") return false; + if (!isPointerLockPossible) return false; + if (isPointerLockActive) return false; + if (isVideoLoading) return false; + if (!isPlaying) return false; + if (videoHeight === 0 || videoWidth === 0) return false; + return true; + }, [ + settings.mouseMode, + isPointerLockPossible, + isPointerLockActive, + isVideoLoading, + isPlaying, + videoHeight, + videoWidth, + ]); + return (
    -
    +
    -
    - - videoElm.current?.requestFullscreen({ - navigationUI: "show", - }) - } - /> +
    +
    -
    +
    -
    diff --git a/ui/vite.config.ts b/ui/vite.config.ts index f6aae50..f8459cd 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -1,23 +1,32 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import tsconfigPaths from "vite-tsconfig-paths"; +import basicSsl from "@vitejs/plugin-basic-ssl"; declare const process: { env: { JETKVM_PROXY_URL: string; + USE_SSL: string; }; }; export default defineConfig(({ mode, command }) => { const isCloud = mode.indexOf("cloud") !== -1; const onDevice = mode === "device"; - const { JETKVM_PROXY_URL } = process.env; + const { JETKVM_PROXY_URL, USE_SSL } = process.env; + const useSSL = USE_SSL === "true"; + + const plugins = [tsconfigPaths(), react()]; + if (useSSL) { + plugins.push(basicSsl()); + } return { - plugins: [tsconfigPaths(), react()], + plugins, build: { outDir: isCloud ? "dist" : "../static" }, server: { host: "0.0.0.0", + https: useSSL, proxy: JETKVM_PROXY_URL ? { "/me": JETKVM_PROXY_URL, From 189b84380b6e9b54166c897f2d5e2592a01dca1e Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Wed, 16 Apr 2025 01:39:23 +0200 Subject: [PATCH 044/165] network enhanecment / refactor (#361) * chore(network): improve connectivity check * refactor(network): rewrite network and timesync component * feat(display): show cloud connection status * chore: change logging verbosity * chore(websecure): update log message * fix(ota): validate root certificate when downloading update * feat(ui): add network settings tab * fix(display): cloud connecting animation * fix: golintci issues * feat: add network settings tab * feat(timesync): query servers in parallel * refactor(network): move to internal/network package * feat(timesync): add metrics * refactor(log): move log to internal/logging package * refactor(mdms): move mdns to internal/mdns package * feat(developer): add pprof endpoint * feat(logging): add a simple logging streaming endpoint * fix(mdns): do not start mdns until network is up * feat(network): allow users to update network settings from ui * fix(network): handle errors when net.IPAddr is nil * fix(mdns): scopedLogger SIGSEGV * fix(dhcp): watch directory instead of file to catch fsnotify.Create event * refactor(nbd): move platform-specific code to different files * refactor(native): move platform-specific code to different files * chore: fix linter issues * chore(dev_deploy): allow to override PION_LOG_TRACE --- block_device.go | 28 -- block_device_linux.go | 34 ++ block_device_notlinux.go | 17 + cloud.go | 57 ++- config.go | 56 ++- dev_deploy.sh | 3 +- display.go | 139 +++++- go.mod | 2 + go.sum | 4 + hw.go | 10 + internal/confparser/confparser.go | 381 ++++++++++++++++ internal/confparser/confparser_test.go | 100 +++++ internal/confparser/utils.go | 28 ++ internal/logging/logger.go | 197 +++++++++ internal/logging/pion.go | 63 +++ internal/logging/root.go | 20 + internal/logging/sse.go | 137 ++++++ internal/logging/sse.html | 319 ++++++++++++++ internal/logging/utils.go | 32 ++ internal/mdns/mdns.go | 190 ++++++++ internal/mdns/utils.go | 1 + internal/network/config.go | 110 +++++ internal/network/dhcp.go | 11 + internal/network/hostname.go | 137 ++++++ internal/network/netif.go | 346 +++++++++++++++ internal/network/netif_linux.go | 58 +++ internal/network/netif_notlinux.go | 21 + internal/network/rpc.go | 126 ++++++ internal/network/utils.go | 26 ++ internal/timesync/http.go | 132 ++++++ internal/timesync/metrics.go | 147 +++++++ internal/timesync/ntp.go | 113 +++++ internal/timesync/rtc.go | 26 ++ internal/timesync/rtc_linux.go | 105 +++++ internal/timesync/rtc_notlinux.go | 16 + internal/timesync/timesync.go | 208 +++++++++ internal/udhcpc/options.go | 12 + internal/udhcpc/parser.go | 186 ++++++++ internal/udhcpc/parser_test.go | 74 ++++ internal/udhcpc/proc.go | 212 +++++++++ internal/udhcpc/udhcpc.go | 191 ++++++++ internal/websecure/store.go | 8 +- jsonrpc.go | 4 + log.go | 299 +------------ main.go | 38 +- mdns.go | 29 ++ native.go | 47 +- native_linux.go | 57 +++ native_notlinux.go | 12 + network.go | 288 ++++--------- ntp.go | 197 --------- ota.go | 10 +- resource/jetkvm_native | Bin 1545740 -> 1545928 bytes resource/jetkvm_native.sha256 | 2 +- timesync.go | 53 +++ ui/dev_device.sh | 10 + ui/package-lock.json | 6 + ui/package.json | 1 + ui/public/sse.html | 1 + ui/src/hooks/stores.ts | 93 +++- ui/src/main.tsx | 5 + .../routes/devices.$id.settings.network.tsx | 408 ++++++++++++++++++ ui/src/routes/devices.$id.settings.tsx | 12 + ui/src/routes/devices.$id.tsx | 9 + ui/vite.config.ts | 1 + usb.go | 2 +- usb_mass_storage.go | 7 +- video.go | 2 +- web.go | 78 +++- web_tls.go | 4 +- webrtc.go | 5 +- 71 files changed, 4938 insertions(+), 825 deletions(-) create mode 100644 block_device_linux.go create mode 100644 block_device_notlinux.go create mode 100644 internal/confparser/confparser.go create mode 100644 internal/confparser/confparser_test.go create mode 100644 internal/confparser/utils.go create mode 100644 internal/logging/logger.go create mode 100644 internal/logging/pion.go create mode 100644 internal/logging/root.go create mode 100644 internal/logging/sse.go create mode 100644 internal/logging/sse.html create mode 100644 internal/logging/utils.go create mode 100644 internal/mdns/mdns.go create mode 100644 internal/mdns/utils.go create mode 100644 internal/network/config.go create mode 100644 internal/network/dhcp.go create mode 100644 internal/network/hostname.go create mode 100644 internal/network/netif.go create mode 100644 internal/network/netif_linux.go create mode 100644 internal/network/netif_notlinux.go create mode 100644 internal/network/rpc.go create mode 100644 internal/network/utils.go create mode 100644 internal/timesync/http.go create mode 100644 internal/timesync/metrics.go create mode 100644 internal/timesync/ntp.go create mode 100644 internal/timesync/rtc.go create mode 100644 internal/timesync/rtc_linux.go create mode 100644 internal/timesync/rtc_notlinux.go create mode 100644 internal/timesync/timesync.go create mode 100644 internal/udhcpc/options.go create mode 100644 internal/udhcpc/parser.go create mode 100644 internal/udhcpc/parser_test.go create mode 100644 internal/udhcpc/proc.go create mode 100644 internal/udhcpc/udhcpc.go create mode 100644 mdns.go create mode 100644 native_linux.go create mode 100644 native_notlinux.go delete mode 100644 ntp.go create mode 100644 timesync.go create mode 120000 ui/public/sse.html create mode 100644 ui/src/routes/devices.$id.settings.network.tsx diff --git a/block_device.go b/block_device.go index e4eab80..2274098 100644 --- a/block_device.go +++ b/block_device.go @@ -7,7 +7,6 @@ import ( "os" "time" - "github.com/pojntfx/go-nbd/pkg/client" "github.com/pojntfx/go-nbd/pkg/server" "github.com/rs/zerolog" ) @@ -149,30 +148,3 @@ func (d *NBDDevice) runServerConn() { d.l.Info().Err(err).Msg("nbd server exited") } - -func (d *NBDDevice) runClientConn() { - err := client.Connect(d.clientConn, d.dev, &client.Options{ - ExportName: "jetkvm", - BlockSize: uint32(4 * 1024), - }) - d.l.Info().Err(err).Msg("nbd client exited") -} - -func (d *NBDDevice) Close() { - if d.dev != nil { - err := client.Disconnect(d.dev) - if err != nil { - d.l.Warn().Err(err).Msg("error disconnecting nbd client") - } - _ = d.dev.Close() - } - if d.listener != nil { - _ = d.listener.Close() - } - if d.clientConn != nil { - _ = d.clientConn.Close() - } - if d.serverConn != nil { - _ = d.serverConn.Close() - } -} diff --git a/block_device_linux.go b/block_device_linux.go new file mode 100644 index 0000000..8ca9372 --- /dev/null +++ b/block_device_linux.go @@ -0,0 +1,34 @@ +//go:build linux + +package kvm + +import ( + "github.com/pojntfx/go-nbd/pkg/client" +) + +func (d *NBDDevice) runClientConn() { + err := client.Connect(d.clientConn, d.dev, &client.Options{ + ExportName: "jetkvm", + BlockSize: uint32(4 * 1024), + }) + d.l.Info().Err(err).Msg("nbd client exited") +} + +func (d *NBDDevice) Close() { + if d.dev != nil { + err := client.Disconnect(d.dev) + if err != nil { + d.l.Warn().Err(err).Msg("error disconnecting nbd client") + } + _ = d.dev.Close() + } + if d.listener != nil { + _ = d.listener.Close() + } + if d.clientConn != nil { + _ = d.clientConn.Close() + } + if d.serverConn != nil { + _ = d.serverConn.Close() + } +} diff --git a/block_device_notlinux.go b/block_device_notlinux.go new file mode 100644 index 0000000..b6a9aba --- /dev/null +++ b/block_device_notlinux.go @@ -0,0 +1,17 @@ +//go:build !linux + +package kvm + +import ( + "os" +) + +func (d *NBDDevice) runClientConn() { + d.l.Error().Msg("platform not supported") + os.Exit(1) +} + +func (d *NBDDevice) Close() { + d.l.Error().Msg("platform not supported") + os.Exit(1) +} diff --git a/cloud.go b/cloud.go index fd96c41..fb1998a 100644 --- a/cloud.go +++ b/cloud.go @@ -139,11 +139,40 @@ var ( ) ) +type CloudConnectionState uint8 + +const ( + CloudConnectionStateNotConfigured CloudConnectionState = iota + CloudConnectionStateDisconnected + CloudConnectionStateConnecting + CloudConnectionStateConnected +) + var ( + cloudConnectionState CloudConnectionState = CloudConnectionStateNotConfigured + cloudConnectionStateLock = &sync.Mutex{} + cloudDisconnectChan chan error cloudDisconnectLock = &sync.Mutex{} ) +func setCloudConnectionState(state CloudConnectionState) { + cloudConnectionStateLock.Lock() + defer cloudConnectionStateLock.Unlock() + + if cloudConnectionState == CloudConnectionStateDisconnected && + (config.CloudToken == "" || config.CloudURL == "") { + state = CloudConnectionStateNotConfigured + } + + previousState := cloudConnectionState + cloudConnectionState = state + + go waitCtrlAndRequestDisplayUpdate( + previousState != state, + ) +} + func wsResetMetrics(established bool, sourceType string, source string) { metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) @@ -285,6 +314,8 @@ func runWebsocketClient() error { wsURL.Scheme = "wss" } + setCloudConnectionState(CloudConnectionStateConnecting) + header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) header.Set("X-App-Version", builtAppVersion) @@ -302,20 +333,26 @@ func runWebsocketClient() error { c, resp, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Info().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") + scopedLogger.Debug().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() + setCloudConnectionState(CloudConnectionStateConnected) + return true }, }) - // get the request id from the response header - connectionId := resp.Header.Get("X-Request-ID") - if connectionId == "" { - connectionId = resp.Header.Get("Cf-Ray") + var connectionId string + if resp != nil { + // get the request id from the response header + connectionId = resp.Header.Get("X-Request-ID") + if connectionId == "" { + connectionId = resp.Header.Get("Cf-Ray") + } } + if connectionId == "" { connectionId = uuid.New().String() scopedLogger.Warn(). @@ -332,6 +369,8 @@ func runWebsocketClient() error { if err != nil { if errors.Is(err, context.Canceled) { cloudLogger.Info().Msg("websocket connection canceled") + setCloudConnectionState(CloudConnectionStateDisconnected) + return nil } return err @@ -450,14 +489,14 @@ func RunWebsocketClient() { } // If the network is not up, well, we can't connect to the cloud. - if !networkState.Up { - cloudLogger.Warn().Msg("waiting for network to be up, will retry in 3 seconds") + if !networkState.IsOnline() { + cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds") time.Sleep(3 * time.Second) continue } // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. - if isTimeSyncNeeded() && !timeSyncSuccess { + if isTimeSyncNeeded() && !timeSync.IsSyncSuccess() { cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds") time.Sleep(3 * time.Second) continue @@ -520,6 +559,8 @@ func rpcDeregisterDevice() error { cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") disconnectCloud(fmt.Errorf("device deregistered")) + setCloudConnectionState(CloudConnectionStateNotConfigured) + return nil } diff --git a/config.go b/config.go index cf096a7..23d4c84 100644 --- a/config.go +++ b/config.go @@ -6,6 +6,8 @@ import ( "os" "sync" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/usbgadget" ) @@ -73,27 +75,28 @@ func (m *KeyboardMacro) Validate() error { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudAppURL string `json:"cloud_app_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` - KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` - EdidString string `json:"hdmi_edid_string"` - ActiveExtension string `json:"active_extension"` - DisplayMaxBrightness int `json:"display_max_brightness"` - DisplayDimAfterSec int `json:"display_dim_after_sec"` - DisplayOffAfterSec int `json:"display_off_after_sec"` - TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" - UsbConfig *usbgadget.Config `json:"usb_config"` - UsbDevices *usbgadget.Devices `json:"usb_devices"` - DefaultLogLevel string `json:"default_log_level"` + CloudURL string `json:"cloud_url"` + CloudAppURL string `json:"cloud_app_url"` + CloudToken string `json:"cloud_token"` + GoogleIdentity string `json:"google_identity"` + JigglerEnabled bool `json:"jiggler_enabled"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` + EdidString string `json:"hdmi_edid_string"` + ActiveExtension string `json:"active_extension"` + DisplayMaxBrightness int `json:"display_max_brightness"` + DisplayDimAfterSec int `json:"display_dim_after_sec"` + DisplayOffAfterSec int `json:"display_off_after_sec"` + TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" + UsbConfig *usbgadget.Config `json:"usb_config"` + UsbDevices *usbgadget.Devices `json:"usb_devices"` + NetworkConfig *network.NetworkConfig `json:"network_config"` + DefaultLogLevel string `json:"default_log_level"` } const configPath = "/userdata/kvm_config.json" @@ -121,6 +124,7 @@ var defaultConfig = &Config{ Keyboard: true, MassStorage: true, }, + NetworkConfig: &network.NetworkConfig{}, DefaultLogLevel: "INFO", } @@ -134,7 +138,7 @@ func LoadConfig() { defer configLock.Unlock() if config != nil { - logger.Info().Msg("config already loaded, skipping") + logger.Debug().Msg("config already loaded, skipping") return } @@ -164,9 +168,15 @@ func LoadConfig() { loadedConfig.UsbDevices = defaultConfig.UsbDevices } + if loadedConfig.NetworkConfig == nil { + loadedConfig.NetworkConfig = defaultConfig.NetworkConfig + } + config = &loadedConfig - rootLogger.UpdateLogLevel() + logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel) + + logger.Info().Str("path", configPath).Msg("config loaded") } func SaveConfig() error { diff --git a/dev_deploy.sh b/dev_deploy.sh index 02bbb24..d0ccaf2 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -24,6 +24,7 @@ show_help() { REMOTE_USER="root" REMOTE_PATH="/userdata/jetkvm/bin" SKIP_UI_BUILD=false +LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -91,7 +92,7 @@ cd "${REMOTE_PATH}" chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug +PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug EOF echo "Deployment complete." diff --git a/display.go b/display.go index cbe9ddd..e2e82e1 100644 --- a/display.go +++ b/display.go @@ -33,50 +33,153 @@ func switchToScreen(screen string) { var displayedTexts = make(map[string]string) +func lvObjSetState(objName string, state string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state}) +} + +func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag}) +} + +func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag}) +} + +func lvObjHide(objName string) (*CtrlResponse, error) { + return lvObjAddFlag(objName, "LV_OBJ_FLAG_HIDDEN") +} + +func lvObjShow(objName string) (*CtrlResponse, error) { + return lvObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN") +} + +func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused + return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity}) +} + +func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvLabelSetText(objName string, text string) (*CtrlResponse, error) { + return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text}) +} + +func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) { + return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src}) +} + func updateLabelIfChanged(objName string, newText string) { if newText != "" && newText != displayedTexts[objName] { - _, _ = CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": newText}) + _, _ = lvLabelSetText(objName, newText) displayedTexts[objName] = newText } } func switchToScreenIfDifferent(screenName string) { - displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") if currentScreen != screenName { + displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") switchToScreen(screenName) } } +var ( + cloudBlinkLock sync.Mutex = sync.Mutex{} + cloudBlinkStopped bool + cloudBlinkTicker *time.Ticker +) + func updateDisplay() { - updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4) + updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String()) if usbState == "configured" { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_DEFAULT"}) + _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_DEFAULT") } else { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_USER_2"}) + _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_USER_2") } if lastVideoState.Ready { updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_DEFAULT"}) + _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_DEFAULT") } else { updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_USER_2"}) + _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_USER_2") } updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions)) - if networkState.Up { + + if networkState.IsUp() { switchToScreenIfDifferent("ui_Home_Screen") } else { switchToScreenIfDifferent("ui_No_Network_Screen") } + + if cloudConnectionState == CloudConnectionStateNotConfigured { + _, _ = lvObjHide("ui_Home_Header_Cloud_Status_Icon") + } else { + _, _ = lvObjShow("ui_Home_Header_Cloud_Status_Icon") + } + + switch cloudConnectionState { + case CloudConnectionStateDisconnected: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png") + stopCloudBlink() + case CloudConnectionStateConnecting: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + startCloudBlink() + case CloudConnectionStateConnected: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + stopCloudBlink() + } +} + +func startCloudBlink() { + if cloudBlinkTicker == nil { + cloudBlinkTicker = time.NewTicker(2 * time.Second) + } else { + // do nothing if the blink isn't stopped + if cloudBlinkStopped { + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + + cloudBlinkStopped = false + cloudBlinkTicker.Reset(2 * time.Second) + } + } + + go func() { + for range cloudBlinkTicker.C { + if cloudConnectionState != CloudConnectionStateConnecting { + continue + } + _, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + } + }() +} + +func stopCloudBlink() { + if cloudBlinkTicker != nil { + cloudBlinkTicker.Stop() + } + + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + cloudBlinkStopped = true } var ( displayInited = false displayUpdateLock = sync.Mutex{} + waitDisplayUpdate = sync.Mutex{} ) -func requestDisplayUpdate() { +func requestDisplayUpdate(shouldWakeDisplay bool) { displayUpdateLock.Lock() defer displayUpdateLock.Unlock() @@ -85,16 +188,26 @@ func requestDisplayUpdate() { return } go func() { - wakeDisplay(false) - displayLogger.Info().Msg("display updating") + if shouldWakeDisplay { + wakeDisplay(false) + } + displayLogger.Debug().Msg("display updating") //TODO: only run once regardless how many pending updates updateDisplay() }() } +func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) { + waitDisplayUpdate.Lock() + defer waitDisplayUpdate.Unlock() + + waitCtrlClientConnected() + requestDisplayUpdate(shouldWakeDisplay) +} + func updateStaticContents() { //contents that never change - updateLabelIfChanged("ui_Home_Content_Mac", networkState.MAC) + updateLabelIfChanged("ui_Home_Content_Mac", networkState.MACString()) systemVersion, appVersion, err := GetLocalVersion() if err == nil { updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String()) @@ -265,7 +378,7 @@ func init() { displayLogger.Info().Msg("display inited") startBacklightTickers() wakeDisplay(true) - requestDisplayUpdate() + requestDisplayUpdate(true) }() go watchTsEvents() diff --git a/go.mod b/go.mod index 1311a33..6784a59 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 + github.com/fsnotify/fsnotify v1.9.0 github.com/gin-contrib/logger v1.2.5 github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 @@ -44,6 +45,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/guregu/null/v6 v6.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect diff --git a/go.sum b/go.sum index 565c0cc..3ad832a 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= @@ -54,6 +56,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= +github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= diff --git a/hw.go b/hw.go index 21bffad..20d88eb 100644 --- a/hw.go +++ b/hw.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strings" "sync" "time" ) @@ -51,6 +52,15 @@ func GetDeviceID() string { return deviceID } +func GetDefaultHostname() string { + deviceId := GetDeviceID() + if deviceId == "unknown_device_id" { + return "jetkvm" + } + + return fmt.Sprintf("jetkvm-%s", strings.ToLower(deviceId)) +} + func runWatchdog() { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) if err != nil { diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go new file mode 100644 index 0000000..76102a3 --- /dev/null +++ b/internal/confparser/confparser.go @@ -0,0 +1,381 @@ +package confparser + +import ( + "fmt" + "net" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/guregu/null/v6" + "golang.org/x/net/idna" +) + +type FieldConfig struct { + Name string + Required bool + RequiredIf map[string]interface{} + OneOf []string + ValidateTypes []string + Defaults interface{} + IsEmpty bool + CurrentValue interface{} + TypeString string + Delegated bool + shouldUpdateValue bool +} + +func SetDefaultsAndValidate(config interface{}) error { + return setDefaultsAndValidate(config, true) +} + +func setDefaultsAndValidate(config interface{}, isRoot bool) error { + // first we need to check if the config is a pointer + if reflect.TypeOf(config).Kind() != reflect.Ptr { + return fmt.Errorf("config is not a pointer") + } + + // now iterate over the lease struct and set the values + configType := reflect.TypeOf(config).Elem() + configValue := reflect.ValueOf(config).Elem() + + fields := make(map[string]FieldConfig) + + for i := 0; i < configType.NumField(); i++ { + field := configType.Field(i) + fieldValue := configValue.Field(i) + + defaultValue := field.Tag.Get("default") + + fieldType := field.Type.String() + + fieldConfig := FieldConfig{ + Name: field.Name, + OneOf: splitString(field.Tag.Get("one_of")), + ValidateTypes: splitString(field.Tag.Get("validate_type")), + RequiredIf: make(map[string]interface{}), + CurrentValue: fieldValue.Interface(), + IsEmpty: false, + TypeString: fieldType, + } + + // check if the field is required + required := field.Tag.Get("required") + if required != "" { + requiredBool, _ := strconv.ParseBool(required) + fieldConfig.Required = requiredBool + } + + var canUseOneOff = false + + // use switch to get the type + switch fieldValue.Interface().(type) { + case string, null.String: + if defaultValue != "" { + fieldConfig.Defaults = defaultValue + } + canUseOneOff = true + case []string: + if defaultValue != "" { + fieldConfig.Defaults = strings.Split(defaultValue, ",") + } + canUseOneOff = true + case int, null.Int: + if defaultValue != "" { + defaultValueInt, err := strconv.Atoi(defaultValue) + if err != nil { + return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue) + } + + fieldConfig.Defaults = defaultValueInt + } + case bool, null.Bool: + if defaultValue != "" { + defaultValueBool, err := strconv.ParseBool(defaultValue) + if err != nil { + return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue) + } + + fieldConfig.Defaults = defaultValueBool + } + default: + if defaultValue != "" { + return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", field.Name, fieldType) + } + + // check if it's a pointer + if fieldValue.Kind() == reflect.Ptr { + // check if the pointer is nil + if fieldValue.IsNil() { + fieldConfig.IsEmpty = true + } else { + fieldConfig.CurrentValue = fieldValue.Elem().Addr() + fieldConfig.Delegated = true + } + } else { + fieldConfig.Delegated = true + } + } + + // now check if the field is nullable interface + switch fieldValue.Interface().(type) { + case null.String: + if fieldValue.Interface().(null.String).IsZero() { + fieldConfig.IsEmpty = true + } + case null.Int: + if fieldValue.Interface().(null.Int).IsZero() { + fieldConfig.IsEmpty = true + } + case null.Bool: + if fieldValue.Interface().(null.Bool).IsZero() { + fieldConfig.IsEmpty = true + } + case []string: + if len(fieldValue.Interface().([]string)) == 0 { + fieldConfig.IsEmpty = true + } + } + + // now check if the field has required_if + requiredIf := field.Tag.Get("required_if") + if requiredIf != "" { + requiredIfParts := strings.Split(requiredIf, ",") + for _, part := range requiredIfParts { + partVal := strings.SplitN(part, "=", 2) + if len(partVal) != 2 { + return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf) + } + + fieldConfig.RequiredIf[partVal[0]] = partVal[1] + } + } + + // check if the field can use one_of + if !canUseOneOff && len(fieldConfig.OneOf) > 0 { + return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", field.Name, fieldType) + } + + fields[field.Name] = fieldConfig + } + + if err := validateFields(config, fields); err != nil { + return err + } + + return nil +} + +func validateFields(config interface{}, fields map[string]FieldConfig) error { + // now we can start to validate the fields + for _, fieldConfig := range fields { + if err := fieldConfig.validate(fields); err != nil { + return err + } + + fieldConfig.populate(config) + } + + return nil +} + +func (f *FieldConfig) validate(fields map[string]FieldConfig) error { + var required bool + var err error + + if required, err = f.validateRequired(fields); err != nil { + return err + } + + // check if the field needs to be updated and set defaults if needed + if err := f.checkIfFieldNeedsUpdate(); err != nil { + return err + } + + // then we can check if the field is one_of + if err := f.validateOneOf(); err != nil { + return err + } + + // and validate the type + if err := f.validateField(); err != nil { + return err + } + + // if the field is delegated, we need to validate the nested field + // but before that, let's check if the field is required + if required && f.Delegated { + if err := setDefaultsAndValidate(f.CurrentValue.(reflect.Value).Interface(), false); err != nil { + return err + } + } + + return nil +} + +func (f *FieldConfig) populate(config interface{}) { + // update the field if it's not empty + if !f.shouldUpdateValue { + return + } + + reflect.ValueOf(config).Elem().FieldByName(f.Name).Set(reflect.ValueOf(f.CurrentValue)) +} + +func (f *FieldConfig) checkIfFieldNeedsUpdate() error { + // populate the field if it's empty and has a default value + if f.IsEmpty && f.Defaults != nil { + switch f.CurrentValue.(type) { + case null.String: + f.CurrentValue = null.StringFrom(f.Defaults.(string)) + case null.Int: + f.CurrentValue = null.IntFrom(int64(f.Defaults.(int))) + case null.Bool: + f.CurrentValue = null.BoolFrom(f.Defaults.(bool)) + case string: + f.CurrentValue = f.Defaults.(string) + case int: + f.CurrentValue = f.Defaults.(int) + case bool: + f.CurrentValue = f.Defaults.(bool) + case []string: + f.CurrentValue = f.Defaults.([]string) + default: + return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", f.Name, f.TypeString) + } + + f.shouldUpdateValue = true + } + + return nil +} + +func (f *FieldConfig) validateRequired(fields map[string]FieldConfig) (bool, error) { + var required = f.Required + + // if the field is not required, we need to check if it's required_if + if !required && len(f.RequiredIf) > 0 { + for key, value := range f.RequiredIf { + // check if the field's result matches the required_if + // right now we only support string and int + requiredField, ok := fields[key] + if !ok { + return required, fmt.Errorf("required_if field `%s` not found", key) + } + + switch requiredField.CurrentValue.(type) { + case string: + if requiredField.CurrentValue.(string) == value.(string) { + required = true + } + case int: + if requiredField.CurrentValue.(int) == value.(int) { + required = true + } + case null.String: + if !requiredField.CurrentValue.(null.String).IsZero() && + requiredField.CurrentValue.(null.String).String == value.(string) { + required = true + } + case null.Int: + if !requiredField.CurrentValue.(null.Int).IsZero() && + requiredField.CurrentValue.(null.Int).Int64 == value.(int64) { + required = true + } + } + + // if the field is required, we can break the loop + // because we only need one of the required_if fields to be true + if required { + break + } + } + } + + if required && f.IsEmpty { + return false, fmt.Errorf("field `%s` is required", f.Name) + } + + return required, nil +} + +func checkIfSliceContains(slice []string, one_of []string) bool { + for _, oneOf := range one_of { + if slices.Contains(slice, oneOf) { + return true + } + } + + return false +} + +func (f *FieldConfig) validateOneOf() error { + if len(f.OneOf) == 0 { + return nil + } + + var val []string + switch f.CurrentValue.(type) { + case string: + val = []string{f.CurrentValue.(string)} + case null.String: + val = []string{f.CurrentValue.(null.String).String} + case []string: + // let's validate the value here + val = f.CurrentValue.([]string) + default: + return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", f.Name, f.TypeString) + } + + if !checkIfSliceContains(val, f.OneOf) { + return fmt.Errorf( + "field `%s` is not one of the allowed values: %s, current value: %s", + f.Name, + strings.Join(f.OneOf, ", "), + strings.Join(val, ", "), + ) + } + + return nil +} + +func (f *FieldConfig) validateField() error { + if len(f.ValidateTypes) == 0 || f.IsEmpty { + return nil + } + + val, err := toString(f.CurrentValue) + if err != nil { + return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err) + } + + if val == "" { + return nil + } + + for _, validateType := range f.ValidateTypes { + switch validateType { + case "ipv4": + if net.ParseIP(val).To4() == nil { + return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val) + } + case "ipv6": + if net.ParseIP(val).To16() == nil { + return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val) + } + case "hwaddr": + if _, err := net.ParseMAC(val); err != nil { + return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val) + } + case "hostname": + if _, err := idna.Lookup.ToASCII(val); err != nil { + return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val) + } + default: + return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType) + } + } + + return nil +} diff --git a/internal/confparser/confparser_test.go b/internal/confparser/confparser_test.go new file mode 100644 index 0000000..dd5e00a --- /dev/null +++ b/internal/confparser/confparser_test.go @@ -0,0 +1,100 @@ +package confparser + +import ( + "net" + "testing" + "time" + + "github.com/guregu/null/v6" +) + +type testIPv6Address struct { //nolint:unused + Address net.IP `json:"address"` + Prefix net.IPNet `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type testIPv4StaticConfig struct { + Address null.String `json:"address" validate_type:"ipv4" required:"true"` + Netmask null.String `json:"netmask" validate_type:"ipv4" required:"true"` + Gateway null.String `json:"gateway" validate_type:"ipv4" required:"true"` + DNS []string `json:"dns" validate_type:"ipv4" required:"true"` +} + +type testIPv6StaticConfig struct { + Address null.String `json:"address" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"` + Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"` + DNS []string `json:"dns" validate_type:"ipv6" required:"true"` +} +type testNetworkConfig struct { + Hostname null.String `json:"hostname,omitempty"` + Domain null.String `json:"domain,omitempty"` + + IPv4Mode null.String `json:"ipv4_mode" one_of:"dhcp,static,disabled" default:"dhcp"` + IPv4Static *testIPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` + + IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` + IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` + + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` +} + +func TestValidateConfig(t *testing.T) { + config := &testNetworkConfig{} + + err := SetDefaultsAndValidate(config) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestValidateIPv4StaticConfigRequired(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("192.168.1.1"), + Gateway: null.StringFrom("192.168.1.1"), + }, + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestValidateIPv4StaticConfigRequiredIf(t *testing.T) { + config := &testNetworkConfig{ + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestValidateIPv4StaticConfigValidateType(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("X"), + Netmask: null.StringFrom("255.255.255.0"), + Gateway: null.StringFrom("192.168.1.1"), + DNS: []string{"8.8.8.8", "8.8.4.4"}, + }, + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} diff --git a/internal/confparser/utils.go b/internal/confparser/utils.go new file mode 100644 index 0000000..a46871e --- /dev/null +++ b/internal/confparser/utils.go @@ -0,0 +1,28 @@ +package confparser + +import ( + "fmt" + "reflect" + "strings" + + "github.com/guregu/null/v6" +) + +func splitString(s string) []string { + if s == "" { + return []string{} + } + + return strings.Split(s, ",") +} + +func toString(v interface{}) (string, error) { + switch v := v.(type) { + case string: + return v, nil + case null.String: + return v.String, nil + } + + return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v)) +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..39156ec --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,197 @@ +package logging + +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/rs/zerolog" +) + +type Logger struct { + l *zerolog.Logger + scopeLoggers map[string]*zerolog.Logger + scopeLevels map[string]zerolog.Level + scopeLevelMutex sync.Mutex + + defaultLogLevelFromEnv zerolog.Level + defaultLogLevelFromConfig zerolog.Level + defaultLogLevel zerolog.Level +} + +const ( + defaultLogLevel = zerolog.ErrorLevel +) + +type logOutput struct { + mu *sync.Mutex +} + +func (w *logOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + // TODO: write to file or syslog + if sseServer != nil { + // use a goroutine to avoid blocking the Write method + go func() { + sseServer.Message <- string(p) + }() + } + return len(p), nil +} + +var ( + consoleLogOutput io.Writer = zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + PartsOrder: []string{"time", "level", "scope", "component", "message"}, + FieldsExclude: []string{"scope", "component"}, + FormatPartValueByName: func(value interface{}, name string) string { + val := fmt.Sprintf("%s", value) + if name == "component" { + if value == nil { + return "-" + } + } + return val + }, + } + fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}} + defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput) + + zerologLevels = map[string]zerolog.Level{ + "DISABLE": zerolog.Disabled, + "NOLEVEL": zerolog.NoLevel, + "PANIC": zerolog.PanicLevel, + "FATAL": zerolog.FatalLevel, + "ERROR": zerolog.ErrorLevel, + "WARN": zerolog.WarnLevel, + "INFO": zerolog.InfoLevel, + "DEBUG": zerolog.DebugLevel, + "TRACE": zerolog.TraceLevel, + } +) + +func NewLogger(zerologLogger zerolog.Logger) *Logger { + return &Logger{ + l: &zerologLogger, + scopeLoggers: make(map[string]*zerolog.Logger), + scopeLevels: make(map[string]zerolog.Level), + scopeLevelMutex: sync.Mutex{}, + defaultLogLevelFromEnv: -2, + defaultLogLevelFromConfig: -2, + defaultLogLevel: defaultLogLevel, + } +} + +func (l *Logger) updateLogLevel() { + l.scopeLevelMutex.Lock() + defer l.scopeLevelMutex.Unlock() + + l.scopeLevels = make(map[string]zerolog.Level) + + finalDefaultLogLevel := l.defaultLogLevel + + for name, level := range zerologLevels { + env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) + + if env == "" { + env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) + } + + if env == "" { + env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) + } + + if env == "" { + continue + } + + if strings.ToLower(env) == "all" { + l.defaultLogLevelFromEnv = level + + if finalDefaultLogLevel > level { + finalDefaultLogLevel = level + } + + continue + } + + scopes := strings.Split(strings.ToLower(env), ",") + for _, scope := range scopes { + l.scopeLevels[scope] = level + } + } + + l.defaultLogLevel = finalDefaultLogLevel +} + +func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level { + if l.scopeLevels == nil { + l.updateLogLevel() + } + + var scopeLevel zerolog.Level + if l.defaultLogLevelFromConfig != -2 { + scopeLevel = l.defaultLogLevelFromConfig + } + if l.defaultLogLevelFromEnv != -2 { + scopeLevel = l.defaultLogLevelFromEnv + } + + // if the scope is not in the map, use the default level from the root logger + if level, ok := l.scopeLevels[scope]; ok { + scopeLevel = level + } + + return scopeLevel +} + +func (l *Logger) newScopeLogger(scope string) zerolog.Logger { + scopeLevel := l.getScopeLoggerLevel(scope) + logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger() + + return logger +} + +func (l *Logger) getLogger(scope string) *zerolog.Logger { + logger, ok := l.scopeLoggers[scope] + if !ok || logger == nil { + scopeLogger := l.newScopeLogger(scope) + l.scopeLoggers[scope] = &scopeLogger + } + + return l.scopeLoggers[scope] +} + +func (l *Logger) UpdateLogLevel(configDefaultLogLevel string) { + needUpdate := false + + if configDefaultLogLevel != "" { + if logLevel, ok := zerologLevels[configDefaultLogLevel]; ok { + l.defaultLogLevelFromConfig = logLevel + } else { + l.l.Warn().Str("logLevel", configDefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR") + } + + if l.defaultLogLevelFromConfig != l.defaultLogLevel { + needUpdate = true + } + } + + l.updateLogLevel() + + if needUpdate { + for scope, logger := range l.scopeLoggers { + currentLevel := logger.GetLevel() + targetLevel := l.getScopeLoggerLevel(scope) + if currentLevel != targetLevel { + *logger = l.newScopeLogger(scope) + } + } + } +} diff --git a/internal/logging/pion.go b/internal/logging/pion.go new file mode 100644 index 0000000..453b8bc --- /dev/null +++ b/internal/logging/pion.go @@ -0,0 +1,63 @@ +package logging + +import ( + "github.com/pion/logging" + "github.com/rs/zerolog" +) + +type pionLogger struct { + logger *zerolog.Logger +} + +// Print all messages except trace. +func (c pionLogger) Trace(msg string) { + c.logger.Trace().Msg(msg) +} +func (c pionLogger) Tracef(format string, args ...interface{}) { + c.logger.Trace().Msgf(format, args...) +} + +func (c pionLogger) Debug(msg string) { + c.logger.Debug().Msg(msg) +} +func (c pionLogger) Debugf(format string, args ...interface{}) { + c.logger.Debug().Msgf(format, args...) +} +func (c pionLogger) Info(msg string) { + c.logger.Info().Msg(msg) +} +func (c pionLogger) Infof(format string, args ...interface{}) { + c.logger.Info().Msgf(format, args...) +} +func (c pionLogger) Warn(msg string) { + c.logger.Warn().Msg(msg) +} +func (c pionLogger) Warnf(format string, args ...interface{}) { + c.logger.Warn().Msgf(format, args...) +} +func (c pionLogger) Error(msg string) { + c.logger.Error().Msg(msg) +} +func (c pionLogger) Errorf(format string, args ...interface{}) { + c.logger.Error().Msgf(format, args...) +} + +// customLoggerFactory satisfies the interface logging.LoggerFactory +// This allows us to create different loggers per subsystem. So we can +// add custom behavior. +type pionLoggerFactory struct{} + +func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { + logger := rootLogger.getLogger(subsystem).With(). + Str("scope", "pion"). + Str("component", subsystem). + Logger() + + return pionLogger{logger: &logger} +} + +var defaultLoggerFactory = &pionLoggerFactory{} + +func GetPionDefaultLoggerFactory() logging.LoggerFactory { + return defaultLoggerFactory +} diff --git a/internal/logging/root.go b/internal/logging/root.go new file mode 100644 index 0000000..397ca64 --- /dev/null +++ b/internal/logging/root.go @@ -0,0 +1,20 @@ +package logging + +import "github.com/rs/zerolog" + +var ( + rootZerologLogger = zerolog.New(defaultLogOutput).With(). + Str("scope", "jetkvm"). + Timestamp(). + Stack(). + Logger() + rootLogger = NewLogger(rootZerologLogger) +) + +func GetRootLogger() *Logger { + return rootLogger +} + +func GetSubsystemLogger(subsystem string) *zerolog.Logger { + return rootLogger.getLogger(subsystem) +} diff --git a/internal/logging/sse.go b/internal/logging/sse.go new file mode 100644 index 0000000..05e6e9e --- /dev/null +++ b/internal/logging/sse.go @@ -0,0 +1,137 @@ +package logging + +import ( + "embed" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" +) + +//go:embed sse.html +var sseHTML embed.FS + +type sseEvent struct { + Message chan string + NewClients chan chan string + ClosedClients chan chan string + TotalClients map[chan string]bool +} + +// New event messages are broadcast to all registered client connection channels +type sseClientChan chan string + +var ( + sseServer *sseEvent + sseLogger *zerolog.Logger +) + +func init() { + sseServer = newSseServer() + sseLogger = GetSubsystemLogger("sse") +} + +// Initialize event and Start procnteessing requests +func newSseServer() (event *sseEvent) { + event = &sseEvent{ + Message: make(chan string), + NewClients: make(chan chan string), + ClosedClients: make(chan chan string), + TotalClients: make(map[chan string]bool), + } + + go event.listen() + + return +} + +// It Listens all incoming requests from clients. +// Handles addition and removal of clients and broadcast messages to clients. +func (stream *sseEvent) listen() { + for { + select { + // Add new available client + case client := <-stream.NewClients: + stream.TotalClients[client] = true + sseLogger.Info(). + Int("total_clients", len(stream.TotalClients)). + Msg("new client connected") + + // Remove closed client + case client := <-stream.ClosedClients: + delete(stream.TotalClients, client) + close(client) + sseLogger.Info().Int("total_clients", len(stream.TotalClients)).Msg("client disconnected") + + // Broadcast message to client + case eventMsg := <-stream.Message: + for clientMessageChan := range stream.TotalClients { + select { + case clientMessageChan <- eventMsg: + // Message sent successfully + default: + // Failed to send, dropping message + } + } + } + } +} + +func (stream *sseEvent) serveHTTP() gin.HandlerFunc { + return func(c *gin.Context) { + clientChan := make(sseClientChan) + stream.NewClients <- clientChan + + go func() { + <-c.Writer.CloseNotify() + + for range clientChan { + } + + stream.ClosedClients <- clientChan + }() + + c.Set("clientChan", clientChan) + c.Next() + } +} + +func sseHeadersMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Method == "GET" && c.NegotiateFormat(gin.MIMEHTML) == gin.MIMEHTML { + c.FileFromFS("/sse.html", http.FS(sseHTML)) + c.Status(http.StatusOK) + c.Abort() + return + } + + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Next() + } +} + +func AttachSSEHandler(router *gin.RouterGroup) { + router.StaticFS("/log-stream", http.FS(sseHTML)) + router.GET("/log-stream", sseHeadersMiddleware(), sseServer.serveHTTP(), func(c *gin.Context) { + v, ok := c.Get("clientChan") + if !ok { + return + } + clientChan, ok := v.(sseClientChan) + if !ok { + return + } + c.Stream(func(w io.Writer) bool { + if msg, ok := <-clientChan; ok { + c.SSEvent("message", msg) + return true + } + return false + }) + }) +} diff --git a/internal/logging/sse.html b/internal/logging/sse.html new file mode 100644 index 0000000..192b464 --- /dev/null +++ b/internal/logging/sse.html @@ -0,0 +1,319 @@ + + + + + + Server Sent Event + + + + +
    + +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/internal/logging/utils.go b/internal/logging/utils.go new file mode 100644 index 0000000..e622d96 --- /dev/null +++ b/internal/logging/utils.go @@ -0,0 +1,32 @@ +package logging + +import ( + "fmt" + "os" + + "github.com/rs/zerolog" +) + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +func GetDefaultLogger() *zerolog.Logger { + return &defaultLogger +} + +func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { + // TODO: move rootLogger to logging package + if l == nil { + l = &defaultLogger + } + + l.Error().Err(err).Msgf(format, args...) + + if err == nil { + return fmt.Errorf(format, args...) + } + + err_msg := err.Error() + ": %v" + err_args := append(args, err) + + return fmt.Errorf(err_msg, err_args...) +} diff --git a/internal/mdns/mdns.go b/internal/mdns/mdns.go new file mode 100644 index 0000000..b882b93 --- /dev/null +++ b/internal/mdns/mdns.go @@ -0,0 +1,190 @@ +package mdns + +import ( + "fmt" + "net" + "reflect" + "strings" + "sync" + + "github.com/jetkvm/kvm/internal/logging" + pion_mdns "github.com/pion/mdns/v2" + "github.com/rs/zerolog" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type MDNS struct { + conn *pion_mdns.Conn + lock sync.Mutex + l *zerolog.Logger + + localNames []string + listenOptions *MDNSListenOptions +} + +type MDNSListenOptions struct { + IPv4 bool + IPv6 bool +} + +type MDNSOptions struct { + Logger *zerolog.Logger + LocalNames []string + ListenOptions *MDNSListenOptions +} + +const ( + DefaultAddressIPv4 = pion_mdns.DefaultAddressIPv4 + DefaultAddressIPv6 = pion_mdns.DefaultAddressIPv6 +) + +func NewMDNS(opts *MDNSOptions) (*MDNS, error) { + if opts.Logger == nil { + opts.Logger = logging.GetDefaultLogger() + } + + if opts.ListenOptions == nil { + opts.ListenOptions = &MDNSListenOptions{ + IPv4: true, + IPv6: true, + } + } + + return &MDNS{ + l: opts.Logger, + lock: sync.Mutex{}, + localNames: opts.LocalNames, + listenOptions: opts.ListenOptions, + }, nil +} + +func (m *MDNS) start(allowRestart bool) error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn != nil { + if !allowRestart { + return fmt.Errorf("mDNS server already running") + } + + m.conn.Close() + } + + if m.listenOptions == nil { + return fmt.Errorf("listen options not set") + } + + if !m.listenOptions.IPv4 && !m.listenOptions.IPv6 { + m.l.Info().Msg("mDNS server disabled") + return nil + } + + var ( + addr4, addr6 *net.UDPAddr + l4, l6 *net.UDPConn + p4 *ipv4.PacketConn + p6 *ipv6.PacketConn + err error + ) + + if m.listenOptions.IPv4 { + addr4, err = net.ResolveUDPAddr("udp4", DefaultAddressIPv4) + if err != nil { + return err + } + + l4, err = net.ListenUDP("udp4", addr4) + if err != nil { + return err + } + + p4 = ipv4.NewPacketConn(l4) + } + + if m.listenOptions.IPv6 { + addr6, err = net.ResolveUDPAddr("udp6", DefaultAddressIPv6) + if err != nil { + return err + } + + l6, err = net.ListenUDP("udp6", addr6) + if err != nil { + return err + } + + p6 = ipv6.NewPacketConn(l6) + } + + scopeLogger := m.l.With(). + Interface("local_names", m.localNames). + Bool("ipv4", m.listenOptions.IPv4). + Bool("ipv6", m.listenOptions.IPv6). + Logger() + + newLocalNames := make([]string, len(m.localNames)) + for i, name := range m.localNames { + newLocalNames[i] = strings.TrimRight(strings.ToLower(name), ".") + if !strings.HasSuffix(newLocalNames[i], ".local") { + newLocalNames[i] = newLocalNames[i] + ".local" + } + } + + mDNSConn, err := pion_mdns.Server(p4, p6, &pion_mdns.Config{ + LocalNames: newLocalNames, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), + }) + + if err != nil { + scopeLogger.Warn().Err(err).Msg("failed to start mDNS server") + return err + } + + m.conn = mDNSConn + scopeLogger.Info().Msg("mDNS server started") + + return nil +} + +func (m *MDNS) Start() error { + return m.start(false) +} + +func (m *MDNS) Restart() error { + return m.start(true) +} + +func (m *MDNS) Stop() error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn == nil { + return nil + } + + return m.conn.Close() +} + +func (m *MDNS) SetLocalNames(localNames []string, always bool) error { + if reflect.DeepEqual(m.localNames, localNames) && !always { + return nil + } + + m.localNames = localNames + _ = m.Restart() + + return nil +} + +func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error { + if m.listenOptions != nil && + m.listenOptions.IPv4 == listenOptions.IPv4 && + m.listenOptions.IPv6 == listenOptions.IPv6 { + return nil + } + + m.listenOptions = listenOptions + _ = m.Restart() + + return nil +} diff --git a/internal/mdns/utils.go b/internal/mdns/utils.go new file mode 100644 index 0000000..7565eee --- /dev/null +++ b/internal/mdns/utils.go @@ -0,0 +1 @@ +package mdns diff --git a/internal/network/config.go b/internal/network/config.go new file mode 100644 index 0000000..74ddf19 --- /dev/null +++ b/internal/network/config.go @@ -0,0 +1,110 @@ +package network + +import ( + "fmt" + "net" + "time" + + "github.com/guregu/null/v6" + "github.com/jetkvm/kvm/internal/mdns" + "golang.org/x/net/idna" +) + +type IPv6Address struct { + Address net.IP `json:"address"` + Prefix net.IPNet `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type IPv4StaticConfig struct { + Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"` + Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"` + Gateway null.String `json:"gateway,omitempty" validate_type:"ipv4" required:"true"` + DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"` +} + +type IPv6StaticConfig struct { + Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"` + Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"` + DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` +} +type NetworkConfig struct { + Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` + Domain null.String `json:"domain,omitempty" validate_type:"hostname"` + + IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"` + IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` + + IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` + IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` + + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` +} + +func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { + mode := c.MDNSMode.String + listenOptions := &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + } + + switch mode { + case "ipv4_only": + listenOptions.IPv6 = false + case "ipv6_only": + listenOptions.IPv4 = false + case "disabled": + listenOptions.IPv4 = false + listenOptions.IPv6 = false + } + + return listenOptions +} +func (s *NetworkInterfaceState) GetHostname() string { + hostname := ToValidHostname(s.config.Hostname.String) + + if hostname == "" { + return s.defaultHostname + } + + return hostname +} + +func ToValidDomain(domain string) string { + ascii, err := idna.Lookup.ToASCII(domain) + if err != nil { + return "" + } + + return ascii +} + +func (s *NetworkInterfaceState) GetDomain() string { + domain := ToValidDomain(s.config.Domain.String) + + if domain == "" { + lease := s.dhcpClient.GetLease() + if lease != nil && lease.Domain != "" { + domain = ToValidDomain(lease.Domain) + } + } + + if domain == "" { + return "local" + } + + return domain +} + +func (s *NetworkInterfaceState) GetFQDN() string { + return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain()) +} diff --git a/internal/network/dhcp.go b/internal/network/dhcp.go new file mode 100644 index 0000000..9e173cc --- /dev/null +++ b/internal/network/dhcp.go @@ -0,0 +1,11 @@ +package network + +type DhcpTargetState int + +const ( + DhcpTargetStateDoNothing DhcpTargetState = iota + DhcpTargetStateStart + DhcpTargetStateStop + DhcpTargetStateRenew + DhcpTargetStateRelease +) diff --git a/internal/network/hostname.go b/internal/network/hostname.go new file mode 100644 index 0000000..d75255c --- /dev/null +++ b/internal/network/hostname.go @@ -0,0 +1,137 @@ +package network + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + + "golang.org/x/net/idna" +) + +const ( + hostnamePath = "/etc/hostname" + hostsPath = "/etc/hosts" +) + +var ( + hostnameLock sync.Mutex = sync.Mutex{} +) + +func updateEtcHosts(hostname string, fqdn string) error { + // update /etc/hosts + hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive) + if err != nil { + return fmt.Errorf("failed to open %s: %w", hostsPath, err) + } + defer hostsFile.Close() + + // read all lines + if _, err := hostsFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to seek %s: %w", hostsPath, err) + } + + lines, err := io.ReadAll(hostsFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", hostsPath, err) + } + + newLines := []string{} + hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn) + hostLineExists := false + + for _, line := range strings.Split(string(lines), "\n") { + if strings.HasPrefix(line, "127.0.1.1") { + hostLineExists = true + line = hostLine + } + newLines = append(newLines, line) + } + + if !hostLineExists { + newLines = append(newLines, hostLine) + } + + if err := hostsFile.Truncate(0); err != nil { + return fmt.Errorf("failed to truncate %s: %w", hostsPath, err) + } + + if _, err := hostsFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to seek %s: %w", hostsPath, err) + } + + if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil { + return fmt.Errorf("failed to write %s: %w", hostsPath, err) + } + + return nil +} + +func ToValidHostname(hostname string) string { + ascii, err := idna.Lookup.ToASCII(hostname) + if err != nil { + return "" + } + return ascii +} + +func SetHostname(hostname string, fqdn string) error { + hostnameLock.Lock() + defer hostnameLock.Unlock() + + hostname = ToValidHostname(strings.TrimSpace(hostname)) + fqdn = ToValidHostname(strings.TrimSpace(fqdn)) + + if hostname == "" { + return fmt.Errorf("invalid hostname: %s", hostname) + } + + if fqdn == "" { + fqdn = hostname + } + + // update /etc/hostname + if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", hostnamePath, err) + } + + // update /etc/hosts + if err := updateEtcHosts(hostname, fqdn); err != nil { + return fmt.Errorf("failed to update /etc/hosts: %w", err) + } + + // run hostname + if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil { + return fmt.Errorf("failed to run hostname: %w", err) + } + + return nil +} + +func (s *NetworkInterfaceState) setHostnameIfNotSame() error { + hostname := s.GetHostname() + currentHostname, _ := os.Hostname() + + fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain()) + + if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname { + return nil + } + + scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger() + + err := SetHostname(hostname, fqdn) + if err != nil { + scopedLogger.Error().Err(err).Msg("failed to set hostname") + return err + } + + s.currentHostname = hostname + s.currentFqdn = fqdn + + scopedLogger.Info().Msg("hostname set") + + return nil +} diff --git a/internal/network/netif.go b/internal/network/netif.go new file mode 100644 index 0000000..c5db806 --- /dev/null +++ b/internal/network/netif.go @@ -0,0 +1,346 @@ +package network + +import ( + "fmt" + "net" + "sync" + + "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/udhcpc" + "github.com/rs/zerolog" + + "github.com/vishvananda/netlink" +) + +type NetworkInterfaceState struct { + interfaceName string + interfaceUp bool + ipv4Addr *net.IP + ipv4Addresses []string + ipv6Addr *net.IP + ipv6Addresses []IPv6Address + ipv6LinkLocal *net.IP + macAddr *net.HardwareAddr + + l *zerolog.Logger + stateLock sync.Mutex + + config *NetworkConfig + dhcpClient *udhcpc.DHCPClient + + defaultHostname string + currentHostname string + currentFqdn string + + onStateChange func(state *NetworkInterfaceState) + onInitialCheck func(state *NetworkInterfaceState) + cbConfigChange func(config *NetworkConfig) + + checked bool +} + +type NetworkInterfaceOptions struct { + InterfaceName string + DhcpPidFile string + Logger *zerolog.Logger + DefaultHostname string + OnStateChange func(state *NetworkInterfaceState) + OnInitialCheck func(state *NetworkInterfaceState) + OnDhcpLeaseChange func(lease *udhcpc.Lease) + OnConfigChange func(config *NetworkConfig) + NetworkConfig *NetworkConfig +} + +func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) { + if opts.NetworkConfig == nil { + return nil, fmt.Errorf("NetworkConfig can not be nil") + } + + if opts.DefaultHostname == "" { + opts.DefaultHostname = "jetkvm" + } + + err := confparser.SetDefaultsAndValidate(opts.NetworkConfig) + if err != nil { + return nil, err + } + + l := opts.Logger + s := &NetworkInterfaceState{ + interfaceName: opts.InterfaceName, + defaultHostname: opts.DefaultHostname, + stateLock: sync.Mutex{}, + l: l, + onStateChange: opts.OnStateChange, + onInitialCheck: opts.OnInitialCheck, + cbConfigChange: opts.OnConfigChange, + config: opts.NetworkConfig, + } + + // create the dhcp client + dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{ + InterfaceName: opts.InterfaceName, + PidFile: opts.DhcpPidFile, + Logger: l, + OnLeaseChange: func(lease *udhcpc.Lease) { + _, err := s.update() + if err != nil { + opts.Logger.Error().Err(err).Msg("failed to update network state") + return + } + + _ = s.setHostnameIfNotSame() + + opts.OnDhcpLeaseChange(lease) + }, + }) + + s.dhcpClient = dhcpClient + + return s, nil +} + +func (s *NetworkInterfaceState) IsUp() bool { + return s.interfaceUp +} + +func (s *NetworkInterfaceState) HasIPAssigned() bool { + return s.ipv4Addr != nil || s.ipv6Addr != nil +} + +func (s *NetworkInterfaceState) IsOnline() bool { + return s.IsUp() && s.HasIPAssigned() +} + +func (s *NetworkInterfaceState) IPv4() *net.IP { + return s.ipv4Addr +} + +func (s *NetworkInterfaceState) IPv4String() string { + if s.ipv4Addr == nil { + return "..." + } + return s.ipv4Addr.String() +} + +func (s *NetworkInterfaceState) IPv6() *net.IP { + return s.ipv6Addr +} + +func (s *NetworkInterfaceState) IPv6String() string { + if s.ipv6Addr == nil { + return "..." + } + return s.ipv6Addr.String() +} + +func (s *NetworkInterfaceState) MAC() *net.HardwareAddr { + return s.macAddr +} + +func (s *NetworkInterfaceState) MACString() string { + if s.macAddr == nil { + return "" + } + return s.macAddr.String() +} + +func (s *NetworkInterfaceState) update() (DhcpTargetState, error) { + s.stateLock.Lock() + defer s.stateLock.Unlock() + + dhcpTargetState := DhcpTargetStateDoNothing + + iface, err := netlink.LinkByName(s.interfaceName) + if err != nil { + s.l.Error().Err(err).Msg("failed to get interface") + return dhcpTargetState, err + } + + // detect if the interface status changed + var changed bool + attrs := iface.Attrs() + state := attrs.OperState + newInterfaceUp := state == netlink.OperUp + + // check if the interface is coming up + interfaceGoingUp := !s.interfaceUp && newInterfaceUp + interfaceGoingDown := s.interfaceUp && !newInterfaceUp + + if s.interfaceUp != newInterfaceUp { + s.interfaceUp = newInterfaceUp + changed = true + } + + if changed { + if interfaceGoingUp { + s.l.Info().Msg("interface state transitioned to up") + dhcpTargetState = DhcpTargetStateRenew + } else if interfaceGoingDown { + s.l.Info().Msg("interface state transitioned to down") + } + } + + // set the mac address + s.macAddr = &attrs.HardwareAddr + + // get the ip addresses + addrs, err := netlinkAddrs(iface) + if err != nil { + return dhcpTargetState, logging.ErrorfL(s.l, "failed to get ip addresses", err) + } + + var ( + ipv4Addresses = make([]net.IP, 0) + ipv4AddressesString = make([]string, 0) + ipv6Addresses = make([]IPv6Address, 0) + // ipv6AddressesString = make([]string, 0) + ipv6LinkLocal *net.IP + ) + + for _, addr := range addrs { + if addr.IP.To4() != nil { + scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger() + if interfaceGoingDown { + // remove all IPv4 addresses from the interface. + scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to delete address") + } + // notify the DHCP client to release the lease + dhcpTargetState = DhcpTargetStateRelease + continue + } + ipv4Addresses = append(ipv4Addresses, addr.IP) + ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String()) + } else if addr.IP.To16() != nil { + scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger() + // check if it's a link local address + if addr.IP.IsLinkLocalUnicast() { + ipv6LinkLocal = &addr.IP + continue + } + + if !addr.IP.IsGlobalUnicast() { + scopedLogger.Trace().Msg("not a global unicast address, skipping") + continue + } + + if interfaceGoingDown { + scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to delete address") + } + continue + } + ipv6Addresses = append(ipv6Addresses, IPv6Address{ + Address: addr.IP, + Prefix: *addr.IPNet, + ValidLifetime: lifetimeToTime(addr.ValidLft), + PreferredLifetime: lifetimeToTime(addr.PreferedLft), + Scope: addr.Scope, + }) + // ipv6AddressesString = append(ipv6AddressesString, addr.IPNet.String()) + } + } + + if len(ipv4Addresses) > 0 { + // compare the addresses to see if there's a change + if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() { + scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger() + if s.ipv4Addr != nil { + scopedLogger.Info(). + Str("old_ipv4", s.ipv4Addr.String()). + Msg("IPv4 address changed") + } else { + scopedLogger.Info().Msg("IPv4 address found") + } + s.ipv4Addr = &ipv4Addresses[0] + changed = true + } + } + s.ipv4Addresses = ipv4AddressesString + + if ipv6LinkLocal != nil { + if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() { + scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger() + if s.ipv6LinkLocal != nil { + scopedLogger.Info(). + Str("old_ipv6", s.ipv6LinkLocal.String()). + Msg("IPv6 link local address changed") + } else { + scopedLogger.Info().Msg("IPv6 link local address found") + } + s.ipv6LinkLocal = ipv6LinkLocal + changed = true + } + } + s.ipv6Addresses = ipv6Addresses + + if len(ipv6Addresses) > 0 { + // compare the addresses to see if there's a change + if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() { + scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger() + if s.ipv6Addr != nil { + scopedLogger.Info(). + Str("old_ipv6", s.ipv6Addr.String()). + Msg("IPv6 address changed") + } else { + scopedLogger.Info().Msg("IPv6 address found") + } + s.ipv6Addr = &ipv6Addresses[0].Address + changed = true + } + } + + // if it's the initial check, we'll set changed to false + initialCheck := !s.checked + if initialCheck { + s.checked = true + changed = false + if dhcpTargetState == DhcpTargetStateRenew { + // it's the initial check, we'll start the DHCP client + // dhcpTargetState = DhcpTargetStateStart + // TODO: manage DHCP client start/stop + dhcpTargetState = DhcpTargetStateDoNothing + } + } + + if initialCheck { + s.onInitialCheck(s) + } else if changed { + s.onStateChange(s) + } + + return dhcpTargetState, nil +} + +func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error { + dhcpTargetState, err := s.update() + if err != nil { + return logging.ErrorfL(s.l, "failed to update network state", err) + } + + switch dhcpTargetState { + case DhcpTargetStateRenew: + s.l.Info().Msg("renewing DHCP lease") + _ = s.dhcpClient.Renew() + case DhcpTargetStateRelease: + s.l.Info().Msg("releasing DHCP lease") + _ = s.dhcpClient.Release() + case DhcpTargetStateStart: + s.l.Warn().Msg("dhcpTargetStateStart not implemented") + case DhcpTargetStateStop: + s.l.Warn().Msg("dhcpTargetStateStop not implemented") + } + + return nil +} + +func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) { + _ = s.setHostnameIfNotSame() + s.cbConfigChange(config) +} diff --git a/internal/network/netif_linux.go b/internal/network/netif_linux.go new file mode 100644 index 0000000..ec057f1 --- /dev/null +++ b/internal/network/netif_linux.go @@ -0,0 +1,58 @@ +//go:build linux + +package network + +import ( + "time" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" +) + +func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) { + if update.Link.Attrs().Name == s.interfaceName { + s.l.Info().Interface("update", update).Msg("interface link update received") + _ = s.CheckAndUpdateDhcp() + } +} + +func (s *NetworkInterfaceState) Run() error { + updates := make(chan netlink.LinkUpdate) + done := make(chan struct{}) + + if err := netlink.LinkSubscribe(updates, done); err != nil { + s.l.Warn().Err(err).Msg("failed to subscribe to link updates") + return err + } + + _ = s.setHostnameIfNotSame() + + // run the dhcp client + go s.dhcpClient.Run() // nolint:errcheck + + if err := s.CheckAndUpdateDhcp(); err != nil { + return err + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case update := <-updates: + s.HandleLinkUpdate(update) + case <-ticker.C: + _ = s.CheckAndUpdateDhcp() + case <-done: + return + } + } + }() + + return nil +} + +func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) { + return netlink.AddrList(iface, nl.FAMILY_ALL) +} diff --git a/internal/network/netif_notlinux.go b/internal/network/netif_notlinux.go new file mode 100644 index 0000000..d101630 --- /dev/null +++ b/internal/network/netif_notlinux.go @@ -0,0 +1,21 @@ +//go:build !linux + +package network + +import ( + "fmt" + + "github.com/vishvananda/netlink" +) + +func (s *NetworkInterfaceState) HandleLinkUpdate() error { + return fmt.Errorf("not implemented") +} + +func (s *NetworkInterfaceState) Run() error { + return fmt.Errorf("not implemented") +} + +func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/internal/network/rpc.go b/internal/network/rpc.go new file mode 100644 index 0000000..32f34f5 --- /dev/null +++ b/internal/network/rpc.go @@ -0,0 +1,126 @@ +package network + +import ( + "fmt" + "time" + + "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/udhcpc" +) + +type RpcIPv6Address struct { + Address string `json:"address"` + ValidLifetime *time.Time `json:"valid_lifetime,omitempty"` + PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"` + Scope int `json:"scope"` +} + +type RpcNetworkState struct { + InterfaceName string `json:"interface_name"` + MacAddress string `json:"mac_address"` + IPv4 string `json:"ipv4,omitempty"` + IPv6 string `json:"ipv6,omitempty"` + IPv6LinkLocal string `json:"ipv6_link_local,omitempty"` + IPv4Addresses []string `json:"ipv4_addresses,omitempty"` + IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"` + DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"` +} + +type RpcNetworkSettings struct { + NetworkConfig +} + +func (s *NetworkInterfaceState) MacAddress() string { + if s.macAddr == nil { + return "" + } + + return s.macAddr.String() +} + +func (s *NetworkInterfaceState) IPv4Address() string { + if s.ipv4Addr == nil { + return "" + } + + return s.ipv4Addr.String() +} + +func (s *NetworkInterfaceState) IPv6Address() string { + if s.ipv6Addr == nil { + return "" + } + + return s.ipv6Addr.String() +} + +func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string { + if s.ipv6LinkLocal == nil { + return "" + } + + return s.ipv6LinkLocal.String() +} + +func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState { + ipv6Addresses := make([]RpcIPv6Address, 0) + + if s.ipv6Addresses != nil { + for _, addr := range s.ipv6Addresses { + ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{ + Address: addr.Prefix.String(), + ValidLifetime: addr.ValidLifetime, + PreferredLifetime: addr.PreferredLifetime, + Scope: addr.Scope, + }) + } + } + + return RpcNetworkState{ + InterfaceName: s.interfaceName, + MacAddress: s.MacAddress(), + IPv4: s.IPv4Address(), + IPv6: s.IPv6Address(), + IPv6LinkLocal: s.IPv6LinkLocalAddress(), + IPv4Addresses: s.ipv4Addresses, + IPv6Addresses: ipv6Addresses, + DHCPLease: s.dhcpClient.GetLease(), + } +} + +func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings { + if s.config == nil { + return RpcNetworkSettings{} + } + + return RpcNetworkSettings{ + NetworkConfig: *s.config, + } +} + +func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error { + currentSettings := s.config + + err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig) + if err != nil { + return err + } + + if IsSame(currentSettings, settings.NetworkConfig) { + // no changes, do nothing + return nil + } + + s.config = &settings.NetworkConfig + s.onConfigChange(s.config) + + return nil +} + +func (s *NetworkInterfaceState) RpcRenewDHCPLease() error { + if s.dhcpClient == nil { + return fmt.Errorf("dhcp client not initialized") + } + + return s.dhcpClient.Renew() +} diff --git a/internal/network/utils.go b/internal/network/utils.go new file mode 100644 index 0000000..6d64332 --- /dev/null +++ b/internal/network/utils.go @@ -0,0 +1,26 @@ +package network + +import ( + "encoding/json" + "time" +) + +func lifetimeToTime(lifetime int) *time.Time { + if lifetime == 0 { + return nil + } + t := time.Now().Add(time.Duration(lifetime) * time.Second) + return &t +} + +func IsSame(a, b interface{}) bool { + aJSON, err := json.Marshal(a) + if err != nil { + return false + } + bJSON, err := json.Marshal(b) + if err != nil { + return false + } + return string(aJSON) == string(bJSON) +} diff --git a/internal/timesync/http.go b/internal/timesync/http.go new file mode 100644 index 0000000..3a51463 --- /dev/null +++ b/internal/timesync/http.go @@ -0,0 +1,132 @@ +package timesync + +import ( + "context" + "errors" + "math/rand" + "net/http" + "strconv" + "time" +) + +var defaultHTTPUrls = []string{ + "http://www.gstatic.com/generate_204", + "http://cp.cloudflare.com/", + "http://edge-http.microsoft.com/captiveportal/generate_204", + // Firefox, Apple, and Microsoft have inconsistent results, so we don't use it + // "http://detectportal.firefox.com/", + // "http://www.apple.com/library/test/success.html", + // "http://www.msftconnecttest.com/connecttest.txt", +} + +func (t *TimeSync) queryAllHttpTime() (now *time.Time) { + chunkSize := 4 + httpUrls := t.httpUrls + + // shuffle the http urls to avoid always querying the same servers + rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] }) + + for i := 0; i < len(httpUrls); i += chunkSize { + chunk := httpUrls[i:min(i+chunkSize, len(httpUrls))] + results := t.queryMultipleHttp(chunk, timeSyncTimeout) + if results != nil { + return results + } + } + + return nil +} + +func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now *time.Time) { + results := make(chan *time.Time, len(urls)) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, url := range urls { + go func(url string) { + scopedLogger := t.l.With(). + Str("http_url", url). + Logger() + + metricHttpRequestCount.WithLabelValues(url).Inc() + metricHttpTotalRequestCount.Inc() + + startTime := time.Now() + now, response, err := queryHttpTime( + ctx, + url, + timeout, + ) + duration := time.Since(startTime) + + metricHttpServerLastRTT.WithLabelValues(url).Set(float64(duration.Milliseconds())) + metricHttpServerRttHistogram.WithLabelValues(url).Observe(float64(duration.Milliseconds())) + + status := 0 + if response != nil { + status = response.StatusCode + } + metricHttpServerInfo.WithLabelValues( + url, + strconv.Itoa(status), + ).Set(1) + + if err == nil { + metricHttpTotalSuccessCount.Inc() + metricHttpSuccessCount.WithLabelValues(url).Inc() + + requestId := response.Header.Get("X-Request-Id") + if requestId != "" { + requestId = response.Header.Get("X-Msedge-Ref") + } + if requestId == "" { + requestId = response.Header.Get("Cf-Ray") + } + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Int("status", status). + Str("request_id", requestId). + Str("time_taken", duration.String()). + Msg("HTTP server returned time") + + cancel() + results <- now + } else if errors.Is(err, context.Canceled) { + metricHttpCancelCount.WithLabelValues(url).Inc() + metricHttpTotalCancelCount.Inc() + } else { + scopedLogger.Warn(). + Str("error", err.Error()). + Int("status", status). + Msg("failed to query HTTP server") + } + }(url) + } + + return <-results +} + +func queryHttpTime( + ctx context.Context, + url string, + timeout time.Duration, +) (now *time.Time, response *http.Response, err error) { + client := http.Client{ + Timeout: timeout, + } + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + dateStr := resp.Header.Get("Date") + parsedTime, err := time.Parse(time.RFC1123, dateStr) + if err != nil { + return nil, nil, err + } + return &parsedTime, resp, nil +} diff --git a/internal/timesync/metrics.go b/internal/timesync/metrics.go new file mode 100644 index 0000000..0e28acb --- /dev/null +++ b/internal/timesync/metrics.go @@ -0,0 +1,147 @@ +package timesync + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + metricTimeSyncStatus = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_status", + Help: "The status of the timesync, 1 if successful, 0 if not", + }, + ) + metricTimeSyncCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_count", + Help: "The number of times the timesync has been run", + }, + ) + metricTimeSyncSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_success_count", + Help: "The number of times the timesync has been successful", + }, + ) + metricRTCUpdateCount = promauto.NewCounter( //nolint:unused + prometheus.CounterOpts{ + Name: "jetkvm_timesync_rtc_update_count", + Help: "The number of times the RTC has been updated", + }, + ) + metricNtpTotalSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_total_success_count", + Help: "The total number of successful NTP requests", + }, + ) + metricNtpTotalRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_total_request_count", + Help: "The total number of NTP requests sent", + }, + ) + metricNtpSuccessCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_success_count", + Help: "The number of successful NTP requests", + }, + []string{"url"}, + ) + metricNtpRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_request_count", + Help: "The number of NTP requests sent to the server", + }, + []string{"url"}, + ) + metricNtpServerLastRTT = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_ntp_server_last_rtt", + Help: "The last RTT of the NTP server in milliseconds", + }, + []string{"url"}, + ) + metricNtpServerRttHistogram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_timesync_ntp_server_rtt", + Help: "The histogram of the RTT of the NTP server in milliseconds", + Buckets: []float64{ + 10, 25, 50, 100, 200, 300, 500, 1000, + }, + }, + []string{"url"}, + ) + metricNtpServerInfo = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_ntp_server_info", + Help: "The info of the NTP server", + }, + []string{"url", "reference", "stratum", "precision"}, + ) + + metricHttpTotalSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_success_count", + Help: "The total number of successful HTTP requests", + }, + ) + metricHttpTotalRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_request_count", + Help: "The total number of HTTP requests sent", + }, + ) + metricHttpTotalCancelCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_cancel_count", + Help: "The total number of HTTP requests cancelled", + }, + ) + metricHttpSuccessCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_success_count", + Help: "The number of successful HTTP requests", + }, + []string{"url"}, + ) + metricHttpRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_request_count", + Help: "The number of HTTP requests sent to the server", + }, + []string{"url"}, + ) + metricHttpCancelCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_cancel_count", + Help: "The number of HTTP requests cancelled", + }, + []string{"url"}, + ) + metricHttpServerLastRTT = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_http_server_last_rtt", + Help: "The last RTT of the HTTP server in milliseconds", + }, + []string{"url"}, + ) + metricHttpServerRttHistogram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_timesync_http_server_rtt", + Help: "The histogram of the RTT of the HTTP server in milliseconds", + Buckets: []float64{ + 10, 25, 50, 100, 200, 300, 500, 1000, + }, + }, + []string{"url"}, + ) + metricHttpServerInfo = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_http_server_info", + Help: "The info of the HTTP server", + }, + []string{"url", "http_code"}, + ) +) diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go new file mode 100644 index 0000000..41656b7 --- /dev/null +++ b/internal/timesync/ntp.go @@ -0,0 +1,113 @@ +package timesync + +import ( + "math/rand/v2" + "strconv" + "time" + + "github.com/beevik/ntp" +) + +var defaultNTPServers = []string{ + "time.apple.com", + "time.aws.com", + "time.windows.com", + "time.google.com", + "162.159.200.123", // time.cloudflare.com + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", +} + +func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) { + chunkSize := 4 + ntpServers := t.ntpServers + + // shuffle the ntp servers to avoid always querying the same servers + rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] }) + + for i := 0; i < len(ntpServers); i += chunkSize { + chunk := ntpServers[i:min(i+chunkSize, len(ntpServers))] + now, offset := t.queryMultipleNTP(chunk, timeSyncTimeout) + if now != nil { + return now, offset + } + } + + return nil, nil +} + +type ntpResult struct { + now *time.Time + offset *time.Duration +} + +func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) { + results := make(chan *ntpResult, len(servers)) + for _, server := range servers { + go func(server string) { + scopedLogger := t.l.With(). + Str("server", server). + Logger() + + // increase request count + metricNtpTotalRequestCount.Inc() + metricNtpRequestCount.WithLabelValues(server).Inc() + + // query the server + now, response, err := queryNtpServer(server, timeout) + + // set the last RTT + metricNtpServerLastRTT.WithLabelValues( + server, + ).Set(float64(response.RTT.Milliseconds())) + + // set the RTT histogram + metricNtpServerRttHistogram.WithLabelValues( + server, + ).Observe(float64(response.RTT.Milliseconds())) + + // set the server info + metricNtpServerInfo.WithLabelValues( + server, + response.ReferenceString(), + strconv.Itoa(int(response.Stratum)), + strconv.Itoa(int(response.Precision)), + ).Set(1) + + if err == nil { + // increase success count + metricNtpTotalSuccessCount.Inc() + metricNtpSuccessCount.WithLabelValues(server).Inc() + + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Str("reference", response.ReferenceString()). + Str("rtt", response.RTT.String()). + Str("clockOffset", response.ClockOffset.String()). + Uint8("stratum", response.Stratum). + Msg("NTP server returned time") + results <- &ntpResult{ + now: now, + offset: &response.ClockOffset, + } + } else { + scopedLogger.Warn(). + Str("error", err.Error()). + Msg("failed to query NTP server") + } + }(server) + } + + result := <-results + return result.now, result.offset +} + +func queryNtpServer(server string, timeout time.Duration) (now *time.Time, response *ntp.Response, err error) { + resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) + if err != nil { + return nil, nil, err + } + return &resp.Time, resp, nil +} diff --git a/internal/timesync/rtc.go b/internal/timesync/rtc.go new file mode 100644 index 0000000..92ee485 --- /dev/null +++ b/internal/timesync/rtc.go @@ -0,0 +1,26 @@ +package timesync + +import ( + "fmt" + "os" +) + +var ( + rtcDeviceSearchPaths = []string{ + "/dev/rtc", + "/dev/rtc0", + "/dev/rtc1", + "/dev/misc/rtc", + "/dev/misc/rtc0", + "/dev/misc/rtc1", + } +) + +func getRtcDevicePath() (string, error) { + for _, path := range rtcDeviceSearchPaths { + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + return "", fmt.Errorf("rtc device not found") +} diff --git a/internal/timesync/rtc_linux.go b/internal/timesync/rtc_linux.go new file mode 100644 index 0000000..27e4ec7 --- /dev/null +++ b/internal/timesync/rtc_linux.go @@ -0,0 +1,105 @@ +//go:build linux + +package timesync + +import ( + "fmt" + "os" + "time" + + "golang.org/x/sys/unix" +) + +func TimetoRtcTime(t time.Time) unix.RTCTime { + return unix.RTCTime{ + Sec: int32(t.Second()), + Min: int32(t.Minute()), + Hour: int32(t.Hour()), + Mday: int32(t.Day()), + Mon: int32(t.Month() - 1), + Year: int32(t.Year() - 1900), + Wday: int32(0), + Yday: int32(0), + Isdst: int32(0), + } +} + +func RtcTimetoTime(t unix.RTCTime) time.Time { + return time.Date( + int(t.Year)+1900, + time.Month(t.Mon+1), + int(t.Mday), + int(t.Hour), + int(t.Min), + int(t.Sec), + 0, + time.UTC, + ) +} + +func (t *TimeSync) getRtcDevice() (*os.File, error) { + if t.rtcDevice == nil { + file, err := os.OpenFile(t.rtcDevicePath, os.O_RDWR, 0666) + if err != nil { + return nil, err + } + t.rtcDevice = file + } + return t.rtcDevice, nil +} + +func (t *TimeSync) getRtcDeviceFd() (int, error) { + device, err := t.getRtcDevice() + if err != nil { + return 0, err + } + return int(device.Fd()), nil +} + +// Read implements Read for the Linux RTC +func (t *TimeSync) readRtcTime() (time.Time, error) { + fd, err := t.getRtcDeviceFd() + if err != nil { + return time.Time{}, fmt.Errorf("failed to get RTC device fd: %w", err) + } + + rtcTime, err := unix.IoctlGetRTCTime(fd) + if err != nil { + return time.Time{}, fmt.Errorf("failed to get RTC time: %w", err) + } + + date := RtcTimetoTime(*rtcTime) + + return date, nil +} + +// Set implements Set for the Linux RTC +// ... +// It might be not accurate as the time consumed by the system call is not taken into account +// but it's good enough for our purposes +func (t *TimeSync) setRtcTime(tu time.Time) error { + rt := TimetoRtcTime(tu) + + fd, err := t.getRtcDeviceFd() + if err != nil { + return fmt.Errorf("failed to get RTC device fd: %w", err) + } + + currentRtcTime, err := t.readRtcTime() + if err != nil { + return fmt.Errorf("failed to read RTC time: %w", err) + } + + t.l.Info(). + Interface("rtc_time", tu). + Str("offset", tu.Sub(currentRtcTime).String()). + Msg("set rtc time") + + if err := unix.IoctlSetRTCTime(fd, &rt); err != nil { + return fmt.Errorf("failed to set RTC time: %w", err) + } + + metricRTCUpdateCount.Inc() + + return nil +} diff --git a/internal/timesync/rtc_notlinux.go b/internal/timesync/rtc_notlinux.go new file mode 100644 index 0000000..e3c1b20 --- /dev/null +++ b/internal/timesync/rtc_notlinux.go @@ -0,0 +1,16 @@ +//go:build !linux + +package timesync + +import ( + "errors" + "time" +) + +func (t *TimeSync) readRtcTime() (time.Time, error) { + return time.Now(), nil +} + +func (t *TimeSync) setRtcTime(tu time.Time) error { + return errors.New("not supported") +} diff --git a/internal/timesync/timesync.go b/internal/timesync/timesync.go new file mode 100644 index 0000000..e956cf9 --- /dev/null +++ b/internal/timesync/timesync.go @@ -0,0 +1,208 @@ +package timesync + +import ( + "fmt" + "os" + "os/exec" + "sync" + "time" + + "github.com/jetkvm/kvm/internal/network" + "github.com/rs/zerolog" +) + +const ( + timeSyncRetryStep = 5 * time.Second + timeSyncRetryMaxInt = 1 * time.Minute + timeSyncWaitNetChkInt = 100 * time.Millisecond + timeSyncWaitNetUpInt = 3 * time.Second + timeSyncInterval = 1 * time.Hour + timeSyncTimeout = 2 * time.Second +) + +var ( + timeSyncRetryInterval = 0 * time.Second +) + +type TimeSync struct { + syncLock *sync.Mutex + l *zerolog.Logger + + ntpServers []string + httpUrls []string + networkConfig *network.NetworkConfig + + rtcDevicePath string + rtcDevice *os.File //nolint:unused + rtcLock *sync.Mutex + + syncSuccess bool + + preCheckFunc func() (bool, error) +} + +type TimeSyncOptions struct { + PreCheckFunc func() (bool, error) + Logger *zerolog.Logger + NetworkConfig *network.NetworkConfig +} + +type SyncMode struct { + Ntp bool + Http bool + Ordering []string + NtpUseFallback bool + HttpUseFallback bool +} + +func NewTimeSync(opts *TimeSyncOptions) *TimeSync { + rtcDevice, err := getRtcDevicePath() + if err != nil { + opts.Logger.Error().Err(err).Msg("failed to get RTC device path") + } else { + opts.Logger.Info().Str("path", rtcDevice).Msg("RTC device found") + } + + t := &TimeSync{ + syncLock: &sync.Mutex{}, + l: opts.Logger, + rtcDevicePath: rtcDevice, + rtcLock: &sync.Mutex{}, + preCheckFunc: opts.PreCheckFunc, + ntpServers: defaultNTPServers, + httpUrls: defaultHTTPUrls, + networkConfig: opts.NetworkConfig, + } + + if t.rtcDevicePath != "" { + rtcTime, _ := t.readRtcTime() + t.l.Info().Interface("rtc_time", rtcTime).Msg("read RTC time") + } + + return t +} + +func (t *TimeSync) getSyncMode() SyncMode { + syncMode := SyncMode{ + NtpUseFallback: true, + HttpUseFallback: true, + } + var syncModeString string + + if t.networkConfig != nil { + syncModeString = t.networkConfig.TimeSyncMode.String + if t.networkConfig.TimeSyncDisableFallback.Bool { + syncMode.NtpUseFallback = false + syncMode.HttpUseFallback = false + } + } + + switch syncModeString { + case "ntp_only": + syncMode.Ntp = true + case "http_only": + syncMode.Http = true + default: + syncMode.Ntp = true + syncMode.Http = true + } + + return syncMode +} + +func (t *TimeSync) doTimeSync() { + metricTimeSyncStatus.Set(0) + for { + if ok, err := t.preCheckFunc(); !ok { + if err != nil { + t.l.Error().Err(err).Msg("pre-check failed") + } + time.Sleep(timeSyncWaitNetChkInt) + continue + } + + t.l.Info().Msg("syncing system time") + start := time.Now() + err := t.Sync() + if err != nil { + t.l.Error().Str("error", err.Error()).Msg("failed to sync system time") + + // retry after a delay + timeSyncRetryInterval += timeSyncRetryStep + time.Sleep(timeSyncRetryInterval) + // reset the retry interval if it exceeds the max interval + if timeSyncRetryInterval > timeSyncRetryMaxInt { + timeSyncRetryInterval = 0 + } + + continue + } + t.syncSuccess = true + t.l.Info().Str("now", time.Now().Format(time.RFC3339)). + Str("time_taken", time.Since(start).String()). + Msg("time sync successful") + + metricTimeSyncStatus.Set(1) + + time.Sleep(timeSyncInterval) // after the first sync is done + } +} + +func (t *TimeSync) Sync() error { + var ( + now *time.Time + offset *time.Duration + ) + + syncMode := t.getSyncMode() + + metricTimeSyncCount.Inc() + + if syncMode.Ntp { + now, offset = t.queryNetworkTime() + } + + if syncMode.Http && now == nil { + now = t.queryAllHttpTime() + } + + if now == nil { + return fmt.Errorf("failed to get time from any source") + } + + if offset != nil { + newNow := time.Now().Add(*offset) + now = &newNow + } + + err := t.setSystemTime(*now) + if err != nil { + return fmt.Errorf("failed to set system time: %w", err) + } + + metricTimeSyncSuccessCount.Inc() + + return nil +} + +func (t *TimeSync) IsSyncSuccess() bool { + return t.syncSuccess +} + +func (t *TimeSync) Start() { + go t.doTimeSync() +} + +func (t *TimeSync) setSystemTime(now time.Time) error { + nowStr := now.Format("2006-01-02 15:04:05") + output, err := exec.Command("date", "-s", nowStr).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) + } + + if t.rtcDevicePath != "" { + return t.setRtcTime(now) + } + + return nil +} diff --git a/internal/udhcpc/options.go b/internal/udhcpc/options.go new file mode 100644 index 0000000..10c9f75 --- /dev/null +++ b/internal/udhcpc/options.go @@ -0,0 +1,12 @@ +package udhcpc + +func (u *DHCPClient) GetNtpServers() []string { + if u.lease == nil { + return nil + } + servers := make([]string, len(u.lease.NTPServers)) + for i, server := range u.lease.NTPServers { + servers[i] = server.String() + } + return servers +} diff --git a/internal/udhcpc/parser.go b/internal/udhcpc/parser.go new file mode 100644 index 0000000..66c3ba2 --- /dev/null +++ b/internal/udhcpc/parser.go @@ -0,0 +1,186 @@ +package udhcpc + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +type Lease struct { + // from https://udhcp.busybox.net/README.udhcpc + IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP + Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask + Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network + TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network + MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network + HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname + Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network + BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option + BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option + BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option + Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC + Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers + DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers + NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers + LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers + TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete) + IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete) + LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete) + CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete) + WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers + SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server + BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile + RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk + LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds + DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored) + ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server + Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK + TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name + BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name + Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds + LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease + isEmpty map[string]bool +} + +func (l *Lease) setIsEmpty(m map[string]bool) { + l.isEmpty = m +} + +func (l *Lease) IsEmpty(key string) bool { + return l.isEmpty[key] +} + +func (l *Lease) ToJSON() string { + json, err := json.Marshal(l) + if err != nil { + return "" + } + return string(json) +} + +func (l *Lease) SetLeaseExpiry() (time.Time, error) { + if l.Uptime == 0 || l.LeaseTime == 0 { + return time.Time{}, fmt.Errorf("uptime or lease time isn't set") + } + + // get the uptime of the device + + file, err := os.Open("/proc/uptime") + if err != nil { + return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err) + } + defer file.Close() + + var uptime time.Duration + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + text := scanner.Text() + parts := strings.Split(text, " ") + uptime, err = time.ParseDuration(parts[0] + "s") + + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err) + } + } + + relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime + leaseExpiry := time.Now().Add(relativeLeaseRemaining) + + l.LeaseExpiry = &leaseExpiry + + return leaseExpiry, nil +} + +func UnmarshalDHCPCLease(lease *Lease, str string) error { + // parse the lease file as a map + data := make(map[string]string) + for _, line := range strings.Split(str, "\n") { + line = strings.TrimSpace(line) + // skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + data[key] = value + } + + // now iterate over the lease struct and set the values + leaseType := reflect.TypeOf(lease).Elem() + leaseValue := reflect.ValueOf(lease).Elem() + + valuesParsed := make(map[string]bool) + + for i := 0; i < leaseType.NumField(); i++ { + field := leaseValue.Field(i) + + // get the env tag + key := leaseType.Field(i).Tag.Get("env") + if key == "" { + continue + } + + valuesParsed[key] = false + + // get the value from the data map + value, ok := data[key] + if !ok || value == "" { + continue + } + + switch field.Interface().(type) { + case string: + field.SetString(value) + case int: + val, err := strconv.Atoi(value) + if err != nil { + continue + } + field.SetInt(int64(val)) + case time.Duration: + val, err := time.ParseDuration(value + "s") + if err != nil { + continue + } + field.Set(reflect.ValueOf(val)) + case net.IP: + ip := net.ParseIP(value) + if ip == nil { + continue + } + field.Set(reflect.ValueOf(ip)) + case []net.IP: + val := make([]net.IP, 0) + for _, ipStr := range strings.Fields(value) { + ip := net.ParseIP(ipStr) + if ip == nil { + continue + } + val = append(val, ip) + } + field.Set(reflect.ValueOf(val)) + default: + return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String()) + } + + valuesParsed[key] = true + } + + lease.setIsEmpty(valuesParsed) + + return nil +} diff --git a/internal/udhcpc/parser_test.go b/internal/udhcpc/parser_test.go new file mode 100644 index 0000000..423ab53 --- /dev/null +++ b/internal/udhcpc/parser_test.go @@ -0,0 +1,74 @@ +package udhcpc + +import ( + "testing" + "time" +) + +func TestUnmarshalDHCPCLease(t *testing.T) { + lease := &Lease{} + err := UnmarshalDHCPCLease(lease, ` +# generated @ Mon Jan 4 19:31:53 UTC 2021 +# 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04 +# the date might be inaccurate if the clock is not set +ip=192.168.0.240 +siaddr=192.168.0.1 +sname= +boot_file= +subnet=255.255.255.0 +timezone= +router=192.168.0.1 +timesvr= +namesvr= +dns=172.19.53.2 +logsvr= +cookiesvr= +lprsvr= +hostname= +bootsize= +domain= +swapsvr= +rootpath= +ipttl= +mtu= +broadcast= +ntpsrv=162.159.200.123 +wins= +lease=172800 +dhcptype= +serverid=192.168.0.1 +message= +tftp= +bootfile= + `) + if lease.IPAddress.String() != "192.168.0.240" { + t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String()) + } + if lease.Netmask.String() != "255.255.255.0" { + t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String()) + } + if len(lease.Routers) != 1 { + t.Fatalf("expected 1 router, got %d", len(lease.Routers)) + } + if lease.Routers[0].String() != "192.168.0.1" { + t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String()) + } + if len(lease.NTPServers) != 1 { + t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers)) + } + if lease.NTPServers[0].String() != "162.159.200.123" { + t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String()) + } + if len(lease.DNS) != 1 { + t.Fatalf("expected 1 dns, got %d", len(lease.DNS)) + } + if lease.DNS[0].String() != "172.19.53.2" { + t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String()) + } + if lease.LeaseTime != 172800*time.Second { + t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime) + } + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/udhcpc/proc.go b/internal/udhcpc/proc.go new file mode 100644 index 0000000..69c2ab9 --- /dev/null +++ b/internal/udhcpc/proc.go @@ -0,0 +1,212 @@ +package udhcpc + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +func readFileNoStat(filename string) ([]byte, error) { + const maxBufferSize = 1024 * 1024 + + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + reader := io.LimitReader(f, maxBufferSize) + return io.ReadAll(reader) +} + +func toCmdline(path string) ([]string, error) { + data, err := readFileNoStat(path) + if err != nil { + return nil, err + } + + if len(data) < 1 { + return []string{}, nil + } + + return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil +} + +func (p *DHCPClient) findUdhcpcProcess() (int, error) { + // read procfs for udhcpc processes + // we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs + processes, err := os.ReadDir("/proc") + if err != nil { + return 0, err + } + + // iterate over the processes + for _, d := range processes { + // check if file is numeric + pid, err := strconv.Atoi(d.Name()) + if err != nil { + continue + } + + // check if it's a directory + if !d.IsDir() { + continue + } + + cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline")) + if err != nil { + continue + } + + if len(cmdline) < 1 { + continue + } + + if cmdline[0] != "udhcpc" { + continue + } + + cmdlineText := strings.Join(cmdline, " ") + + // check if it's a udhcpc process + if strings.Contains(cmdlineText, fmt.Sprintf("-i %s", p.InterfaceName)) { + p.logger.Debug(). + Str("pid", d.Name()). + Interface("cmdline", cmdline). + Msg("found udhcpc process") + return pid, nil + } + } + + return 0, errors.New("udhcpc process not found") +} + +func (c *DHCPClient) getProcessPid() (int, error) { + var pid int + if c.pidFile != "" { + // try to read the pid file + pidHandle, err := os.ReadFile(c.pidFile) + if err != nil { + c.logger.Warn().Err(err). + Str("pidFile", c.pidFile).Msg("failed to read udhcpc pid file") + } + + // if it exists, try to read the pid + if pidHandle != nil { + pidFromFile, err := strconv.Atoi(string(pidHandle)) + if err != nil { + c.logger.Warn().Err(err). + Str("pidFile", c.pidFile).Msg("failed to convert pid file to int") + } + pid = pidFromFile + } + } + + // if the pid is 0, try to find the pid using procfs + if pid == 0 { + newPid, err := c.findUdhcpcProcess() + if err != nil { + return 0, err + } + pid = newPid + } + + return pid, nil +} + +func (c *DHCPClient) getProcess() *os.Process { + pid, err := c.getProcessPid() + if err != nil { + return nil + } + + process, err := os.FindProcess(pid) + if err != nil { + c.logger.Warn().Err(err). + Int("pid", pid).Msg("failed to find process") + return nil + } + + return process +} + +func (c *DHCPClient) GetProcess() *os.Process { + if c.process == nil { + process := c.getProcess() + if process == nil { + return nil + } + c.process = process + } + + err := c.process.Signal(syscall.Signal(0)) + if err != nil && errors.Is(err, os.ErrProcessDone) { + oldPid := c.process.Pid + + c.process = nil + c.process = c.getProcess() + if c.process == nil { + c.logger.Error().Msg("failed to find new udhcpc process") + return nil + } + c.logger.Warn(). + Int("oldPid", oldPid). + Int("newPid", c.process.Pid). + Msg("udhcpc process pid changed") + } else if err != nil { + c.logger.Warn().Err(err). + Int("pid", c.process.Pid).Msg("udhcpc process is not running") + } + + return c.process +} + +func (c *DHCPClient) KillProcess() error { + process := c.GetProcess() + if process == nil { + return nil + } + + return process.Kill() +} + +func (c *DHCPClient) ReleaseProcess() error { + process := c.GetProcess() + if process == nil { + return nil + } + + return process.Release() +} + +func (c *DHCPClient) signalProcess(sig syscall.Signal) error { + process := c.GetProcess() + if process == nil { + return nil + } + + s := process.Signal(sig) + if s != nil { + c.logger.Warn().Err(s). + Int("pid", process.Pid). + Str("signal", sig.String()). + Msg("failed to signal udhcpc process") + return s + } + + return nil +} + +func (c *DHCPClient) Renew() error { + return c.signalProcess(syscall.SIGUSR1) +} + +func (c *DHCPClient) Release() error { + return c.signalProcess(syscall.SIGUSR2) +} diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go new file mode 100644 index 0000000..70ac1b8 --- /dev/null +++ b/internal/udhcpc/udhcpc.go @@ -0,0 +1,191 @@ +package udhcpc + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog" +) + +const ( + DHCPLeaseFile = "/run/udhcpc.%s.info" + DHCPPidFile = "/run/udhcpc.%s.pid" +) + +type DHCPClient struct { + InterfaceName string + leaseFile string + pidFile string + lease *Lease + logger *zerolog.Logger + process *os.Process + onLeaseChange func(lease *Lease) +} + +type DHCPClientOptions struct { + InterfaceName string + PidFile string + Logger *zerolog.Logger + OnLeaseChange func(lease *Lease) +} + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +func NewDHCPClient(options *DHCPClientOptions) *DHCPClient { + if options.Logger == nil { + options.Logger = &defaultLogger + } + + l := options.Logger.With().Str("interface", options.InterfaceName).Logger() + return &DHCPClient{ + InterfaceName: options.InterfaceName, + logger: &l, + leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName), + pidFile: options.PidFile, + onLeaseChange: options.OnLeaseChange, + } +} + +func (c *DHCPClient) getWatchPaths() []string { + watchPaths := make(map[string]interface{}) + watchPaths[filepath.Dir(c.leaseFile)] = nil + + if c.pidFile != "" { + watchPaths[filepath.Dir(c.pidFile)] = nil + } + + paths := make([]string, 0) + for path := range watchPaths { + paths = append(paths, path) + } + return paths +} + +// Run starts the DHCP client and watches the lease file for changes. +// this isn't a blocking call, and the lease file is reloaded when a change is detected. +func (c *DHCPClient) Run() error { + err := c.loadLeaseFile() + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + continue + } + if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) { + continue + } + + if event.Name == c.leaseFile { + c.logger.Debug(). + Str("event", event.Op.String()). + Str("path", event.Name). + Msg("udhcpc lease file updated, reloading lease") + _ = c.loadLeaseFile() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + c.logger.Error().Err(err).Msg("error watching lease file") + } + } + }() + + for _, path := range c.getWatchPaths() { + err = watcher.Add(path) + if err != nil { + c.logger.Error(). + Err(err). + Str("path", path). + Msg("failed to watch directory") + return err + } + } + + // TODO: update udhcpc pid file + // we'll comment this out for now because the pid might change + // process := c.GetProcess() + // if process == nil { + // c.logger.Error().Msg("udhcpc process not found") + // } + + // block the goroutine until the lease file is updated + <-make(chan struct{}) + + return nil +} + +func (c *DHCPClient) loadLeaseFile() error { + file, err := os.ReadFile(c.leaseFile) + if err != nil { + return err + } + + data := string(file) + if data == "" { + c.logger.Debug().Msg("udhcpc lease file is empty") + return nil + } + + lease := &Lease{} + err = UnmarshalDHCPCLease(lease, string(file)) + if err != nil { + return err + } + + isFirstLoad := c.lease == nil + c.lease = lease + + if lease.IPAddress == nil { + c.logger.Info(). + Interface("lease", lease). + Str("data", string(file)). + Msg("udhcpc lease cleared") + return nil + } + + msg := "udhcpc lease updated" + if isFirstLoad { + msg = "udhcpc lease loaded" + } + + leaseExpiry, err := lease.SetLeaseExpiry() + if err != nil { + c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry") + } else { + expiresIn := time.Until(leaseExpiry) + c.logger.Info(). + Interface("expiry", leaseExpiry). + Str("expiresIn", expiresIn.String()). + Msg("current dhcp lease expiry time calculated") + } + + c.onLeaseChange(lease) + + c.logger.Info(). + Str("ip", lease.IPAddress.String()). + Str("leaseTime", lease.LeaseTime.String()). + Interface("data", lease). + Msg(msg) + + return nil +} + +func (c *DHCPClient) GetLease() *Lease { + return c.lease +} diff --git a/internal/websecure/store.go b/internal/websecure/store.go index 69ae3ef..ea7911c 100644 --- a/internal/websecure/store.go +++ b/internal/websecure/store.go @@ -96,7 +96,11 @@ func (s *CertStore) loadCertificate(hostname string) { s.certificates[hostname] = &cert - s.log.Info().Str("hostname", hostname).Msg("Loaded certificate") + if hostname == selfSignerCAMagicName { + s.log.Info().Msg("loaded CA certificate") + } else { + s.log.Info().Str("hostname", hostname).Msg("loaded certificate") + } } // GetCertificate returns the certificate for the given hostname @@ -131,7 +135,7 @@ func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key if !ignoreWarning { return nil, fmt.Errorf("certificate does not match hostname: %w", err) } - s.log.Warn().Err(err).Msg("Certificate does not match hostname") + s.log.Warn().Err(err).Msg("certificate does not match hostname") } } diff --git a/jsonrpc.go b/jsonrpc.go index 248390e..d35f635 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -962,6 +962,10 @@ var rpcHandlers = map[string]RPCHandler{ "getDeviceID": {Func: rpcGetDeviceID}, "deregisterDevice": {Func: rpcDeregisterDevice}, "getCloudState": {Func: rpcGetCloudState}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, diff --git a/log.go b/log.go index ed46852..b353a2c 100644 --- a/log.go +++ b/log.go @@ -1,291 +1,32 @@ package kvm import ( - "fmt" - "io" - "os" - "strings" - "sync" - "time" - - "github.com/pion/logging" + "github.com/jetkvm/kvm/internal/logging" "github.com/rs/zerolog" ) -type Logger struct { - l *zerolog.Logger - scopeLoggers map[string]*zerolog.Logger - scopeLevels map[string]zerolog.Level - scopeLevelMutex sync.Mutex - - defaultLogLevelFromEnv zerolog.Level - defaultLogLevelFromConfig zerolog.Level - defaultLogLevel zerolog.Level -} - -const ( - defaultLogLevel = zerolog.ErrorLevel -) - -type logOutput struct { - mu *sync.Mutex -} - -func (w *logOutput) Write(p []byte) (n int, err error) { - w.mu.Lock() - defer w.mu.Unlock() - - // TODO: write to file or syslog - - return len(p), nil -} - -var ( - consoleLogOutput io.Writer = zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: time.RFC3339, - PartsOrder: []string{"time", "level", "scope", "component", "message"}, - FieldsExclude: []string{"scope", "component"}, - FormatPartValueByName: func(value interface{}, name string) string { - val := fmt.Sprintf("%s", value) - if name == "component" { - if value == nil { - return "-" - } - } - return val - }, - } - fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}} - defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput) - - zerologLevels = map[string]zerolog.Level{ - "DISABLE": zerolog.Disabled, - "NOLEVEL": zerolog.NoLevel, - "PANIC": zerolog.PanicLevel, - "FATAL": zerolog.FatalLevel, - "ERROR": zerolog.ErrorLevel, - "WARN": zerolog.WarnLevel, - "INFO": zerolog.InfoLevel, - "DEBUG": zerolog.DebugLevel, - "TRACE": zerolog.TraceLevel, - } - - rootZerologLogger = zerolog.New(defaultLogOutput).With(). - Str("scope", "jetkvm"). - Timestamp(). - Stack(). - Logger() - rootLogger = NewLogger(rootZerologLogger) -) - -func NewLogger(zerologLogger zerolog.Logger) *Logger { - return &Logger{ - l: &zerologLogger, - scopeLoggers: make(map[string]*zerolog.Logger), - scopeLevels: make(map[string]zerolog.Level), - scopeLevelMutex: sync.Mutex{}, - defaultLogLevelFromEnv: -2, - defaultLogLevelFromConfig: -2, - defaultLogLevel: defaultLogLevel, - } -} - -func (l *Logger) updateLogLevel() { - l.scopeLevelMutex.Lock() - defer l.scopeLevelMutex.Unlock() - - l.scopeLevels = make(map[string]zerolog.Level) - - finalDefaultLogLevel := l.defaultLogLevel - - for name, level := range zerologLevels { - env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) - - if env == "" { - env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) - } - - if env == "" { - env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) - } - - if env == "" { - continue - } - - if strings.ToLower(env) == "all" { - l.defaultLogLevelFromEnv = level - - if finalDefaultLogLevel > level { - finalDefaultLogLevel = level - } - - continue - } - - scopes := strings.Split(strings.ToLower(env), ",") - for _, scope := range scopes { - l.scopeLevels[scope] = level - } - } - - l.defaultLogLevel = finalDefaultLogLevel -} - -func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level { - if l.scopeLevels == nil { - l.updateLogLevel() - } - - var scopeLevel zerolog.Level - if l.defaultLogLevelFromConfig != -2 { - scopeLevel = l.defaultLogLevelFromConfig - } - if l.defaultLogLevelFromEnv != -2 { - scopeLevel = l.defaultLogLevelFromEnv - } - - // if the scope is not in the map, use the default level from the root logger - if level, ok := l.scopeLevels[scope]; ok { - scopeLevel = level - } - - return scopeLevel -} - -func (l *Logger) newScopeLogger(scope string) zerolog.Logger { - scopeLevel := l.getScopeLoggerLevel(scope) - logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger() - - return logger -} - -func (l *Logger) getLogger(scope string) *zerolog.Logger { - logger, ok := l.scopeLoggers[scope] - if !ok || logger == nil { - scopeLogger := l.newScopeLogger(scope) - l.scopeLoggers[scope] = &scopeLogger - } - - return l.scopeLoggers[scope] -} - -func (l *Logger) UpdateLogLevel() { - needUpdate := false - - if config != nil && config.DefaultLogLevel != "" { - if logLevel, ok := zerologLevels[config.DefaultLogLevel]; ok { - l.defaultLogLevelFromConfig = logLevel - } else { - l.l.Warn().Str("logLevel", config.DefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR") - } - - if l.defaultLogLevelFromConfig != l.defaultLogLevel { - needUpdate = true - } - } - - l.updateLogLevel() - - if needUpdate { - for scope, logger := range l.scopeLoggers { - currentLevel := logger.GetLevel() - targetLevel := l.getScopeLoggerLevel(scope) - if currentLevel != targetLevel { - *logger = l.newScopeLogger(scope) - } - } - } -} - func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { - if l == nil { - l = rootLogger.getLogger("jetkvm") - } - - l.Error().Err(err).Msgf(format, args...) - - if err == nil { - return fmt.Errorf(format, args...) - } - - err_msg := err.Error() + ": %v" - err_args := append(args, err) - - return fmt.Errorf(err_msg, err_args...) + return logging.ErrorfL(l, format, err, args...) } var ( - logger = rootLogger.getLogger("jetkvm") - cloudLogger = rootLogger.getLogger("cloud") - websocketLogger = rootLogger.getLogger("websocket") - webrtcLogger = rootLogger.getLogger("webrtc") - nativeLogger = rootLogger.getLogger("native") - nbdLogger = rootLogger.getLogger("nbd") - ntpLogger = rootLogger.getLogger("ntp") - jsonRpcLogger = rootLogger.getLogger("jsonrpc") - watchdogLogger = rootLogger.getLogger("watchdog") - websecureLogger = rootLogger.getLogger("websecure") - otaLogger = rootLogger.getLogger("ota") - serialLogger = rootLogger.getLogger("serial") - terminalLogger = rootLogger.getLogger("terminal") - displayLogger = rootLogger.getLogger("display") - wolLogger = rootLogger.getLogger("wol") - usbLogger = rootLogger.getLogger("usb") + logger = logging.GetSubsystemLogger("jetkvm") + networkLogger = logging.GetSubsystemLogger("network") + cloudLogger = logging.GetSubsystemLogger("cloud") + websocketLogger = logging.GetSubsystemLogger("websocket") + webrtcLogger = logging.GetSubsystemLogger("webrtc") + nativeLogger = logging.GetSubsystemLogger("native") + nbdLogger = logging.GetSubsystemLogger("nbd") + timesyncLogger = logging.GetSubsystemLogger("timesync") + jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc") + watchdogLogger = logging.GetSubsystemLogger("watchdog") + websecureLogger = logging.GetSubsystemLogger("websecure") + otaLogger = logging.GetSubsystemLogger("ota") + serialLogger = logging.GetSubsystemLogger("serial") + terminalLogger = logging.GetSubsystemLogger("terminal") + displayLogger = logging.GetSubsystemLogger("display") + wolLogger = logging.GetSubsystemLogger("wol") + usbLogger = logging.GetSubsystemLogger("usb") // external components - ginLogger = rootLogger.getLogger("gin") + ginLogger = logging.GetSubsystemLogger("gin") ) - -type pionLogger struct { - logger *zerolog.Logger -} - -// Print all messages except trace. -func (c pionLogger) Trace(msg string) { - c.logger.Trace().Msg(msg) -} -func (c pionLogger) Tracef(format string, args ...interface{}) { - c.logger.Trace().Msgf(format, args...) -} - -func (c pionLogger) Debug(msg string) { - c.logger.Debug().Msg(msg) -} -func (c pionLogger) Debugf(format string, args ...interface{}) { - c.logger.Debug().Msgf(format, args...) -} -func (c pionLogger) Info(msg string) { - c.logger.Info().Msg(msg) -} -func (c pionLogger) Infof(format string, args ...interface{}) { - c.logger.Info().Msgf(format, args...) -} -func (c pionLogger) Warn(msg string) { - c.logger.Warn().Msg(msg) -} -func (c pionLogger) Warnf(format string, args ...interface{}) { - c.logger.Warn().Msgf(format, args...) -} -func (c pionLogger) Error(msg string) { - c.logger.Error().Msg(msg) -} -func (c pionLogger) Errorf(format string, args ...interface{}) { - c.logger.Error().Msgf(format, args...) -} - -// customLoggerFactory satisfies the interface logging.LoggerFactory -// This allows us to create different loggers per subsystem. So we can -// add custom behavior. -type pionLoggerFactory struct{} - -func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { - logger := rootLogger.getLogger(subsystem).With(). - Str("scope", "pion"). - Str("component", subsystem). - Logger() - - return pionLogger{logger: &logger} -} - -var defaultLoggerFactory = &pionLoggerFactory{} diff --git a/main.go b/main.go index 9eab708..25fbb3a 100644 --- a/main.go +++ b/main.go @@ -15,28 +15,54 @@ var appCtx context.Context func Main() { LoadConfig() - logger.Debug().Msg("config loaded") var cancel context.CancelFunc appCtx, cancel = context.WithCancel(context.Background()) defer cancel() - logger.Info().Msg("starting JetKvm") + + systemVersionLocal, appVersionLocal, err := GetLocalVersion() + if err != nil { + logger.Warn().Err(err).Msg("failed to get local version") + } + + logger.Info(). + Interface("system_version", systemVersionLocal). + Interface("app_version", appVersionLocal). + Msg("starting JetKVM") go runWatchdog() go confirmCurrentSystem() http.DefaultClient.Timeout = 1 * time.Minute - err := rootcerts.UpdateDefaultTransport() + err = rootcerts.UpdateDefaultTransport() if err != nil { - logger.Warn().Err(err).Msg("failed to load CA certs") + logger.Warn().Err(err).Msg("failed to load Root CA certificates") + } + logger.Info(). + Int("ca_certs_loaded", len(rootcerts.Certs())). + Msg("loaded Root CA certificates") + + // Initialize network + if err := initNetwork(); err != nil { + logger.Error().Err(err).Msg("failed to initialize network") + os.Exit(1) } - initNetwork() + // Initialize time sync + initTimeSync() + timeSync.Start() - go TimeSyncLoop() + // Initialize mDNS + if err := initMdns(); err != nil { + logger.Error().Err(err).Msg("failed to initialize mDNS") + os.Exit(1) + } + // Initialize native ctrl socket server StartNativeCtrlSocketServer() + + // Initialize native video socket server StartNativeVideoSocketServer() initPrometheus() diff --git a/mdns.go b/mdns.go new file mode 100644 index 0000000..d7a3b55 --- /dev/null +++ b/mdns.go @@ -0,0 +1,29 @@ +package kvm + +import ( + "github.com/jetkvm/kvm/internal/mdns" +) + +var mDNS *mdns.MDNS + +func initMdns() error { + m, err := mdns.NewMDNS(&mdns.MDNSOptions{ + Logger: logger, + LocalNames: []string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, + ListenOptions: &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + }, + }) + if err != nil { + return err + } + + // do not start the server yet, as we need to wait for the network state to be set + mDNS = m + + return nil +} diff --git a/native.go b/native.go index b61598c..496f580 100644 --- a/native.go +++ b/native.go @@ -8,13 +8,10 @@ import ( "io" "net" "os" - "os/exec" "sync" - "syscall" "time" "github.com/jetkvm/kvm/resource" - "github.com/rs/zerolog" "github.com/pion/webrtc/v4/pkg/media" ) @@ -36,19 +33,6 @@ type CtrlResponse struct { Data json.RawMessage `json:"data,omitempty"` } -type nativeOutput struct { - mu *sync.Mutex - logger *zerolog.Event -} - -func (w *nativeOutput) Write(p []byte) (n int, err error) { - w.mu.Lock() - defer w.mu.Unlock() - - w.logger.Msg(string(p)) - return len(p), nil -} - type EventHandler func(event CtrlResponse) var seq int32 = 1 @@ -262,30 +246,8 @@ func ExtractAndRunNativeBin() error { return fmt.Errorf("failed to make binary executable: %w", err) } // Run the binary in the background - cmd := exec.Command(binaryPath) - - nativeOutputLock := sync.Mutex{} - nativeStdout := &nativeOutput{ - mu: &nativeOutputLock, - logger: nativeLogger.Info().Str("pipe", "stdout"), - } - nativeStderr := &nativeOutput{ - mu: &nativeOutputLock, - logger: nativeLogger.Info().Str("pipe", "stderr"), - } - - // Redirect stdout and stderr to the current process - cmd.Stdout = nativeStdout - cmd.Stderr = nativeStderr - - // Set the process group ID so we can kill the process and its children when this process exits - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - Pdeathsig: syscall.SIGKILL, - } - - // Start the command - if err := cmd.Start(); err != nil { + cmd, err := startNativeBinary(binaryPath) + if err != nil { return fmt.Errorf("failed to start binary: %w", err) } @@ -335,7 +297,10 @@ func ensureBinaryUpdated(destPath string) error { _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - nativeLogger.Info().Msg("writing jetkvm_native") + nativeLogger.Info(). + Interface("hash", srcHash). + Msg("writing jetkvm_native") + _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { diff --git a/native_linux.go b/native_linux.go new file mode 100644 index 0000000..54d2150 --- /dev/null +++ b/native_linux.go @@ -0,0 +1,57 @@ +//go:build linux + +package kvm + +import ( + "fmt" + "os/exec" + "sync" + "syscall" + + "github.com/rs/zerolog" +) + +type nativeOutput struct { + mu *sync.Mutex + logger *zerolog.Event +} + +func (w *nativeOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.logger.Msg(string(p)) + return len(p), nil +} + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary in the background + cmd := exec.Command(binaryPath) + + nativeOutputLock := sync.Mutex{} + nativeStdout := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stdout"), + } + nativeStderr := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stderr"), + } + + // Redirect stdout and stderr to the current process + cmd.Stdout = nativeStdout + cmd.Stderr = nativeStderr + + // Set the process group ID so we can kill the process and its children when this process exits + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } + + // Start the command + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start binary: %w", err) + } + + return cmd, nil +} diff --git a/native_notlinux.go b/native_notlinux.go new file mode 100644 index 0000000..df6df74 --- /dev/null +++ b/native_notlinux.go @@ -0,0 +1,12 @@ +//go:build !linux + +package kvm + +import ( + "fmt" + "os/exec" +) + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + return nil, fmt.Errorf("not supported") +} diff --git a/network.go b/network.go index 6948d9a..8d9261b 100644 --- a/network.go +++ b/network.go @@ -1,237 +1,107 @@ package kvm import ( - "bytes" "fmt" - "net" - "os" - "strings" - "time" - "os/exec" - - "github.com/hashicorp/go-envparse" - "github.com/pion/mdns/v2" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" - - "github.com/vishvananda/netlink" - "github.com/vishvananda/netlink/nl" + "github.com/jetkvm/kvm/internal/network" + "github.com/jetkvm/kvm/internal/udhcpc" ) -var mDNSConn *mdns.Conn - -var networkState NetworkState - -type NetworkState struct { - Up bool - IPv4 string - IPv6 string - MAC string - - checked bool -} - -type LocalIpInfo struct { - IPv4 string - IPv6 string - MAC string -} - const ( - NetIfName = "eth0" - DHCPLeaseFile = "/run/udhcpc.%s.info" + NetIfName = "eth0" ) -// setDhcpClientState sends signals to udhcpc to change it's current mode -// of operation. Setting active to true will force udhcpc to renew the DHCP lease. -// Setting active to false will put udhcpc into idle mode. -func setDhcpClientState(active bool) { - var signal string - if active { - signal = "-SIGUSR1" - } else { - signal = "-SIGUSR2" - } +var ( + networkState *network.NetworkInterfaceState +) - cmd := exec.Command("/usr/bin/killall", signal, "udhcpc") - if err := cmd.Run(); err != nil { - logger.Warn().Err(err).Msg("network: setDhcpClientState: failed to change udhcpc state") +func networkStateChanged() { + // do not block the main thread + go waitCtrlAndRequestDisplayUpdate(true) + + // always restart mDNS when the network state changes + if mDNS != nil { + _ = mDNS.SetLocalNames([]string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, true) } } -func checkNetworkState() { - iface, err := netlink.LinkByName(NetIfName) - if err != nil { - logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get interface") - return - } +func initNetwork() error { + ensureConfigLoaded() - newState := NetworkState{ - Up: iface.Attrs().OperState == netlink.OperUp, - MAC: iface.Attrs().HardwareAddr.String(), + state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{ + DefaultHostname: GetDefaultHostname(), + InterfaceName: NetIfName, + NetworkConfig: config.NetworkConfig, + Logger: networkLogger, + OnStateChange: func(state *network.NetworkInterfaceState) { + networkStateChanged() + }, + OnInitialCheck: func(state *network.NetworkInterfaceState) { + networkStateChanged() + }, + OnDhcpLeaseChange: func(lease *udhcpc.Lease) { + networkStateChanged() - checked: true, - } - - addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL) - if err != nil { - logger.Warn().Err(err).Str("interface", NetIfName).Msg("failed to get addresses") - } - - // If the link is going down, put udhcpc into idle mode. - // If the link is coming back up, activate udhcpc and force it to renew the lease. - if newState.Up != networkState.Up { - setDhcpClientState(newState.Up) - } - - for _, addr := range addrs { - if addr.IP.To4() != nil { - if !newState.Up && networkState.Up { - // If the network is going down, remove all IPv4 addresses from the interface. - logger.Info().Str("address", addr.IP.String()).Msg("network: state transitioned to down, removing IPv4 address") - err := netlink.AddrDel(iface, &addr) - if err != nil { - logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("network: failed to delete address") - } - - newState.IPv4 = "..." - } else { - newState.IPv4 = addr.IP.String() + if currentSession == nil { + return } - } else if addr.IP.To16() != nil && newState.IPv6 == "" { - newState.IPv6 = addr.IP.String() - } - } - if newState != networkState { - logger.Info(). - Interface("newState", newState). - Interface("oldState", networkState). - Msg("network state changed") + writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession) + }, + OnConfigChange: func(networkConfig *network.NetworkConfig) { + config.NetworkConfig = networkConfig + networkStateChanged() - // restart MDNS - _ = startMDNS() - networkState = newState - requestDisplayUpdate() - } -} - -func startMDNS() error { - // If server was previously running, stop it - if mDNSConn != nil { - logger.Info().Msg("stopping mDNS server") - err := mDNSConn.Close() - if err != nil { - logger.Warn().Err(err).Msg("failed to stop mDNS server") - } - } - - // Start a new server - hostname := "jetkvm.local" - - scopedLogger := logger.With().Str("hostname", hostname).Logger() - scopedLogger.Info().Msg("starting mDNS server") - - addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) - if err != nil { - return err - } - - addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) - if err != nil { - return err - } - - l4, err := net.ListenUDP("udp4", addr4) - if err != nil { - return err - } - - l6, err := net.ListenUDP("udp6", addr6) - if err != nil { - return err - } - - mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ - LocalNames: []string{hostname}, //TODO: make it configurable - LoggerFactory: defaultLoggerFactory, + if mDNS != nil { + _ = mDNS.SetListenOptions(networkConfig.GetMDNSMode()) + _ = mDNS.SetLocalNames([]string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, true) + } + }, }) - if err != nil { - scopedLogger.Warn().Err(err).Msg("failed to start mDNS server") - mDNSConn = nil + + if state == nil { + if err == nil { + return fmt.Errorf("failed to create NetworkInterfaceState") + } return err } - //defer server.Close() + + if err := state.Run(); err != nil { + return err + } + + networkState = state + return nil } -func getNTPServersFromDHCPInfo() ([]string, error) { - buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName)) - if err != nil { - // do not return error if file does not exist - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to load udhcpc info: %w", err) - } - - // parse udhcpc info - env, err := envparse.Parse(bytes.NewReader(buf)) - if err != nil { - return nil, fmt.Errorf("failed to parse udhcpc info: %w", err) - } - - val, ok := env["ntpsrv"] - if !ok { - return nil, nil - } - - var servers []string - - for _, server := range strings.Fields(val) { - if net.ParseIP(server) == nil { - logger.Info().Str("server", server).Msg("invalid NTP server IP, ignoring") - } - servers = append(servers, server) - } - - return servers, nil +func rpcGetNetworkState() network.RpcNetworkState { + return networkState.RpcGetNetworkState() } -func initNetwork() { - ensureConfigLoaded() - - updates := make(chan netlink.LinkUpdate) - done := make(chan struct{}) - - if err := netlink.LinkSubscribe(updates, done); err != nil { - logger.Warn().Err(err).Msg("failed to subscribe to link updates") - return - } - - go func() { - waitCtrlClientConnected() - checkNetworkState() - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case update := <-updates: - if update.Link.Attrs().Name == NetIfName { - logger.Info().Interface("update", update).Msg("link update") - checkNetworkState() - } - case <-ticker.C: - checkNetworkState() - case <-done: - return - } - } - }() - err := startMDNS() - if err != nil { - logger.Warn().Err(err).Msg("failed to run mDNS") - } +func rpcGetNetworkSettings() network.RpcNetworkSettings { + return networkState.RpcGetNetworkSettings() +} + +func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) { + s := networkState.RpcSetNetworkSettings(settings) + if s != nil { + return nil, s + } + + if err := SaveConfig(); err != nil { + return nil, err + } + + return &network.RpcNetworkSettings{NetworkConfig: *config.NetworkConfig}, nil +} + +func rpcRenewDHCPLease() error { + return networkState.RpcRenewDHCPLease() } diff --git a/ntp.go b/ntp.go deleted file mode 100644 index a104c56..0000000 --- a/ntp.go +++ /dev/null @@ -1,197 +0,0 @@ -package kvm - -import ( - "fmt" - "net/http" - "os/exec" - "strconv" - "time" - - "github.com/beevik/ntp" -) - -const ( - timeSyncRetryStep = 5 * time.Second - timeSyncRetryMaxInt = 1 * time.Minute - timeSyncWaitNetChkInt = 100 * time.Millisecond - timeSyncWaitNetUpInt = 3 * time.Second - timeSyncInterval = 1 * time.Hour - timeSyncTimeout = 2 * time.Second -) - -var ( - builtTimestamp string - timeSyncRetryInterval = 0 * time.Second - timeSyncSuccess = false - defaultNTPServers = []string{ - "time.cloudflare.com", - "time.apple.com", - } -) - -func isTimeSyncNeeded() bool { - if builtTimestamp == "" { - ntpLogger.Warn().Msg("Built timestamp is not set, time sync is needed") - return true - } - - ts, err := strconv.Atoi(builtTimestamp) - if err != nil { - ntpLogger.Warn().Str("error", err.Error()).Msg("Failed to parse built timestamp") - return true - } - - // builtTimestamp is UNIX timestamp in seconds - builtTime := time.Unix(int64(ts), 0) - now := time.Now() - - ntpLogger.Debug().Str("built_time", builtTime.Format(time.RFC3339)).Str("now", now.Format(time.RFC3339)).Msg("Built time and now") - - if now.Sub(builtTime) < 0 { - ntpLogger.Warn().Msg("System time is behind the built time, time sync is needed") - return true - } - - return false -} - -func TimeSyncLoop() { - for { - if !networkState.checked { - time.Sleep(timeSyncWaitNetChkInt) - continue - } - - if !networkState.Up { - ntpLogger.Info().Msg("Waiting for network to come up") - time.Sleep(timeSyncWaitNetUpInt) - continue - } - - // check if time sync is needed, but do nothing for now - isTimeSyncNeeded() - - ntpLogger.Info().Msg("Syncing system time") - start := time.Now() - err := SyncSystemTime() - if err != nil { - ntpLogger.Error().Str("error", err.Error()).Msg("Failed to sync system time") - - // retry after a delay - timeSyncRetryInterval += timeSyncRetryStep - time.Sleep(timeSyncRetryInterval) - // reset the retry interval if it exceeds the max interval - if timeSyncRetryInterval > timeSyncRetryMaxInt { - timeSyncRetryInterval = 0 - } - - continue - } - timeSyncSuccess = true - ntpLogger.Info().Str("now", time.Now().Format(time.RFC3339)). - Str("time_taken", time.Since(start).String()). - Msg("Time sync successful") - time.Sleep(timeSyncInterval) // after the first sync is done - } -} - -func SyncSystemTime() (err error) { - now, err := queryNetworkTime() - if err != nil { - return fmt.Errorf("failed to query network time: %w", err) - } - err = setSystemTime(*now) - if err != nil { - return fmt.Errorf("failed to set system time: %w", err) - } - return nil -} - -func queryNetworkTime() (*time.Time, error) { - ntpServers, err := getNTPServersFromDHCPInfo() - if err != nil { - ntpLogger.Info().Err(err).Msg("failed to get NTP servers from DHCP info") - } - - if ntpServers == nil { - ntpServers = defaultNTPServers - ntpLogger.Info(). - Interface("ntp_servers", ntpServers). - Msg("Using default NTP servers") - } else { - ntpLogger.Info(). - Interface("ntp_servers", ntpServers). - Msg("Using NTP servers from DHCP") - } - - for _, server := range ntpServers { - now, err := queryNtpServer(server, timeSyncTimeout) - if err == nil { - ntpLogger.Info(). - Str("ntp_server", server). - Str("time", now.Format(time.RFC3339)). - Msg("NTP server returned time") - return now, nil - } else { - ntpLogger.Error(). - Str("ntp_server", server). - Str("error", err.Error()). - Msg("failed to query NTP server") - } - } - - httpUrls := []string{ - "http://apple.com", - "http://cloudflare.com", - } - for _, url := range httpUrls { - now, err := queryHttpTime(url, timeSyncTimeout) - if err == nil { - ntpLogger.Info(). - Str("http_url", url). - Str("time", now.Format(time.RFC3339)). - Msg("HTTP server returned time") - return now, nil - } else { - ntpLogger.Error(). - Str("http_url", url). - Str("error", err.Error()). - Msg("failed to query HTTP server") - } - } - - return nil, ErrorfL(ntpLogger, "failed to query network time, all NTP servers and HTTP servers failed", nil) -} - -func queryNtpServer(server string, timeout time.Duration) (now *time.Time, err error) { - resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) - if err != nil { - return nil, err - } - return &resp.Time, nil -} - -func queryHttpTime(url string, timeout time.Duration) (*time.Time, error) { - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Head(url) - if err != nil { - return nil, err - } - dateStr := resp.Header.Get("Date") - now, err := time.Parse(time.RFC1123, dateStr) - if err != nil { - return nil, err - } - return &now, nil -} - -func setSystemTime(now time.Time) error { - nowStr := now.Format("2006-01-02 15:04:05") - output, err := exec.Command("date", "-s", nowStr).CombinedOutput() - if err != nil { - return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) - } - return nil -} diff --git a/ota.go b/ota.go index a5da772..0559978 100644 --- a/ota.go +++ b/ota.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "crypto/tls" "encoding/hex" "encoding/json" "fmt" @@ -16,6 +17,7 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/gwatts/rootcerts" "github.com/rs/zerolog" ) @@ -127,10 +129,14 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error creating request: %w", err) } - // TODO: set a separate timeout for the download but keep the TLS handshake short - // use Transport here will cause CA certificate validation failure so we temporarily removed it client := http.Client{ Timeout: 10 * time.Minute, + Transport: &http.Transport{ + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: &tls.Config{ + RootCAs: rootcerts.ServerCertPool(), + }, + }, } resp, err := client.Do(req) diff --git a/resource/jetkvm_native b/resource/jetkvm_native index 0d0719c796e876a7b4277cacc1fbac0772cf10e7..084ce14970c8ef559f9c2e2bcdfd05d5a8993952 100644 GIT binary patch delta 217848 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%0DNCF?H0wY#XdT*#ZCKLc`ookvFC`X3gw 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

    jgn(2`Y77r9+`;oXtVnAPw3dq&38Ytr-y~6{=ZI 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 z zV=p^kjYz$C%EZZOB4#ehI`iV3f(y>gk>8hOPCR9b$d^lUr%s+iyYpRo*2F0xc~w$9 z&p!2lbz;Qqnz<#_*Vr8gtkDDWa&jl7q)xG4I$)ihQ&B#%e16qJ$*J18^rF56B^8LU zdE|nE*;NbW*SvZBT~xHV!s$LkWrvqySU$hN`Bgh{V&~M)t%c!XlTNXZI%tg?Ffrwn z$y27kBiHBG6c|>Oa|Bpp0$6;A%MOA6FR8Kt2s-(CtL9Ud}@0{_K73H_5MZ{Y6q%W*c_4f diff --git a/resource/jetkvm_native.sha256 b/resource/jetkvm_native.sha256 index 65da816..b540b94 100644 --- a/resource/jetkvm_native.sha256 +++ b/resource/jetkvm_native.sha256 @@ -1 +1 @@ -c0803a9185298398eff9a925de69bd0ca882cd5983b989a45b748648146475c6 +4b925c7aa73d2e35a227833e806658cb17e1d25900611f93ed70b11ac9f1716d diff --git a/timesync.go b/timesync.go new file mode 100644 index 0000000..7b25fe2 --- /dev/null +++ b/timesync.go @@ -0,0 +1,53 @@ +package kvm + +import ( + "strconv" + "time" + + "github.com/jetkvm/kvm/internal/timesync" +) + +var ( + timeSync *timesync.TimeSync + builtTimestamp string +) + +func isTimeSyncNeeded() bool { + if builtTimestamp == "" { + timesyncLogger.Warn().Msg("built timestamp is not set, time sync is needed") + return true + } + + ts, err := strconv.Atoi(builtTimestamp) + if err != nil { + timesyncLogger.Warn().Str("error", err.Error()).Msg("failed to parse built timestamp") + return true + } + + // builtTimestamp is UNIX timestamp in seconds + builtTime := time.Unix(int64(ts), 0) + now := time.Now() + + if now.Sub(builtTime) < 0 { + timesyncLogger.Warn(). + Str("built_time", builtTime.Format(time.RFC3339)). + Str("now", now.Format(time.RFC3339)). + Msg("system time is behind the built time, time sync is needed") + return true + } + + return false +} + +func initTimeSync() { + timeSync = timesync.NewTimeSync(×ync.TimeSyncOptions{ + Logger: timesyncLogger, + NetworkConfig: config.NetworkConfig, + PreCheckFunc: func() (bool, error) { + if !networkState.IsOnline() { + return false, nil + } + return true, nil + }, + }) +} diff --git a/ui/dev_device.sh b/ui/dev_device.sh index 650cadd..2c7b497 100755 --- a/ui/dev_device.sh +++ b/ui/dev_device.sh @@ -15,5 +15,15 @@ echo "└─────────────────────── # Set the environment variable and run Vite echo "Starting development server with JetKVM device at: $ip_address" + +# Check if pwd is the current directory of the script +if [ "$(pwd)" != "$(dirname "$0")" ]; then + pushd "$(dirname "$0")" > /dev/null + echo "Changed directory to: $(pwd)" +fi + sleep 1 + JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device + +popd > /dev/null diff --git a/ui/package-lock.json b/ui/package-lock.json index b51a2ea..9e77e10 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -19,6 +19,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", + "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^10.2.3", "framer-motion": "^11.15.0", @@ -2433,6 +2434,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/ui/package.json b/ui/package.json index 3160297..4dab092 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,6 +30,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "cva": "^1.0.0-beta.1", + "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^10.2.3", "framer-motion": "^11.15.0", diff --git a/ui/public/sse.html b/ui/public/sse.html new file mode 120000 index 0000000..0a8b4f3 --- /dev/null +++ b/ui/public/sse.html @@ -0,0 +1 @@ +../../internal/logging/sse.html \ No newline at end of file diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 0fa4121..db1fd04 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -663,6 +663,95 @@ export const useDeviceStore = create(set => ({ setSystemVersion: version => set({ systemVersion: version }), })); +export interface DhcpLease { + ip?: string; + netmask?: string; + broadcast?: string; + ttl?: string; + mtu?: string; + hostname?: string; + domain?: string; + bootp_next_server?: string; + bootp_server_name?: string; + bootp_file?: string; + timezone?: string; + routers?: string[]; + dns?: string[]; + ntp_servers?: string[]; + lpr_servers?: string[]; + _time_servers?: string[]; + _name_servers?: string[]; + _log_servers?: string[]; + _cookie_servers?: string[]; + _wins_servers?: string[]; + _swap_server?: string; + boot_size?: string; + root_path?: string; + lease?: string; + lease_expiry?: Date; + dhcp_type?: string; + server_id?: string; + message?: string; + tftp?: string; + bootfile?: string; +} + +export interface IPv6Address { + address: string; + prefix: string; + valid_lifetime: string; + preferred_lifetime: string; + scope: string; +} + +export interface NetworkState { + interface_name?: string; + mac_address?: string; + ipv4?: string; + ipv4_addresses?: string[]; + ipv6?: string; + ipv6_addresses?: IPv6Address[]; + ipv6_link_local?: string; + dhcp_lease?: DhcpLease; + + setNetworkState: (state: NetworkState) => void; + setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void; + setDhcpLeaseExpiry: (expiry: Date) => void; +} + + +export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown"; +export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; +export type LLDPMode = "disabled" | "basic" | "all" | "unknown"; +export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; +export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown"; + +export interface NetworkSettings { + hostname: string; + domain: string; + ipv4_mode: IPv4Mode; + ipv6_mode: IPv6Mode; + lldp_mode: LLDPMode; + lldp_tx_tlvs: string[]; + mdns_mode: mDNSMode; + time_sync_mode: TimeSyncMode; +} + +export const useNetworkStateStore = create((set, get) => ({ + setNetworkState: (state: NetworkState) => set(state), + setDhcpLease: (lease: NetworkState["dhcp_lease"]) => set({ dhcp_lease: lease }), + setDhcpLeaseExpiry: (expiry: Date) => { + const lease = get().dhcp_lease; + if (!lease) { + console.warn("No lease found"); + return; + } + + lease.lease_expiry = expiry; + set({ dhcp_lease: lease }); + } +})); + export interface KeySequenceStep { keys: string[]; modifiers: string[]; @@ -767,8 +856,8 @@ export const useMacrosStore = create((set, get) => ({ for (let i = 0; i < macro.steps.length; i++) { const step = macro.steps[i]; if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) { - console.error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); - throw new Error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); } } } diff --git a/ui/src/main.tsx b/ui/src/main.tsx index e09a2a9..f4bdd34 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -42,6 +42,7 @@ import SettingsVideoRoute from "./routes/devices.$id.settings.video"; import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index"; import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update"; +import SettingsNetworkRoute from "./routes/devices.$id.settings.network"; import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth"; import SettingsMacrosRoute from "./routes/devices.$id.settings.macros"; import SettingsMacrosAddRoute from "./routes/devices.$id.settings.macros.add"; @@ -156,6 +157,10 @@ if (isOnDevice) { path: "hardware", element: , }, + { + path: "network", + element: , + }, { path: "access", children: [ diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx new file mode 100644 index 0000000..59d52ef --- /dev/null +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -0,0 +1,408 @@ +import { useCallback, useEffect, useState } from "react"; + +import { SelectMenuBasic } from "../components/SelectMenuBasic"; +import { SettingsPageHeader } from "../components/SettingsPageheader"; + +import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import notifications from "@/notifications"; +import { Button } from "@components/Button"; +import { GridCard } from "@components/Card"; +import InputField from "@components/InputField"; +import { SettingsItem } from "./devices.$id.settings"; + +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +dayjs.extend(relativeTime); + +const defaultNetworkSettings: NetworkSettings = { + hostname: "", + domain: "", + ipv4_mode: "unknown", + ipv6_mode: "unknown", + lldp_mode: "unknown", + lldp_tx_tlvs: [], + mdns_mode: "unknown", + time_sync_mode: "unknown", +} + +export function LifeTimeLabel({ lifetime }: { lifetime: string }) { + if (lifetime == "") { + return N/A; + } + + const [remaining, setRemaining] = useState(null); + + useEffect(() => { + setRemaining(dayjs(lifetime).fromNow()); + + const interval = setInterval(() => { + setRemaining(dayjs(lifetime).fromNow()); + }, 1000 * 30); + return () => clearInterval(interval); + }, [lifetime]); + + return <> + {dayjs(lifetime).format()} + {remaining && <> + {" "} + ({remaining}) + + } + +} + +export default function SettingsNetworkRoute() { + const [send] = useJsonRpc(); + const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]); + + const [networkSettings, setNetworkSettings] = useState(defaultNetworkSettings); + const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); + + const getNetworkSettings = useCallback(() => { + setNetworkSettingsLoaded(false); + send("getNetworkSettings", {}, resp => { + if ("error" in resp) return; + console.log(resp.result); + setNetworkSettings(resp.result as NetworkSettings); + setNetworkSettingsLoaded(true); + }); + }, [send]); + + const setNetworkSettingsRemote = useCallback((settings: NetworkSettings) => { + setNetworkSettingsLoaded(false); + send("setNetworkSettings", { settings }, resp => { + if ("error" in resp) { + notifications.error("Failed to save network settings: " + (resp.error.data ? resp.error.data : resp.error.message)); + setNetworkSettingsLoaded(true); + return; + } + setNetworkSettings(resp.result as NetworkSettings); + setNetworkSettingsLoaded(true); + notifications.success("Network settings saved"); + }); + }, [send]); + + const getNetworkState = useCallback(() => { + send("getNetworkState", {}, resp => { + if ("error" in resp) return; + console.log(resp.result); + setNetworkState(resp.result as NetworkState); + }); + }, [send]); + + const handleRenewLease = useCallback(() => { + send("renewDHCPLease", {}, resp => { + if ("error" in resp) { + notifications.error("Failed to renew lease: " + resp.error.message); + } else { + notifications.success("DHCP lease renewed"); + } + }); + }, [send]); + + useEffect(() => { + getNetworkState(); + getNetworkSettings(); + }, [getNetworkState, getNetworkSettings]); + + const handleIpv4ModeChange = (value: IPv4Mode | string) => { + setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode }); + }; + + const handleIpv6ModeChange = (value: IPv6Mode | string) => { + setNetworkSettings({ ...networkSettings, ipv6_mode: value as IPv6Mode }); + }; + + const handleLldpModeChange = (value: LLDPMode | string) => { + setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode }); + }; + + // const handleLldpTxTlvsChange = (value: string[]) => { + // setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value }); + // }; + + const handleMdnsModeChange = (value: mDNSMode | string) => { + setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode }); + }; + + const handleTimeSyncModeChange = (value: TimeSyncMode | string) => { + setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode }); + }; + + const filterUnknown = useCallback((options: { value: string; label: string; }[]) => { + if (!networkSettingsLoaded) return options; + return options.filter(option => option.value !== "unknown"); + }, [networkSettingsLoaded]); + + return ( +

    + +
    + } + > + + {networkState?.mac_address} + + +
    +
    + + Hostname for the device +
    + + Leave blank for default + + + } + > + { + setNetworkSettings({ ...networkSettings, hostname: e.target.value }); + }} + disabled={!networkSettingsLoaded} + /> +
    +
    +
    + + Domain for the device +
    + + Leave blank to use DHCP provided domain, if there is no domain, use local + + + } + > + { + setNetworkSettings({ ...networkSettings, domain: e.target.value }); + }} + disabled={!networkSettingsLoaded} + /> +
    +
    +
    + + handleIpv4ModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "dhcp", label: "DHCP" }, + // { value: "static", label: "Static" }, + ])} + /> + + {networkState?.dhcp_lease && ( + +
    +
    +
    +

    + Current DHCP Lease +

    +
    +
      + {networkState?.dhcp_lease?.ip &&
    • IP: {networkState?.dhcp_lease?.ip}
    • } + {networkState?.dhcp_lease?.netmask &&
    • Subnet: {networkState?.dhcp_lease?.netmask}
    • } + {networkState?.dhcp_lease?.broadcast &&
    • Broadcast: {networkState?.dhcp_lease?.broadcast}
    • } + {networkState?.dhcp_lease?.ttl &&
    • TTL: {networkState?.dhcp_lease?.ttl}
    • } + {networkState?.dhcp_lease?.mtu &&
    • MTU: {networkState?.dhcp_lease?.mtu}
    • } + {networkState?.dhcp_lease?.hostname &&
    • Hostname: {networkState?.dhcp_lease?.hostname}
    • } + {networkState?.dhcp_lease?.domain &&
    • Domain: {networkState?.dhcp_lease?.domain}
    • } + {networkState?.dhcp_lease?.routers &&
    • Gateway: {networkState?.dhcp_lease?.routers.join(", ")}
    • } + {networkState?.dhcp_lease?.dns &&
    • DNS: {networkState?.dhcp_lease?.dns.join(", ")}
    • } + {networkState?.dhcp_lease?.ntp_servers &&
    • NTP Servers: {networkState?.dhcp_lease?.ntp_servers.join(", ")}
    • } + {networkState?.dhcp_lease?.server_id &&
    • Server ID: {networkState?.dhcp_lease?.server_id}
    • } + {networkState?.dhcp_lease?.bootp_next_server &&
    • BootP Next Server: {networkState?.dhcp_lease?.bootp_next_server}
    • } + {networkState?.dhcp_lease?.bootp_server_name &&
    • BootP Server Name: {networkState?.dhcp_lease?.bootp_server_name}
    • } + {networkState?.dhcp_lease?.bootp_file &&
    • Boot File: {networkState?.dhcp_lease?.bootp_file}
    • } + {networkState?.dhcp_lease?.lease_expiry &&
    • + Lease Expiry: +
    • } + {/* {JSON.stringify(networkState?.dhcp_lease)} */} +
    +
    +
    +
    +
    +
    +
    +
    +
    + )} +
    +
    + + handleIpv6ModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + // { value: "disabled", label: "Disabled" }, + { value: "slaac", label: "SLAAC" }, + // { value: "dhcpv6", label: "DHCPv6" }, + // { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" }, + // { value: "static", label: "Static" }, + // { value: "link_local", label: "Link-local only" }, + ])} + /> + + {networkState?.ipv6_addresses && ( + +
    +
    +
    +

    + IPv6 Information +

    +
    +
    +

    + IPv6 Link-local +

    +

    + {networkState?.ipv6_link_local} +

    +
    +
    +

    + IPv6 Addresses +

    +
      + {networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => ( +
    • + {addr.address} + {addr.valid_lifetime && <> +
      + - valid_lft: {" "} + + + + } + {addr.preferred_lifetime && <> +
      + - pref_lft: {" "} + + + + } +
    • + ))} +
    +
    +
    +
    +
    +
    +
    + )} +
    +
    + + handleLldpModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "basic", label: "Basic" }, + { value: "all", label: "All" }, + ])} + /> + +
    +
    + + handleMdnsModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "auto", label: "Auto" }, + { value: "ipv4_only", label: "IPv4 only" }, + { value: "ipv6_only", label: "IPv6 only" }, + ])} + /> + +
    +
    + + handleTimeSyncModeChange(e.target.value)} + disabled={!networkSettingsLoaded} + options={filterUnknown([ + { value: "unknown", label: "..." }, + // { value: "auto", label: "Auto" }, + { value: "ntp_only", label: "NTP only" }, + { value: "ntp_and_http", label: "NTP and HTTP" }, + { value: "http_only", label: "HTTP only" }, + // { value: "custom", label: "Custom" }, + ])} + /> + +
    +
    +
    +
    + ); +} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index c0b4181..f8e5262 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -9,6 +9,7 @@ import { LuArrowLeft, LuPalette, LuCommand, + LuNetwork, } from "react-icons/lu"; import React, { useEffect, useRef, useState } from "react"; @@ -207,6 +208,17 @@ export default function SettingsRoute() {
    +
    + (isActive ? "active" : "")} + > +
    + +

    Network

    +
    +
    +
    state.setNetworkState); + const setUsbState = useHidStore(state => state.setUsbState); const setHdmiState = useVideoStore(state => state.setHdmiState); @@ -600,6 +604,11 @@ export default function KvmIdRoute() { setHdmiState(resp.params as Parameters[0]); } + if (resp.method === "networkState") { + console.log("Setting network state", resp.params); + setNetworkState(resp.params as NetworkState); + } + if (resp.method === "otaState") { const otaState = resp.params as UpdateState["otaState"]; setOtaState(otaState); diff --git a/ui/vite.config.ts b/ui/vite.config.ts index f8459cd..e47774f 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -35,6 +35,7 @@ export default defineConfig(({ mode, command }) => { "/auth": JETKVM_PROXY_URL, "/storage": JETKVM_PROXY_URL, "/cloud": JETKVM_PROXY_URL, + "/developer": JETKVM_PROXY_URL, } : undefined, }, diff --git a/usb.go b/usb.go index 3395db4..91674c9 100644 --- a/usb.go +++ b/usb.go @@ -66,6 +66,6 @@ func checkUSBState() { usbState = newState usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed") - requestDisplayUpdate() + requestDisplayUpdate(true) triggerUSBStateUpdate() } diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 2b03f1f..79a05d1 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -62,7 +62,11 @@ func onDiskMessage(msg webrtc.DataChannelMessage) { func mountImage(imagePath string) error { err := setMassStorageImage("") if err != nil { - return fmt.Errorf("remove Mass Storage Image Error: %w", err) + return fmt.Errorf("remove mass storage image error: %w", err) + } + err = setMassStorageImage(imagePath) + if err != nil { + return fmt.Errorf("set mass storage image error: %w", err) } err = setMassStorageImage(imagePath) if err != nil { @@ -477,7 +481,6 @@ func handleUploadChannel(d *webrtc.DataChannel) { totalBytesWritten += int64(bytesWritten) sendProgress := time.Since(lastProgressTime) >= 200*time.Millisecond - if totalBytesWritten >= pendingUpload.Size { sendProgress = true close(uploadComplete) diff --git a/video.go b/video.go index d74add8..6fa77b9 100644 --- a/video.go +++ b/video.go @@ -43,7 +43,7 @@ func HandleVideoStateMessage(event CtrlResponse) { } lastVideoState = videoState triggerVideoStateUpdate() - requestDisplayUpdate() + requestDisplayUpdate(true) } func rpcGetVideoState() (VideoInputState, error) { diff --git a/web.go b/web.go index 6e74a13..766eaf5 100644 --- a/web.go +++ b/web.go @@ -9,6 +9,7 @@ import ( "fmt" "io/fs" "net/http" + "net/http/pprof" "path/filepath" "strings" "time" @@ -18,6 +19,7 @@ import ( gin_logger "github.com/gin-contrib/logger" "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -103,6 +105,27 @@ func setupRouter() *gin.Engine { // A Prometheus metrics endpoint. r.GET("/metrics", gin.WrapH(promhttp.Handler())) + // Developer mode protected routes + developerModeRouter := r.Group("/developer/") + developerModeRouter.Use(basicAuthProtectedMiddleware(true)) + { + // pprof + developerModeRouter.GET("/pprof/", gin.WrapF(pprof.Index)) + developerModeRouter.GET("/pprof/cmdline", gin.WrapF(pprof.Cmdline)) + developerModeRouter.GET("/pprof/profile", gin.WrapF(pprof.Profile)) + developerModeRouter.POST("/pprof/symbol", gin.WrapF(pprof.Symbol)) + developerModeRouter.GET("/pprof/symbol", gin.WrapF(pprof.Symbol)) + developerModeRouter.GET("/pprof/trace", gin.WrapF(pprof.Trace)) + developerModeRouter.GET("/pprof/allocs", gin.WrapH(pprof.Handler("allocs"))) + developerModeRouter.GET("/pprof/block", gin.WrapH(pprof.Handler("block"))) + developerModeRouter.GET("/pprof/goroutine", gin.WrapH(pprof.Handler("goroutine"))) + developerModeRouter.GET("/pprof/heap", gin.WrapH(pprof.Handler("heap"))) + developerModeRouter.GET("/pprof/mutex", gin.WrapH(pprof.Handler("mutex"))) + developerModeRouter.GET("/pprof/threadcreate", gin.WrapH(pprof.Handler("threadcreate"))) + + logging.AttachSSEHandler(developerModeRouter) + } + // Protected routes (allows both password and noPassword modes) protected := r.Group("/") protected.Use(protectedMiddleware()) @@ -203,7 +226,7 @@ func handleLocalWebRTCSignal(c *gin.Context) { wsOptions := &websocket.AcceptOptions{ InsecureSkipVerify: true, // Allow connections from any origin OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Info().Bytes("payload", payload).Msg("ping frame received") + scopedLogger.Debug().Bytes("payload", payload).Msg("ping frame received") metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc() metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime() @@ -242,7 +265,12 @@ func handleWebRTCSignalWsMessages( scopedLogger *zerolog.Logger, ) error { runCtx, cancelRun := context.WithCancel(context.Background()) - defer cancelRun() + defer func() { + if isCloudConnection { + setCloudConnectionState(CloudConnectionStateDisconnected) + } + cancelRun() + }() // connection type var sourceType string @@ -459,11 +487,51 @@ func protectedMiddleware() gin.HandlerFunc { } } +func sendErrorJsonThenAbort(c *gin.Context, status int, message string) { + c.JSON(status, gin.H{"error": message}) + c.Abort() +} + +func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc { + return func(c *gin.Context) { + if requireDeveloperMode { + devModeState, err := rpcGetDevModeState() + if err != nil { + sendErrorJsonThenAbort(c, http.StatusInternalServerError, "Failed to get developer mode state") + return + } + + if !devModeState.Enabled { + sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Developer mode is not enabled") + return + } + } + + if config.LocalAuthMode == "noPassword" { + sendErrorJsonThenAbort(c, http.StatusForbidden, "The resource is not available in noPassword mode") + return + } + + // calculate basic auth credentials + _, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", "Basic realm=\"JetKVM\"") + sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Basic auth is required") + return + } + + err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(password)) + if err != nil { + sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Invalid password") + return + } + + c.Next() + } +} + func RunWebServer() { r := setupRouter() - //if strings.Contains(builtAppVersion, "-dev") { - // pprof.Register(r) - //} err := r.Run(":80") if err != nil { panic(err) diff --git a/web_tls.go b/web_tls.go index cbff56b..564f150 100644 --- a/web_tls.go +++ b/web_tls.go @@ -54,7 +54,7 @@ func initCertStore() { func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { switch config.TLSMode { case "self-signed": - if isTimeSyncNeeded() || !timeSyncSuccess { + if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() { return nil, fmt.Errorf("time is not synced") } return certSigner.GetCertificate(info) @@ -174,7 +174,7 @@ func runWebSecureServer() { websecureLogger.Info().Msg("Shutting down websecure server") err := server.Shutdown(context.Background()) if err != nil { - websecureLogger.Error().Err(err).Msg("Failed to shutdown websecure server") + websecureLogger.Error().Err(err).Msg("failed to shutdown websecure server") } } }() diff --git a/webrtc.go b/webrtc.go index 1e093e2..f6c8529 100644 --- a/webrtc.go +++ b/webrtc.go @@ -10,6 +10,7 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/gin-gonic/gin" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" "github.com/rs/zerolog" ) @@ -68,7 +69,7 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { func newSession(config SessionConfig) (*Session, error) { webrtcSettingEngine := webrtc.SettingEngine{ - LoggerFactory: defaultLoggerFactory, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), } iceServer := webrtc.ICEServer{} @@ -205,7 +206,7 @@ func newSession(config SessionConfig) (*Session, error) { var actionSessions = 0 func onActiveSessionsChanged() { - requestDisplayUpdate() + requestDisplayUpdate(true) } func onFirstSessionConnected() { From d79f359c4394b236e142d7e60a030569971931df Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Wed, 16 Apr 2025 02:17:09 +0200 Subject: [PATCH 045/165] chore: bump version to 0.4.0 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2aefdea..062c782 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV := 0.3.9-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.3.8 +VERSION_DEV := 0.4.0-dev$(shell date +%Y%m%d%H%M) +VERSION := 0.3.9 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm From 5a4f1766b70d6d0b1d33b6f7ec928b2edaf78137 Mon Sep 17 00:00:00 2001 From: Peder Toftegaard Olsen Date: Sun, 11 May 2025 17:17:41 +0200 Subject: [PATCH 046/165] feat: UI for changing display orientation * Added UI for changing display orientation. * Fixed lint issue. --- config.go | 2 + display.go | 5 +++ jsonrpc.go | 24 ++++++++++++ ui/src/hooks/stores.ts | 7 ++++ .../routes/devices.$id.settings.hardware.tsx | 37 +++++++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/config.go b/config.go index 23d4c84..196a73d 100644 --- a/config.go +++ b/config.go @@ -89,6 +89,7 @@ type Config struct { KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` EdidString string `json:"hdmi_edid_string"` ActiveExtension string `json:"active_extension"` + DisplayRotation string `json:"display_rotation"` DisplayMaxBrightness int `json:"display_max_brightness"` DisplayDimAfterSec int `json:"display_dim_after_sec"` DisplayOffAfterSec int `json:"display_off_after_sec"` @@ -107,6 +108,7 @@ var defaultConfig = &Config{ AutoUpdateEnabled: true, // Set a default value ActiveExtension: "", KeyboardMacros: []KeyboardMacro{}, + DisplayRotation: "270", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes diff --git a/display.go b/display.go index e2e82e1..f4d2a94 100644 --- a/display.go +++ b/display.go @@ -73,6 +73,10 @@ func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) { return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src}) } +func lvDispSetRotation(rotation string) (*CtrlResponse, error) { + return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation}) +} + func updateLabelIfChanged(objName string, newText string) { if newText != "" && newText != displayedTexts[objName] { _, _ = lvLabelSetText(objName, newText) @@ -373,6 +377,7 @@ func init() { waitCtrlClientConnected() displayLogger.Info().Msg("setting initial display contents") time.Sleep(500 * time.Millisecond) + _, _ = lvDispSetRotation(config.DisplayRotation) updateStaticContents() displayInited = true displayLogger.Info().Msg("display inited") diff --git a/jsonrpc.go b/jsonrpc.go index d35f635..05db3d5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -38,6 +38,10 @@ type JSONRPCEvent struct { Params interface{} `json:"params,omitempty"` } +type DisplayRotationSettings struct { + Rotation string `json:"rotation"` +} + type BacklightSettings struct { MaxBrightness int `json:"max_brightness"` DimAfter int `json:"dim_after"` @@ -280,6 +284,24 @@ func rpcTryUpdate() error { return nil } +func rpcSetDisplayRotation(params DisplayRotationSettings) error { + var err error + _, err = lvDispSetRotation(params.Rotation) + if err == nil { + config.DisplayRotation = params.Rotation + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + } + return err +} + +func rpcGetDisplayRotation() (*DisplayRotationSettings, error) { + return &DisplayRotationSettings{ + Rotation: config.DisplayRotation, + }, nil +} + func rpcSetBacklightSettings(params BacklightSettings) error { blConfig := params @@ -1012,6 +1034,8 @@ var rpcHandlers = map[string]RPCHandler{ "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "resetConfig": {Func: rpcResetConfig}, + "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, + "getDisplayRotation": {Func: rpcGetDisplayRotation}, "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, "getBacklightSettings": {Func: rpcGetBacklightSettings}, "getDCPowerState": {Func: rpcGetDCPowerState}, diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index db1fd04..c100d88 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -292,6 +292,9 @@ interface SettingsState { developerMode: boolean; setDeveloperMode: (enabled: boolean) => void; + displayRotation: string; + setDisplayRotation: (rotation: string) => void; + backlightSettings: BacklightSettings; setBacklightSettings: (settings: BacklightSettings) => void; } @@ -312,6 +315,10 @@ export const useSettingsStore = create( developerMode: false, setDeveloperMode: enabled => set({ developerMode: enabled }), + displayRotation: "270", + setDisplayRotation: (rotation: string) => + set({ displayRotation: rotation }), + backlightSettings: { max_brightness: 100, dim_after: 10000, diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 9fde3e3..82cc6a1 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -15,6 +15,25 @@ export default function SettingsHardwareRoute() { const [send] = useJsonRpc(); const settings = useSettingsStore(); + const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation); + + const handleDisplayRotationChange = (rotation: string) => { + setDisplayRotation(rotation); + handleDisplayRotationSave(); + }; + + const handleDisplayRotationSave = () => { + send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set display orientation: ${resp.error.data || "Unknown error"}`, + ); + return; + } + notifications.success("Display orientation updated successfully"); + }); + }; + const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings); const handleBacklightSettingsChange = (settings: BacklightSettings) => { @@ -59,6 +78,24 @@ export default function SettingsHardwareRoute() { description="Configure display settings and hardware options for your JetKVM device" />
    + + { + settings.displayRotation = e.target.value; + handleDisplayRotationChange(settings.displayRotation); + }} + /> + Date: Sun, 11 May 2025 11:19:07 -0400 Subject: [PATCH 047/165] fix: absolute mouse scroll (#434) Co-authored-by: wup-one --- internal/usbgadget/hid_mouse_absolute.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index de77b1e..6629caa 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -55,6 +55,8 @@ var absoluteMouseCombinedReportDesc = []byte{ 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) + 0x35, 0x00, // Physical Minimum (0) = Reset Physical Minimum + 0x45, 0x00, // Physical Maximum (0) = Reset Physical Maximum 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x06, // Input (Data, Var, Rel) From 77b4c1c531b54e3c9466ff12191ff28c046467df Mon Sep 17 00:00:00 2001 From: Qishuai Liu Date: Sun, 11 May 2025 23:19:22 +0800 Subject: [PATCH 048/165] ntp: fix panic on NTP query error and add IPv6 server for IPv6-only support (#424) * fix(ntp): prevent panic on NTP query error and add IPv6 server in defaultNTPServers * fix(ntp): make sure queryMultipleNTP finish if all servers failed --- internal/timesync/ntp.go | 53 +++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go index 41656b7..d45112c 100644 --- a/internal/timesync/ntp.go +++ b/internal/timesync/ntp.go @@ -13,7 +13,8 @@ var defaultNTPServers = []string{ "time.aws.com", "time.windows.com", "time.google.com", - "162.159.200.123", // time.cloudflare.com + "162.159.200.123", // time.cloudflare.com IPv4 + "2606:4700:f1::123", // time.cloudflare.com IPv6 "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", @@ -57,6 +58,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no // query the server now, response, err := queryNtpServer(server, timeout) + if err != nil { + scopedLogger.Warn(). + Str("error", err.Error()). + Msg("failed to query NTP server") + results <- nil + return + } // set the last RTT metricNtpServerLastRTT.WithLabelValues( @@ -76,32 +84,33 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no strconv.Itoa(int(response.Precision)), ).Set(1) - if err == nil { - // increase success count - metricNtpTotalSuccessCount.Inc() - metricNtpSuccessCount.WithLabelValues(server).Inc() + // increase success count + metricNtpTotalSuccessCount.Inc() + metricNtpSuccessCount.WithLabelValues(server).Inc() - scopedLogger.Info(). - Str("time", now.Format(time.RFC3339)). - Str("reference", response.ReferenceString()). - Str("rtt", response.RTT.String()). - Str("clockOffset", response.ClockOffset.String()). - Uint8("stratum", response.Stratum). - Msg("NTP server returned time") - results <- &ntpResult{ - now: now, - offset: &response.ClockOffset, - } - } else { - scopedLogger.Warn(). - Str("error", err.Error()). - Msg("failed to query NTP server") + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Str("reference", response.ReferenceString()). + Str("rtt", response.RTT.String()). + Str("clockOffset", response.ClockOffset.String()). + Uint8("stratum", response.Stratum). + Msg("NTP server returned time") + results <- &ntpResult{ + now: now, + offset: &response.ClockOffset, } }(server) } - result := <-results - return result.now, result.offset + for range servers { + result := <-results + if result == nil { + continue + } + now, offset = result.now, result.offset + return + } + return } func queryNtpServer(server string, timeout time.Duration) (now *time.Time, response *ntp.Response, err error) { From d0faf03239332ab3545c1c9d64dff8035f6997b7 Mon Sep 17 00:00:00 2001 From: Daniel Lorch Date: Mon, 12 May 2025 18:59:32 +0200 Subject: [PATCH 049/165] Fix: Alt Gr not recognized (#399) * Fix: Alt-Gr not recognized * Proper fix for Alt-Gr not being recognized * Add comment on codes and modifiers * Add comment on paste box * Remove comment * Improve description * Wording... * Formatting... * Improve description again --- ui/src/components/WebRTCVideo.tsx | 24 ++++++++++++++++++++++-- ui/src/keyboardMappings.ts | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index b73135b..3cdb2e9 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -330,11 +330,31 @@ export default function WebRTCVideo() { ) // Alt: Keep if Alt is pressed or if the key isn't an Alt key // Example: If altKey is true, keep all modifiers - // If altKey is false, filter out 0x04 (AltLeft) and 0x40 (AltRight) + // If altKey is false, filter out 0x04 (AltLeft) + // + // But intentionally do not filter out 0x40 (AltRight) to accomodate + // Alt Gr (Alt Graph) as a modifier. Oddly, Alt Gr does not declare + // itself to be an altKey. For example, the KeyboardEvent for + // Alt Gr + 2 has the following structure: + // - altKey: false + // - code: "Digit2" + // - type: [ "keydown" | "keyup" ] + // + // For context, filteredModifiers aims to keep track which modifiers + // are being pressed on the physical keyboard at any point in time. + // There is logic in the keyUpHandler and keyDownHandler to add and + // remove 0x40 (AltRight) from the list of new modifiers. + // + // But relying on the two handlers alone to track the state of the + // modifier bears the risk that the key up event for Alt Gr could + // get lost while the browser window is temporarily out of focus, + // which means the Alt Gr key state would then be "stuck". At this + // point, we would need to rely on the user to press Alt Gr again + // to properly release the state of that modifier. .filter( modifier => altKey || - (modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]), + (modifier !== modifiers["AltLeft"]), ) // Meta: Keep if Meta is pressed or if the key isn't a Meta key // Example: If metaKey is true, keep all modifiers diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 347939a..79ed11a 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -1,6 +1,6 @@ +// Key codes and modifiers correspond to definitions in the +// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt) export const keys = { - AltLeft: 0xe2, - AltRight: 0xe6, ArrowDown: 0x51, ArrowLeft: 0x50, ArrowRight: 0x4f, From 8ee0532f0e18622de492b42395e65681cd7b2c56 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 12 May 2025 12:00:49 -0500 Subject: [PATCH 050/165] Update npm packages for the UI (#432) Upgraded most packages to current as of 2025-05-09 for almost everything. Remove the erroneous extra dependency to old xterm package since the correct @xterm/xterm package was already included (suspect a bad merge) and it was causing issues with react 19.1. Switched to using the hooks exposed in the usehooks-ts package (this package was already referenced, suspect a bad merge) removing our private copies of useInterval, useIsMounted, useResizeObserver which are identical. Added import of JSX from react now needed because NPX is not in global scope in react 19.x. Explicitly cast the ref of included elements due to change in react 19.x --- ui/package-lock.json | 2613 ++++++++++++++---------- ui/package.json | 59 +- ui/src/components/Button.tsx | 2 +- ui/src/components/Checkbox.tsx | 2 +- ui/src/components/InputField.tsx | 2 +- ui/src/components/SelectMenuBasic.tsx | 2 +- ui/src/components/Terminal.tsx | 34 +- ui/src/components/TextArea.tsx | 2 +- ui/src/components/WebRTCVideo.tsx | 4 +- ui/src/hooks/useInterval.ts | 21 - ui/src/hooks/useIsMounted.ts | 26 - ui/src/hooks/useResizeObserver.ts | 131 -- ui/src/routes/devices.$id.settings.tsx | 4 +- ui/src/routes/devices.tsx | 2 +- 14 files changed, 1631 insertions(+), 1273 deletions(-) delete mode 100644 ui/src/hooks/useInterval.ts delete mode 100644 ui/src/hooks/useIsMounted.ts delete mode 100644 ui/src/hooks/useResizeObserver.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index 9e77e10..d3c964b 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,8 +8,8 @@ "name": "kvm-ui", "version": "0.0.0", "dependencies": { - "@headlessui/react": "^2.2.0", - "@headlessui/tailwindcss": "^0.2.1", + "@headlessui/react": "^2.2.2", + "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", "@vitejs/plugin-basic-ssl": "^1.2.0", "@xterm/addon-clipboard": "^0.1.0", @@ -25,44 +25,43 @@ "framer-motion": "^11.15.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", - "react": "^18.2.0", + "react": "^19.1.0", "react-animate-height": "^3.2.3", - "react-dom": "^18.2.0", - "react-hot-toast": "^2.4.1", - "react-icons": "^5.4.0", + "react-dom": "^19.1.0", + "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.7.112", + "react-simple-keyboard": "^3.8.71", "react-use-websocket": "^4.13.0", - "react-xtermjs": "^1.0.9", - "recharts": "^2.15.0", + "react-xtermjs": "^1.0.10", + "recharts": "^2.15.3", "tailwind-merge": "^2.5.5", - "usehooks-ts": "^3.1.0", - "validator": "^13.12.0", - "xterm": "^5.3.0", + "usehooks-ts": "^3.1.1", + "validator": "^13.15.0", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/typography": "^0.5.15", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.3.0", - "@types/semver": "^7.5.8", - "@types/validator": "^13.12.2", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "@vitejs/plugin-react-swc": "^3.7.2", - "autoprefixer": "^10.4.20", - "eslint": "^8.20.0", - "eslint-config-prettier": "^10.0.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", + "@types/semver": "^7.7.0", + "@types/validator": "^13.15.0", + "@typescript-eslint/eslint-plugin": "^8.32.0", + "@typescript-eslint/parser": "^8.32.0", + "@vitejs/plugin-react-swc": "^3.9.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.26.0", + "eslint-config-prettier": "^10.1.5", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "postcss": "^8.4.49", - "prettier": "^3.4.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "postcss": "^8.5.3", + "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", + "typescript": "^5.8.3", "vite": "^5.2.0", "vite-tsconfig-paths": "^5.1.4" }, @@ -70,14 +69,6 @@ "node": "21.1.0" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -90,20 +81,17 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -116,9 +104,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -131,9 +119,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -146,9 +134,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -161,9 +149,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -176,9 +164,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -191,9 +179,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -206,9 +194,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -221,9 +209,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -236,9 +224,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -251,9 +239,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -266,9 +254,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -281,9 +269,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -296,9 +284,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -311,9 +299,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -326,9 +314,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -341,9 +329,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -356,9 +344,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -371,9 +359,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -386,9 +374,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -401,9 +389,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -416,9 +404,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -431,9 +419,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -446,36 +434,91 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -483,7 +526,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -510,37 +553,57 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", - "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", "dependencies": { - "@floating-ui/utils": "^0.2.7" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.10", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", - "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.7" + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/react": { - "version": "0.26.23", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.23.tgz", - "integrity": "sha512-9u3i62fV0CFF3nIegiWiRDwOs7OW/KhSUJDNx2MkQM3LbE5zQOY01sL3nelcVBXvX7Ovvo3A49I8ql+20Wg/Hw==", + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", "dependencies": { - "@floating-ui/react-dom": "^2.1.1", - "@floating-ui/utils": "^0.2.7", + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -549,9 +612,9 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "dependencies": { "@floating-ui/dom": "^1.0.0" }, @@ -561,19 +624,20 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", - "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, "node_modules/@headlessui/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", - "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.2.tgz", + "integrity": "sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA==", "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.17.1", "@react-aria/interactions": "^3.21.3", - "@tanstack/react-virtual": "^3.8.1" + "@tanstack/react-virtual": "^3.13.6", + "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=10" @@ -602,37 +666,36 @@ "react": ">= 16 || ^19.0.0-rc" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": "*" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { @@ -647,10 +710,17 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -668,35 +738,10 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -723,9 +768,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -736,6 +781,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", + "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -778,38 +843,41 @@ } }, "node_modules/@react-aria/focus": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.2.tgz", - "integrity": "sha512-Jc/IY+StjA3uqN73o6txKQ527RFU7gnG5crEl5Xy3V+gbYp2O5L3ezAo/E0Ipi2cyMbG6T5Iit1IDs7hcGu8aw==", + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", + "integrity": "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==", "dependencies": { - "@react-aria/interactions": "^3.22.2", - "@react-aria/utils": "^3.25.2", - "@react-types/shared": "^3.24.1", + "@react-aria/interactions": "^3.25.0", + "@react-aria/utils": "^3.28.2", + "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/interactions": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.2.tgz", - "integrity": "sha512-xE/77fRVSlqHp2sfkrMeNLrqf2amF/RyuAS6T5oDJemRSgYM3UoxTbWjucPhfnoW7r32pFPHHgz4lbdX8xqD/g==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.0.tgz", + "integrity": "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==", "dependencies": { - "@react-aria/ssr": "^3.9.5", - "@react-aria/utils": "^3.25.2", - "@react-types/shared": "^3.24.1", + "@react-aria/ssr": "^3.9.8", + "@react-aria/utils": "^3.28.2", + "@react-stately/flags": "^3.1.1", + "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/ssr": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz", - "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==", + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", + "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", "dependencies": { "@swc/helpers": "^0.5.0" }, @@ -817,55 +885,65 @@ "node": ">= 12" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/utils": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.2.tgz", - "integrity": "sha512-GdIvG8GBJJZygB4L2QJP1Gabyn2mjFsha73I2wSe+o4DYeGWoJiMZRM06PyTIxLH4S7Sn7eVDtsSBfkc2VY/NA==", + "version": "3.28.2", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.2.tgz", + "integrity": "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==", "dependencies": { - "@react-aria/ssr": "^3.9.5", - "@react-stately/utils": "^3.10.3", - "@react-types/shared": "^3.24.1", + "@react-aria/ssr": "^3.9.8", + "@react-stately/flags": "^3.1.1", + "@react-stately/utils": "^3.10.6", + "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", + "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", + "dependencies": { + "@swc/helpers": "^0.5.0" } }, "node_modules/@react-stately/utils": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.3.tgz", - "integrity": "sha512-moClv7MlVSHpbYtQIkm0Cx+on8Pgt1XqtPx6fy9rQFb2DNc9u1G3AUVnqA17buOkH1vLxAtX4MedlxMWyRCYYA==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", + "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-types/shared": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.24.1.tgz", - "integrity": "sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==", + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.0.tgz", + "integrity": "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "engines": { "node": ">=14.0.0" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz", - "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", "cpu": [ "arm" ], @@ -875,9 +953,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz", - "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", "cpu": [ "arm64" ], @@ -887,9 +965,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz", - "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", "cpu": [ "arm64" ], @@ -899,9 +977,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz", - "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", "cpu": [ "x64" ], @@ -910,10 +988,46 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz", - "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", "cpu": [ "arm" ], @@ -923,9 +1037,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz", - "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", "cpu": [ "arm64" ], @@ -935,9 +1049,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz", - "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", "cpu": [ "arm64" ], @@ -946,12 +1060,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz", - "integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==", + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", "cpu": [ - "ppc64le" + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" ], "optional": true, "os": [ @@ -959,9 +1085,21 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz", - "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", "cpu": [ "riscv64" ], @@ -971,9 +1109,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz", - "integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", "cpu": [ "s390x" ], @@ -983,9 +1121,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz", - "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", "cpu": [ "x64" ], @@ -995,9 +1133,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz", - "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", "cpu": [ "x64" ], @@ -1007,9 +1145,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz", - "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", "cpu": [ "arm64" ], @@ -1019,9 +1157,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz", - "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", "cpu": [ "ia32" ], @@ -1031,9 +1169,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz", - "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", "cpu": [ "x64" ], @@ -1048,14 +1186,14 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" }, "node_modules/@swc/core": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.4.tgz", - "integrity": "sha512-EHl6eNod/914xDRK4nu7gr78riK2cfi4DkAMvJt6COdaNGOnbR5eKrLe3SnRizyzzrPcxUMhflDL5hrcXS8rAQ==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", "dev": true, "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.19" + "@swc/types": "^0.1.21" }, "engines": { "node": ">=10" @@ -1065,19 +1203,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.4", - "@swc/core-darwin-x64": "1.11.4", - "@swc/core-linux-arm-gnueabihf": "1.11.4", - "@swc/core-linux-arm64-gnu": "1.11.4", - "@swc/core-linux-arm64-musl": "1.11.4", - "@swc/core-linux-x64-gnu": "1.11.4", - "@swc/core-linux-x64-musl": "1.11.4", - "@swc/core-win32-arm64-msvc": "1.11.4", - "@swc/core-win32-ia32-msvc": "1.11.4", - "@swc/core-win32-x64-msvc": "1.11.4" + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" }, "peerDependencies": { - "@swc/helpers": "*" + "@swc/helpers": ">=0.5.17" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1086,9 +1224,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.4.tgz", - "integrity": "sha512-Oi4lt4wqjpp80pcCh+vzvpsESJ8XXozYCE5EM/dDpr+9m2oRpkseds7Gq4ulzgdbUDPo1jJ1PonjjrKpfKY+sQ==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", "cpu": [ "arm64" ], @@ -1102,9 +1240,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.4.tgz", - "integrity": "sha512-Tb7ez94DXxhX5iJ5slnAlT2gwJinQk3pMnQ46Npi6adKr3ZXM5Bdk0jpRUp8XjEcgNXkQRV1DtrySgCz6YlEnQ==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", "cpu": [ "x64" ], @@ -1118,9 +1256,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.4.tgz", - "integrity": "sha512-p1uV+6Mi+0M+1kL7qL206ZaohomYMW7yroXSLDTJXbIylx7wG2xrUQL6AFtz2DwqDoX/E8jMNBjp+GcEy8r8Ig==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", "cpu": [ "arm" ], @@ -1134,9 +1272,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.4.tgz", - "integrity": "sha512-4ijX4bWf9oc7kWkT6xUhugVGzEJ7U9c7CHNmt/xhI/yWsQdfM11+HECqWh7ay3m+aaEoVdvTeU5gykeF5jSxDA==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", "cpu": [ "arm64" ], @@ -1150,9 +1288,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.4.tgz", - "integrity": "sha512-XI+gOgcuSanejbAC5QXKTjNA3GUJi7bzHmeJbNhKpX9d349RdVwan0k9okHmhMBY7BywAg3LK0ovF9PmOLgMHg==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", "cpu": [ "arm64" ], @@ -1166,9 +1304,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.4.tgz", - "integrity": "sha512-wyD6noaCPFayKOvl9mTxuiQoEULAagGuO0od2VkW7h4HvlgpOAZNekZYX73WEP/b+WuePNHurZ9KGpom43IzmA==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", "cpu": [ "x64" ], @@ -1182,9 +1320,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.4.tgz", - "integrity": "sha512-e2vG9gUF1BRX0BWqSEHop6u14l5BtV3VS2Pmr+oquc0Ycs/zj81xhYc3ML4ByK5OxDkAaKBWryAOKTLaJA/DVg==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", "cpu": [ "x64" ], @@ -1198,9 +1336,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.4.tgz", - "integrity": "sha512-rm51iljNqjCA/41gxYameuyjX1ENaTlvdxmaoPPYeUDt6hfypG93IxMJJCewaeHN9XfNxqZU7d4cupNqk+8nng==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", "cpu": [ "arm64" ], @@ -1214,9 +1352,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.4.tgz", - "integrity": "sha512-PHy3N6zlyU8te7Umi0ggXNbcx2VUkwpE59PW9FQQy9MBZM1Qn+OEGnO/4KLWjGFABw+9CwIeaRYgq6uCi1ry6A==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", "cpu": [ "ia32" ], @@ -1230,9 +1368,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.4.tgz", - "integrity": "sha512-0TiriDGl7Dr4ObfMBk07PS4Ql5hgQH0QnU3E8I+fbs45hqfwC5OrN47HOsXx4ZbEw8XYxp2NM8SGnVoTIm4J8w==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", "cpu": [ "x64" ], @@ -1252,17 +1390,17 @@ "dev": true }, "node_modules/@swc/helpers": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", - "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@swc/types": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", - "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", "dev": true, "dependencies": { "@swc/counter": "^0.1.3" @@ -1295,39 +1433,26 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, - "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@tanstack/react-virtual": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.7.tgz", - "integrity": "sha512-yeP+M0G8D+15ZFPivpuQ5hoM4Fa/PzERBx8P8EGcfEsXX3JOb9G9UUrqc47ZXAxvK+YqzM9T5qlJUYUFOwCZJw==", + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.8.tgz", + "integrity": "sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==", "dependencies": { - "@tanstack/virtual-core": "3.10.7" + "@tanstack/virtual-core": "3.13.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@tanstack/virtual-core": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.7.tgz", - "integrity": "sha512-ND5dfsU0n9F4gROzwNNDJmg6y8n9pI8YWxtgbfJ5UcNn7Hx+MxEXtXcQ189tS7sh8pmCObgz2qSiyRKTZxT4dg==", + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.8.tgz", + "integrity": "sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -1357,30 +1482,30 @@ } }, "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" }, "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" }, "node_modules/@types/d3-timer": { "version": "3.0.2", @@ -1388,67 +1513,65 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true - }, "node_modules/@types/react": { - "version": "18.2.74", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", - "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", + "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", "devOptional": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", + "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", "dev": true, - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", "dev": true }, "node_modules/@types/validator": { - "version": "13.12.2", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", - "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", - "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", + "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/type-utils": "8.28.0", - "@typescript-eslint/utils": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/type-utils": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1464,15 +1587,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", - "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", + "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "debug": "^4.3.4" }, "engines": { @@ -1488,13 +1611,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", - "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0" + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1505,15 +1628,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", - "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", + "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/utils": "8.32.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1528,9 +1651,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", - "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1541,19 +1664,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", - "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1566,31 +1689,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1605,12 +1713,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", - "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1633,11 +1741,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", @@ -1650,12 +1753,12 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", - "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", "dev": true, "dependencies": { - "@swc/core": "^1.10.15" + "@swc/core": "^1.11.21" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" @@ -1709,10 +1812,22 @@ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "bin": { "acorn": "bin/acorn" }, @@ -1744,11 +1859,14 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -1867,14 +1985,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1945,9 +2063,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dev": true, "funding": [ { @@ -1964,11 +2082,11 @@ } ], "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2011,6 +2129,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2031,9 +2168,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -2050,10 +2187,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -2062,6 +2199,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2123,9 +2268,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", "dev": true, "funding": [ { @@ -2192,9 +2337,9 @@ } }, "node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -2228,10 +2373,57 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2258,17 +2450,17 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cva": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.1.tgz", - "integrity": "sha512-gznFqTgERU9q4wg7jfgqtt34+RUt9S5t0xDAAEuDwQEAXEgjdDkKXpLLNjwSxsB4Ln/sqWJEH7yhE8Ny0mxd0w==", + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.3.tgz", + "integrity": "sha512-CZa8pTkpEygxJRLH9aod/wfnSgK5z/0GJqG/NNehlwam+S8llqCWUXS3eCenvAiW5sTUpwTWE6bJaeeZ/b4pzA==", "dependencies": { - "clsx": "2.0.0" + "clsx": "^2.1.1" }, "funding": { - "url": "https://joebell.co.uk/sponsors" + "url": "https://polar.sh/cva" }, "peerDependencies": { - "typescript": ">= 4.5.5 < 6" + "typescript": ">= 4.5.5" }, "peerDependenciesMeta": { "typescript": { @@ -2440,11 +2632,11 @@ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2497,6 +2689,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2508,14 +2708,14 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dependencies": { "esutils": "^2.0.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, "node_modules/dom-helpers": { @@ -2545,10 +2745,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "node_modules/electron-to-chromium": { - "version": "1.5.75", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", - "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", + "version": "1.5.151", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", + "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", "dev": true }, "node_modules/emoji-regex": { @@ -2556,6 +2761,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -2716,9 +2929,9 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2727,29 +2940,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -2761,6 +2974,11 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2773,67 +2991,77 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "zod": "^3.24.2" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", - "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } @@ -2867,25 +3095,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-import-resolver-node/node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-module-utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", @@ -2959,17 +3168,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-plugin-import/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2990,9 +3188,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "dependencies": { "array-includes": "^3.1.8", @@ -3005,7 +3203,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -3034,9 +3232,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", - "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", "dev": true, "peerDependencies": { "eslint": ">=8.40" @@ -3052,18 +3250,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-plugin-react/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3076,6 +3262,23 @@ "node": "*" } }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3086,15 +3289,15 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3120,6 +3323,17 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3132,25 +3346,36 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dependencies": { "estraverse": "^5.1.0" }, @@ -3185,11 +3410,93 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3204,15 +3511,15 @@ } }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3240,22 +3547,22 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -3269,6 +3576,22 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3285,37 +3608,36 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" }, "node_modules/focus-trap": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", - "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", + "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", "dependencies": { "tabbable": "^6.2.0" } }, "node_modules/focus-trap-react": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.3.tgz", - "integrity": "sha512-YXBpFu/hIeSu6NnmV2xlXzOYxuWkoOtar9jzgp3lOmjWLWY59C/b8DtDHEAV4SPU07Nd/t+nS/SBNGkhUBFmEw==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.3.1.tgz", + "integrity": "sha512-PN4Ya9xf9nyj/Nd9VxBNMuD7IrlRbmaG6POAQ8VLqgtc6IY/Ln1tYakow+UIq4fihYYYFM70/2oyidE6bbiPgw==", "dependencies": { - "focus-trap": "^7.5.4", + "focus-trap": "^7.6.1", "tabbable": "^6.2.0" }, "peerDependencies": { @@ -3339,11 +3661,11 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -3353,6 +3675,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3367,12 +3697,12 @@ } }, "node_modules/framer-motion": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", - "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "dependencies": { - "motion-dom": "^11.14.3", - "motion-utils": "^11.14.3", + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { @@ -3392,10 +3722,13 @@ } } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -3497,19 +3830,19 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3526,35 +3859,12 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3603,7 +3913,8 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/has-bigints": { "version": "1.1.0", @@ -3685,18 +3996,44 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3716,15 +4053,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3751,6 +4079,14 @@ "node": ">=12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3973,13 +4309,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "node_modules/is-regex": { "version": "1.2.1", @@ -4136,15 +4469,12 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -4315,6 +4645,11 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4323,6 +4658,25 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4343,6 +4697,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -4352,9 +4725,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4374,27 +4747,30 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/motion-dom": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", - "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==" + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "dependencies": { + "motion-utils": "^11.18.1" + } }, "node_modules/motion-utils": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", - "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==" + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -4407,9 +4783,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -4428,6 +4804,14 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -4506,14 +4890,15 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -4566,6 +4951,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4575,16 +4971,16 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -4634,6 +5030,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4645,6 +5046,14 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4653,14 +5062,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4675,26 +5076,26 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "engines": { - "node": "14 || >=16.14" + "node": ">=16" } }, "node_modules/picocolors": { @@ -4722,13 +5123,21 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "engines": { "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -4780,22 +5189,6 @@ "postcss": "^8.0.0" } }, - "node_modules/postcss-import/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/postcss-js": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", @@ -4872,7 +5265,7 @@ "postcss": "^8.2.14" } }, - "node_modules/postcss-selector-parser": { + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", @@ -4884,6 +5277,19 @@ "node": ">=4" } }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -4898,9 +5304,9 @@ } }, "node_modules/prettier": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", - "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -5000,6 +5406,18 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5008,6 +5426,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5027,13 +5459,32 @@ } ] }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dependencies": { - "loose-envify": "^1.1.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "engines": { "node": ">=0.10.0" } @@ -5051,15 +5502,14 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^19.1.0" } }, "node_modules/react-hot-toast": { @@ -5092,11 +5542,11 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", "dependencies": { - "@remix-run/router": "1.15.3" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -5106,12 +5556,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" }, "engines": { "node": ">=14.0.0" @@ -5122,12 +5572,12 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.7.112", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.7.112.tgz", - "integrity": "sha512-wpSgxQ6UHQcHqcC0eQgD7mZ91kpjGYj2mT+Ir1Be5jxIMxz1rQOtXLYpsQcNvYXCJyrVqntYPOPCPnpVxsxPvA==", + "version": "3.8.71", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.71.tgz", + "integrity": "sha512-U0f+bRe0wuuzp1gsuHPjjYWv3ZywSr7wDYyBv12d4w5FDXmib7FfyefzHP+yRuW2hH444OpfJvlv4y70+8tEqw==", "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-smooth": { @@ -5165,9 +5615,9 @@ "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==" }, "node_modules/react-xtermjs": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz", - "integrity": "sha512-lrK1xiWfgxAC+4shtMHh0Irxg2t5t7JbTtpP0W7GIf1gQ9SHW/djmyiLpQSA75mN1DpT0bKeqj1fOKd0XX8RBA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.10.tgz", + "integrity": "sha512-+xpKEKbmsypWzRKE0FR1LNIGcI8gx+R6VMHe8IQW7iTbgeqp3Qg7SbiVNOzR+Ovb1QK4DPA3KqsIQV+XP0iRUA==", "peerDependencies": { "@xterm/xterm": "^5.5.0" } @@ -5192,9 +5642,9 @@ } }, "node_modules/recharts": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", - "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -5247,11 +5697,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -5272,18 +5717,20 @@ } }, "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5297,34 +5744,20 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", - "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -5334,24 +5767,44 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.1", - "@rollup/rollup-android-arm64": "4.14.1", - "@rollup/rollup-darwin-arm64": "4.14.1", - "@rollup/rollup-darwin-x64": "4.14.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.1", - "@rollup/rollup-linux-arm64-gnu": "4.14.1", - "@rollup/rollup-linux-arm64-musl": "4.14.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1", - "@rollup/rollup-linux-riscv64-gnu": "4.14.1", - "@rollup/rollup-linux-s390x-gnu": "4.14.1", - "@rollup/rollup-linux-x64-gnu": "4.14.1", - "@rollup/rollup-linux-x64-musl": "4.14.1", - "@rollup/rollup-win32-arm64-msvc": "4.14.1", - "@rollup/rollup-win32-ia32-msvc": "4.14.1", - "@rollup/rollup-win32-x64-msvc": "4.14.1", + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5392,6 +5845,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5423,13 +5895,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, "node_modules/semver": { "version": "7.7.1", @@ -5443,6 +5917,41 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5486,6 +5995,11 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5592,6 +6106,14 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5622,34 +6144,28 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/string-width/node_modules/ansi-regex": { + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/string.prototype.matchall": { @@ -5743,14 +6259,17 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi-cjs": { @@ -5765,6 +6284,14 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5805,27 +6332,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5854,9 +6360,9 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tailwind-merge": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", - "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -5898,27 +6404,18 @@ "node": ">=14.0.0" } }, - "node_modules/tailwindcss/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4" } }, - "node_modules/text-table": { - "version": "0.2.0", - "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", @@ -5954,6 +6451,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5972,9 +6477,9 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tsconfck": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", - "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", + "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", "dev": true, "bin": { "tsconfck": "bin/tsconfck.js" @@ -6003,9 +6508,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/type-check": { "version": "0.4.0", @@ -6018,15 +6523,17 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" } }, "node_modules/typed-array-buffer": { @@ -6100,9 +6607,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -6129,10 +6636,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -6150,7 +6665,7 @@ ], "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6168,11 +6683,11 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/usehooks-ts": { @@ -6195,13 +6710,21 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", "engines": { "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -6224,13 +6747,13 @@ } }, "node_modules/vite": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", - "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -6249,6 +6772,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -6266,6 +6790,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -6391,6 +6918,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -6424,6 +6959,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -6442,15 +6985,15 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=8" } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { @@ -6464,35 +7007,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/xterm": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", - "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", - "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." - }, "node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "bin": { "yaml": "bin.mjs" }, @@ -6511,12 +7034,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/zustand": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", - "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", "dependencies": { - "use-sync-external-store": "1.2.0" + "use-sync-external-store": "^1.2.2" }, "engines": { "node": ">=12.7.0" diff --git a/ui/package.json b/ui/package.json index 4dab092..ab2e141 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,8 +19,8 @@ "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.2.0", - "@headlessui/tailwindcss": "^0.2.1", + "@headlessui/react": "^2.2.2", + "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", "@vitejs/plugin-basic-ssl": "^1.2.0", "@xterm/addon-clipboard": "^0.1.0", @@ -36,44 +36,43 @@ "framer-motion": "^11.15.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", - "react": "^18.2.0", + "react": "^19.1.0", "react-animate-height": "^3.2.3", - "react-dom": "^18.2.0", - "react-hot-toast": "^2.4.1", - "react-icons": "^5.4.0", + "react-dom": "^19.1.0", + "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.7.112", + "react-simple-keyboard": "^3.8.71", "react-use-websocket": "^4.13.0", - "react-xtermjs": "^1.0.9", - "recharts": "^2.15.0", + "react-xtermjs": "^1.0.10", + "recharts": "^2.15.3", "tailwind-merge": "^2.5.5", - "usehooks-ts": "^3.1.0", - "validator": "^13.12.0", - "xterm": "^5.3.0", + "usehooks-ts": "^3.1.1", + "validator": "^13.15.0", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/typography": "^0.5.15", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.3.0", - "@types/semver": "^7.5.8", - "@types/validator": "^13.12.2", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "@vitejs/plugin-react-swc": "^3.7.2", - "autoprefixer": "^10.4.20", - "eslint": "^8.20.0", - "eslint-config-prettier": "^10.0.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", + "@types/semver": "^7.7.0", + "@types/validator": "^13.15.0", + "@typescript-eslint/eslint-plugin": "^8.32.0", + "@typescript-eslint/parser": "^8.32.0", + "@vitejs/plugin-react-swc": "^3.9.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.26.0", + "eslint-config-prettier": "^10.1.5", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "postcss": "^8.4.49", - "prettier": "^3.4.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "postcss": "^8.5.3", + "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", + "typescript": "^5.8.3", "vite": "^5.2.0", "vite-tsconfig-paths": "^5.1.4" } diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 3b7ac95..6085ec7 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { JSX } from "react"; import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom"; import ExtLink from "@/components/ExtLink"; diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/Checkbox.tsx index e3237b1..4c81d86 100644 --- a/ui/src/components/Checkbox.tsx +++ b/ui/src/components/Checkbox.tsx @@ -1,5 +1,5 @@ import type { Ref } from "react"; -import React, { forwardRef } from "react"; +import React, { forwardRef, JSX } from "react"; import clsx from "clsx"; import FieldLabel from "@/components/FieldLabel"; diff --git a/ui/src/components/InputField.tsx b/ui/src/components/InputField.tsx index 2f580a0..1a788ef 100644 --- a/ui/src/components/InputField.tsx +++ b/ui/src/components/InputField.tsx @@ -1,5 +1,5 @@ import type { Ref } from "react"; -import React, { forwardRef } from "react"; +import React, { forwardRef, JSX } from "react"; import clsx from "clsx"; import FieldLabel from "@/components/FieldLabel"; diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index c518bfe..acc8827 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { JSX } from "react"; import clsx from "clsx"; import FieldLabel from "@/components/FieldLabel"; diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx index 7e09c6f..6a2c253 100644 --- a/ui/src/components/Terminal.tsx +++ b/ui/src/components/Terminal.tsx @@ -79,10 +79,11 @@ function Terminal({ return () => { setDisableKeyboardFocusTrap(false); }; - }, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]); + }, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]); const readyState = dataChannel.readyState; useEffect(() => { + if (!instance) return; if (readyState !== "open") return; const abortController = new AbortController(); @@ -93,11 +94,10 @@ function Terminal({ // Handle binary data differently based on browser implementation // Firefox sends data as blobs, chrome sends data as arraybuffer if (binaryType === "arraybuffer") { - instance?.write(new Uint8Array(e.data)); + instance.write(new Uint8Array(e.data)); } else if (binaryType === "blob") { const reader = new FileReader(); reader.onload = () => { - if (!instance) return; if (!reader.result) return; instance.write(new Uint8Array(reader.result as ArrayBuffer)); }; @@ -107,12 +107,12 @@ function Terminal({ { signal: abortController.signal }, ); - const onDataHandler = instance?.onData(data => { + const onDataHandler = instance.onData(data => { dataChannel.send(data); }); // Setup escape key handler - const onKeyHandler = instance?.onKey(e => { + const onKeyHandler = instance.onKey(e => { const { domEvent } = e; if (domEvent.key === "Escape") { setTerminalType("none"); @@ -123,32 +123,32 @@ function Terminal({ // Send initial terminal size if (dataChannel.readyState === "open") { - dataChannel.send(JSON.stringify({ rows: instance?.rows, cols: instance?.cols })); + dataChannel.send(JSON.stringify({ rows: instance.rows, cols: instance.cols })); } return () => { abortController.abort(); - onDataHandler?.dispose(); - onKeyHandler?.dispose(); + onDataHandler.dispose(); + onKeyHandler.dispose(); }; - }, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]); + }, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]); useEffect(() => { if (!instance) return; // Load the fit addon const fitAddon = new FitAddon(); - instance?.loadAddon(fitAddon); + instance.loadAddon(fitAddon); - instance?.loadAddon(new ClipboardAddon()); - instance?.loadAddon(new Unicode11Addon()); - instance?.loadAddon(new WebLinksAddon()); + instance.loadAddon(new ClipboardAddon()); + instance.loadAddon(new Unicode11Addon()); + instance.loadAddon(new WebLinksAddon()); instance.unicode.activeVersion = "11"; if (isWebGl2Supported) { const webGl2Addon = new WebglAddon(); webGl2Addon.onContextLoss(() => webGl2Addon.dispose()); - instance?.loadAddon(webGl2Addon); + instance.loadAddon(webGl2Addon); } const handleResize = () => fitAddon.fit(); @@ -158,13 +158,11 @@ function Terminal({ return () => { window.removeEventListener("resize", handleResize); }; - }, [ref, instance, dataChannel]); + }, [ref, instance]); return (
    { - e.stopPropagation(); - }} + onKeyDown={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()} >
    diff --git a/ui/src/components/TextArea.tsx b/ui/src/components/TextArea.tsx index 51747a3..f3657cf 100644 --- a/ui/src/components/TextArea.tsx +++ b/ui/src/components/TextArea.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { JSX } from "react"; import clsx from "clsx"; import FieldLabel from "@/components/FieldLabel"; diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 3cdb2e9..8ebe257 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -10,7 +10,7 @@ import { useVideoStore, } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; -import { useResizeObserver } from "@/hooks/useResizeObserver"; +import { useResizeObserver } from "usehooks-ts"; import { cx } from "@/cva.config"; import VirtualKeyboard from "@components/VirtualKeyboard"; import Actionbar from "@components/ActionBar"; @@ -67,7 +67,7 @@ export default function WebRTCVideo() { // Video-related useResizeObserver({ - ref: videoElm, + ref: videoElm as React.RefObject, onResize: ({ width, height }) => { // This is actually client size, not videoSize if (width && height) { diff --git a/ui/src/hooks/useInterval.ts b/ui/src/hooks/useInterval.ts deleted file mode 100644 index 34acbb4..0000000 --- a/ui/src/hooks/useInterval.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useRef } from "react"; - -export default function useInterval(callback: () => void, delay: number) { - const savedCallback = useRef(); - - // Save the callback directly in the useRef object - savedCallback.current = callback; - - // Set up the interval. - useEffect(() => { - function tick() { - if (!savedCallback.current) return; - savedCallback.current(); - } - - if (delay !== null) { - const id = setInterval(tick, delay); - return () => clearInterval(id); - } - }, [delay]); -} diff --git a/ui/src/hooks/useIsMounted.ts b/ui/src/hooks/useIsMounted.ts deleted file mode 100644 index c355cbf..0000000 --- a/ui/src/hooks/useIsMounted.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useCallback, useEffect, useRef } from "react"; - -/** - * Custom hook that determines if the component is currently mounted. - * @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted. - * @public - * @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted) - * @example - * ```tsx - * const isComponentMounted = useIsMounted(); - * // Use isComponentMounted() to check if the component is currently mounted before performing certain actions. - * ``` - */ -export function useIsMounted(): () => boolean { - const isMounted = useRef(false); - - useEffect(() => { - isMounted.current = true; - - return () => { - isMounted.current = false; - }; - }, []); - - return useCallback(() => isMounted.current, []); -} diff --git a/ui/src/hooks/useResizeObserver.ts b/ui/src/hooks/useResizeObserver.ts deleted file mode 100644 index c74b152..0000000 --- a/ui/src/hooks/useResizeObserver.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import type { RefObject } from "react"; - -import { useIsMounted } from "./useIsMounted"; - -/** The size of the observed element. */ -interface Size { - /** The width of the observed element. */ - width: number | undefined; - /** The height of the observed element. */ - height: number | undefined; -} - -/** The options for the ResizeObserver. */ -interface UseResizeObserverOptions { - /** The ref of the element to observe. */ - ref: RefObject; - /** - * When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback. - * @default undefined - */ - onResize?: (size: Size) => void; - /** - * The box model to use for the ResizeObserver. - * @default 'content-box' - */ - box?: "border-box" | "content-box" | "device-pixel-content-box"; -} - -const initialSize: Size = { - width: undefined, - height: undefined, -}; - -/** - * Custom hook that observes the size of an element using the ResizeObserver API. - * @template T - The type of the element to observe. - * @param {UseResizeObserverOptions} options - The options for the ResizeObserver. - * @returns {Size} - The size of the observed element. - * @public - * @see [Documentation](https://usehooks-ts.com/react-hook/use-resize-observer) - * @see [MDN ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) - * @example - * ```tsx - * const myRef = useRef(null); - * const { width = 0, height = 0 } = useResizeObserver({ - * ref: myRef, - * box: 'content-box', - * }); - * - *
    Hello, world!
    - * ``` - */ -export function useResizeObserver( - options: UseResizeObserverOptions, -): Size { - const { ref, box = "content-box" } = options; - const [{ width, height }, setSize] = useState(initialSize); - const isMounted = useIsMounted(); - const previousSize = useRef({ ...initialSize }); - const onResize = useRef<((size: Size) => void) | undefined>(undefined); - onResize.current = options.onResize; - - useEffect(() => { - if (!ref.current) return; - - if (typeof window === "undefined" || !("ResizeObserver" in window)) return; - - const observer = new ResizeObserver(([entry]) => { - const boxProp = - box === "border-box" - ? "borderBoxSize" - : box === "device-pixel-content-box" - ? "devicePixelContentBoxSize" - : "contentBoxSize"; - - const newWidth = extractSize(entry, boxProp, "inlineSize"); - const newHeight = extractSize(entry, boxProp, "blockSize"); - - const hasChanged = - previousSize.current.width !== newWidth || - previousSize.current.height !== newHeight; - - if (hasChanged) { - const newSize: Size = { width: newWidth, height: newHeight }; - previousSize.current.width = newWidth; - previousSize.current.height = newHeight; - - if (onResize.current) { - onResize.current(newSize); - } else { - if (isMounted()) { - setSize(newSize); - } - } - } - }); - - observer.observe(ref.current, { box }); - - return () => { - observer.disconnect(); - }; - }, [box, isMounted, ref]); - - return { width, height }; -} - -/** @private */ -type BoxSizesKey = keyof Pick< - ResizeObserverEntry, - "borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize" ->; - -function extractSize( - entry: ResizeObserverEntry, - box: BoxSizesKey, - sizeType: keyof ResizeObserverSize, -): number | undefined { - if (!entry[box]) { - if (box === "contentBoxSize") { - return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"]; - } - return undefined; - } - - return Array.isArray(entry[box]) - ? entry[box][0][sizeType] - : // @ts-expect-error Support Firefox's non-standard behavior - (entry[box][sizeType] as number); -} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index f8e5262..b75d1e1 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -19,7 +19,7 @@ import { LinkButton } from "../components/Button"; import { cx } from "../cva.config"; import { useUiStore } from "../hooks/stores"; import useKeyboard from "../hooks/useKeyboard"; -import { useResizeObserver } from "../hooks/useResizeObserver"; +import { useResizeObserver } from "usehooks-ts"; import LoadingSpinner from "../components/LoadingSpinner"; /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ @@ -30,7 +30,7 @@ export default function SettingsRoute() { const scrollContainerRef = useRef(null); const [showLeftGradient, setShowLeftGradient] = useState(false); const [showRightGradient, setShowRightGradient] = useState(false); - const { width } = useResizeObserver({ ref: scrollContainerRef }); + const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject }); // Handle scroll position to show/hide gradients const handleScroll = () => { diff --git a/ui/src/routes/devices.tsx b/ui/src/routes/devices.tsx index df384cb..391b62a 100644 --- a/ui/src/routes/devices.tsx +++ b/ui/src/routes/devices.tsx @@ -5,7 +5,7 @@ import { ArrowRightIcon } from "@heroicons/react/16/solid"; import DashboardNavbar from "@components/Header"; import { LinkButton } from "@components/Button"; import KvmCard from "@components/KvmCard"; -import useInterval from "@/hooks/useInterval"; +import { useInterval } from "usehooks-ts"; import { checkAuth } from "@/main"; import { User } from "@/hooks/stores"; import EmptyCard from "@components/EmptyCard"; From 63c2272c4589c9b052ee75d5496a555af1a1632b Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Mon, 12 May 2025 19:07:27 +0200 Subject: [PATCH 051/165] feat(usb_mass_storage): mount as disk (#333) * feat(usb_mass_storage): mount as disk * chore: try to set initial virtual media state from sysfs * chore(usb-mass-storage): fix inquiry_string --- internal/usbgadget/config.go | 23 ++++++ internal/usbgadget/mass_storage.go | 13 ++-- jsonrpc.go | 9 ++- main.go | 5 ++ ui/src/routes/devices.$id.mount.tsx | 10 +-- usb_mass_storage.go | 104 ++++++++++++++++++++++++---- 6 files changed, 137 insertions(+), 27 deletions(-) diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index b73d392..5c287da 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -137,6 +137,29 @@ func (u *UsbGadget) GetPath(itemKey string) (string, error) { return joinPath(u.kvmGadgetPath, item.path), nil } +// OverrideGadgetConfig overrides the gadget config for the given item and attribute. +// It returns an error if the item is not found or the attribute is not found. +// It returns true if the attribute is overridden, false otherwise. +func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) { + u.configLock.Lock() + defer u.configLock.Unlock() + + // get it as a pointer + _, ok := u.configMap[itemKey] + if !ok { + return fmt.Errorf("config item %s not found", itemKey), false + } + + if u.configMap[itemKey].attrs[itemAttr] == value { + return nil, false + } + + u.configMap[itemKey].attrs[itemAttr] = value + u.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("overriding gadget config") + + return nil, true +} + func mountConfigFS() error { _, err := os.Stat(gadgetPath) // TODO: check if it's mounted properly diff --git a/internal/usbgadget/mass_storage.go b/internal/usbgadget/mass_storage.go index f962cb4..41c1521 100644 --- a/internal/usbgadget/mass_storage.go +++ b/internal/usbgadget/mass_storage.go @@ -14,10 +14,13 @@ var massStorageLun0Config = gadgetConfigItem{ order: 3001, path: []string{"functions", "mass_storage.usb0", "lun.0"}, attrs: gadgetAttributes{ - "cdrom": "1", - "ro": "1", - "removable": "1", - "file": "\n", - "inquiry_string": "JetKVM Virtual Media", + "cdrom": "1", + "ro": "1", + "removable": "1", + "file": "\n", + // the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string + // https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556 + // Vendor (8 chars), product (16 chars) + "inquiry_string": "JetKVM Virtual Media", }, } diff --git a/jsonrpc.go b/jsonrpc.go index 05db3d5..3c805e4 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -566,9 +566,12 @@ type RPCHandler struct { func rpcSetMassStorageMode(mode string) (string, error) { logger.Info().Str("mode", mode).Msg("Setting mass storage mode") var cdrom bool - if mode == "cdrom" { + switch mode { + case "cdrom": cdrom = true - } else if mode != "file" { + case "file": + cdrom = false + default: logger.Info().Str("mode", mode).Msg("Invalid mode provided") return "", fmt.Errorf("invalid mode: %s", mode) } @@ -587,7 +590,7 @@ func rpcSetMassStorageMode(mode string) (string, error) { } func rpcGetMassStorageMode() (string, error) { - cdrom, err := getMassStorageMode() + cdrom, err := getMassStorageCDROMEnabled() if err != nil { return "", fmt.Errorf("failed to get mass storage mode: %w", err) } diff --git a/main.go b/main.go index 25fbb3a..39b1427 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,11 @@ func Main() { initUsbGadget() + err = setInitialVirtualMediaState() + if err != nil { + logger.Warn().Err(err).Msg("failed to set initial virtual media state") + } + go func() { time.Sleep(15 * time.Minute) for { diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/routes/devices.$id.mount.tsx index 74fcae2..4d3369a 100644 --- a/ui/src/routes/devices.$id.mount.tsx +++ b/ui/src/routes/devices.$id.mount.tsx @@ -414,7 +414,7 @@ function BrowserFileView({ if (file?.name.endsWith(".iso")) { setUsbMode("CDROM"); } else if (file?.name.endsWith(".img")) { - setUsbMode("CDROM"); + setUsbMode("Disk"); } }; @@ -566,7 +566,7 @@ function UrlView({ if (url.endsWith(".iso")) { setUsbMode("CDROM"); } else if (url.endsWith(".img")) { - setUsbMode("CDROM"); + setUsbMode("Disk"); } } @@ -773,7 +773,7 @@ function DeviceFileView({ if (file.name.endsWith(".iso")) { setUsbMode("CDROM"); } else if (file.name.endsWith(".img")) { - setUsbMode("CDROM"); + setUsbMode("Disk"); } } @@ -1579,7 +1579,6 @@ function UsbModeSelector({ type="radio" id="disk" name="mountType" - disabled checked={usbMode === "Disk"} onChange={() => setUsbMode("Disk")} className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800" @@ -1588,9 +1587,6 @@ function UsbModeSelector({ Disk -
    - Coming soon -
    diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 79a05d1..3ecbdd8 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -26,6 +26,19 @@ func writeFile(path string, data string) error { return os.WriteFile(path, []byte(data), 0644) } +func getMassStorageImage() (string, error) { + massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") + if err != nil { + return "", fmt.Errorf("failed to get mass storage path: %w", err) + } + + imagePath, err := os.ReadFile(path.Join(massStorageFunctionPath, "file")) + if err != nil { + return "", fmt.Errorf("failed to get mass storage image path: %w", err) + } + return strings.TrimSpace(string(imagePath)), nil +} + func setMassStorageImage(imagePath string) error { massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") if err != nil { @@ -39,19 +52,21 @@ func setMassStorageImage(imagePath string) error { } func setMassStorageMode(cdrom bool) error { - massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") - if err != nil { - return fmt.Errorf("failed to get mass storage path: %w", err) - } - mode := "0" if cdrom { mode = "1" } - if err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode); err != nil { + + err, changed := gadget.OverrideGadgetConfig("mass_storage_lun0", "cdrom", mode) + if err != nil { return fmt.Errorf("failed to set cdrom mode: %w", err) } - return nil + + if !changed { + return nil + } + + return gadget.UpdateGadgetConfig() } func onDiskMessage(msg webrtc.DataChannelMessage) { @@ -113,20 +128,17 @@ func rpcMountBuiltInImage(filename string) error { return mountImage(imagePath) } -func getMassStorageMode() (bool, error) { +func getMassStorageCDROMEnabled() (bool, error) { massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") if err != nil { return false, fmt.Errorf("failed to get mass storage path: %w", err) } - - data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom")) + data, err := os.ReadFile(path.Join(massStorageFunctionPath, "cdrom")) if err != nil { return false, fmt.Errorf("failed to read cdrom mode: %w", err) } - // Trim any whitespace characters. It has a newline at the end trimmedData := strings.TrimSpace(string(data)) - return trimmedData == "1", nil } @@ -191,6 +203,60 @@ func rpcUnmountImage() error { var httpRangeReader *httpreadat.RangeReader +func getInitialVirtualMediaState() (*VirtualMediaState, error) { + cdromEnabled, err := getMassStorageCDROMEnabled() + if err != nil { + return nil, fmt.Errorf("failed to get mass storage cdrom enabled: %w", err) + } + + diskPath, err := getMassStorageImage() + if err != nil { + return nil, fmt.Errorf("failed to get mass storage image: %w", err) + } + + initialState := &VirtualMediaState{ + Source: Storage, + Mode: Disk, + } + + if cdromEnabled { + initialState.Mode = CDROM + } + + // TODO: check if it's WebRTC or HTTP + if diskPath == "" { + return nil, nil + } else if diskPath == "/dev/nbd0" { + initialState.Source = HTTP + initialState.URL = "/" + initialState.Size = 1 + } else { + initialState.Filename = filepath.Base(diskPath) + // get size from file + logger.Info().Str("diskPath", diskPath).Msg("getting file size") + info, err := os.Stat(diskPath) + if err != nil { + return nil, fmt.Errorf("failed to get file info: %w", err) + } + initialState.Size = info.Size() + } + + return initialState, nil +} + +func setInitialVirtualMediaState() error { + virtualMediaStateMutex.Lock() + defer virtualMediaStateMutex.Unlock() + initialState, err := getInitialVirtualMediaState() + if err != nil { + return fmt.Errorf("failed to get initial virtual media state: %w", err) + } + currentVirtualMediaState = initialState + + logger.Info().Interface("initial_virtual_media_state", initialState).Msg("initial virtual media state set") + return nil +} + func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { virtualMediaStateMutex.Lock() if currentVirtualMediaState != nil { @@ -204,6 +270,11 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { return fmt.Errorf("failed to use http url: %w", err) } logger.Info().Str("url", url).Int64("size", n).Msg("using remote url") + + if err := setMassStorageMode(mode == CDROM); err != nil { + return fmt.Errorf("failed to set mass storage mode: %w", err) + } + currentVirtualMediaState = &VirtualMediaState{ Source: HTTP, Mode: mode, @@ -243,6 +314,11 @@ func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) erro Size: size, } virtualMediaStateMutex.Unlock() + + if err := setMassStorageMode(mode == CDROM); err != nil { + return fmt.Errorf("failed to set mass storage mode: %w", err) + } + logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState") logger.Debug().Msg("Starting nbd device") nbdDevice = NewNBDDevice() @@ -280,6 +356,10 @@ func rpcMountWithStorage(filename string, mode VirtualMediaMode) error { return fmt.Errorf("failed to get file info: %w", err) } + if err := setMassStorageMode(mode == CDROM); err != nil { + return fmt.Errorf("failed to set mass storage mode: %w", err) + } + err = setMassStorageImage(fullPath) if err != nil { return fmt.Errorf("failed to set mass storage image: %w", err) From 38252de03c74ce995d18284a22d49225b8c58b0c Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Tue, 13 May 2025 21:13:21 +0200 Subject: [PATCH 052/165] chore: create images folder when starting the application (#437) --- main.go | 7 +++++-- usb_mass_storage.go | 20 ++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 39b1427..aa743d9 100644 --- a/main.go +++ b/main.go @@ -77,11 +77,14 @@ func Main() { initUsbGadget() - err = setInitialVirtualMediaState() - if err != nil { + if err := setInitialVirtualMediaState(); err != nil { logger.Warn().Err(err).Msg("failed to set initial virtual media state") } + if err := initImagesFolder(); err != nil { + logger.Warn().Err(err).Msg("failed to init images folder") + } + go func() { time.Sleep(15 * time.Minute) for { diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 3ecbdd8..498c311 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -94,9 +94,20 @@ var nbdDevice *NBDDevice const imagesFolder = "/userdata/jetkvm/images" +func initImagesFolder() error { + err := os.MkdirAll(imagesFolder, 0755) + if err != nil { + return fmt.Errorf("failed to create images folder: %w", err) + } + return nil +} + func rpcMountBuiltInImage(filename string) error { logger.Info().Str("filename", filename).Msg("Mount Built-In Image") - _ = os.MkdirAll(imagesFolder, 0755) + if err := initImagesFolder(); err != nil { + return err + } + imagePath := filepath.Join(imagesFolder, filename) // Check if the file exists in the imagesFolder @@ -224,13 +235,14 @@ func getInitialVirtualMediaState() (*VirtualMediaState, error) { } // TODO: check if it's WebRTC or HTTP - if diskPath == "" { + switch diskPath { + case "": return nil, nil - } else if diskPath == "/dev/nbd0" { + case "/dev/nbd0": initialState.Source = HTTP initialState.URL = "/" initialState.Size = 1 - } else { + default: initialState.Filename = filepath.Base(diskPath) // get size from file logger.Info().Str("diskPath", diskPath).Msg("getting file size") From 19bd161a7f20e487fbd84a10e72fecc085168235 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Tue, 13 May 2025 21:49:42 +0200 Subject: [PATCH 053/165] chore: update jetkvm_native binary (4e2ce48) (#442) --- resource/jetkvm_native | Bin 1545928 -> 1545740 bytes resource/jetkvm_native.sha256 | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/resource/jetkvm_native b/resource/jetkvm_native index 084ce14970c8ef559f9c2e2bcdfd05d5a8993952..a47288b99656f189e3544bd633999e07278d4cc6 100644 GIT binary patch delta 195247 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

    }8O_-c4uVoUF$2;8q@g7j^oL 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�rm@2Cki@B^C=%c+6%9&{?!5rO{*>>I08yIk^%^Vipn!BX^el(zk`Q zg>!!d3#{bx#ALu{ZqPZ5K@ZIz?nO?Uy_LBIxY?!VT?HuO!Z0wli`|>>lbJYA?A8kF z&DbETK0d0`->5nsRMZTvr)wQw*dlR5fi67 zqGG25)LEa2>w|P4R1K3`ap4cBs2wlU?ppRFW%xq6UCSO}eh5rD%a}D0Oi%*E0?yqYZ5>@CjPui;A|P9L^qN0AS|BWx2B3?En&3li`)6PbN`l zZTQgVllU~U@S%~=8;BfG(rTl%IY2#Ej(S4&(kfsppYM_PueJMI@@`(fhf#R~ZjKdH zMC?iN1C?TtC1TGd26eXKy6ZfH=J-LCC3$?5QN)|g0*rs$?jL#(vhUSJmOx$2CeEO{ zT=FGEw7sH#O`Of34&Ae6IWBq%P&ADuI$-gBd9=lWu=*mnUzX;29o#RAYE4vM2B;I= zvs(XzYM$XHu_raHgSvnSJk7i~O6omcjFsM_4?LYn%I_(cD ze*PA>Ms}FQQw-|R+|BHF2(M2M(^basSEyG2$gC9sVWGty7|D>13R_ISH?$M^=)Q1v zwbs!Fa@=tdV<6aplM+H@(it%<;v`#&hV{c2axjv!Sq?^4yhKf%VEx`pys0P#BPA2~ z2&6dVdR2hp?S!r{4iI=e(no+|JaTInr;2l~l{Mgj8t{k!#W~lz0_4WxG%*F8dTh%_ zU!pnn5Z(_jz?Y}x6l)vU;|IifkGu+I)lw&mRP2%0GtU&D7%cJa7~a_moCfLI z3DcZ9k<#m95THK+`SL_Qp(3KlQ=l)w25AiHRCk|%?0`*m9Ae>Pi30>E{CpauK`Pef z%^zklxQ}NgcK~{7^;Q-Q2!d9Dpl^`otH=1U@$p3pZ9^pf!RGMkt&DrJ|HPmmFx_<7K{M-rpS%wsZUN8 zC5Ipi7VZ>x=Qlfn?O{frEJ_an{Y9W}m8RP$Kw*q8p&AZH@T(^egDwy-U^YJ{K+(&$ zhsahLjt7If1eMgC)T@C#*d$We6P*bR>gs@N@Xj>a(E#zL3-~ROUNKaF>=mI|V7){q zY@Dw3UYbUUp=d%)5g?YxmEJZ2T-eZFFL4%wx{}pRwd{0?EnMU7)LKXuAI3Q%9 zt@Z6DBTk{RFs&iX0UgQqI$_pKqwWnM%tf@Y19IMUV>c~mXb(#408aX1EQ$V*=kF-u zSs`C{qq7Ym$sRb)CRrgT1Sl%x8sys~eKvJ`CBS{W#hoEQUXnBifaN9`o7aaSHt%=w zvj`&J3Qz=wj2duSHK?SxC^j4})LkTr^?`Gc5W7UYQ#Cyt#xS7=!%yMWUl$}Zs6WZEAI{r;ZJZ{>*Qm6Xn}?}#p-s{ln8 zkiSD9a2K#qfT9cNB|tGH9khdHM4_csr|7QV;rhgQ|Bb8sc@B{Vra*!Crpa-L;CsS-=Fgibv zC)i>`=^p`#ex@!MVJ42(&dhIuI_Brx&yWB`Sj+{ZA-bw(*^i}@cZ^5${~;Z_PqJz^ zgS-SAe8r$nAb0eqxr`#B|5gEt+CKqXAeC$13v6R#^zX~6>S(Y8zh=_47$`Ln^k+-! z4TQ`ZI0PhTQz;+%o-9B?eL1LW2(<;SuxYRW*``iEFq-Yb=z~nU-2{x{z^J+GX6m9u zbDHo2RwO}DCWil%hD5iSFcs3j4|0+7}w0FhuSt7Jr=M9w2?EskERuTGJvFz9psKC_oXqzXY9I zI)T!3aVB9ZDJ@WPDni*UIV7LOpbp)$^LP{ap60VmrD;rNAh@PV2h0#4S9<6?2#~6) z(=43;e@~%nT(Ui=riv&X^%#RXbk9DcH}XZG@VDh^5$6?5W%)I#m=9!l=`@MvwFK42 zCUN`{JIQljs07F@%XKsNDBf@uJ zJxmaJ3)}%|v0&c=A)$c)d1Pa*Gl>zzD?fd~eHe*#-60I>II!=%Gm)ZOL6Asr50w7a zL4d-^xW)oS?D76~95z0+S6+W&ST>g<6M^B;TyPw*vXwpBbo+IFd#^RR#o8d5B4uv^ zk}2YqSMw0W!W3RvZEUtdv=liwsQU@+ZjD~{QIRX(dSx0nKV}qOLG5ynK@UZ~dOVeY91=-O;DT}gm1 zy^O+G9^^MWmIzQZt}UQbs1r1=PR&Y21FQ+P!G!L)Pq`$oCnt_$P>1f>iBCfQbPr>1 znLwx8*t;6rQlCUPK?tfQNA^(Tz&uAr-je4_If4c36FR52Bxt zZg>i!pAcLX^;6o>&fe6xi#k4p3Ax{Hd|=?fHmXEP^BfrBgcNz90ENi!2#}kX@kuTe zYl( zM}JO?Lq(wVxAoMLe0R(6yz6}#p1&VYT|1!GCS_212L!(3G@w^C&XM2^NHNsPSWt3l&mRN6t?moOVevheyA0u+P5whVfB{gm;v zI~nn@YBnF-)N4C**?T}F?myN^Jyk04lpjAcav6pYS8#sE2b8wBbBavqc_K^{px z4MqVTBy&^fcnVB96LfOqw+dDWkYky${WWUZ5!Sr~r_7Q2VPc=+Z3vem{6We8h!%82 zNPATziOsGB4C-8jMISVlu5`qBM<0@hjIF|F0E9~>fTspNAwbdRje$lb^5vPO=>T;! z-A&>m(mi$hYyz$61f6PuQof`gEkGt3o&uuzI!Z_D)WqkHFm=hN;cehE&3Uup<~F7v z;`BWRb;UgW-V76YI#NnHEqWC7^)oZ}Es8eTD$JM-QI_ z-Nt&bp=OIfXubipX3Lsw z*997{0v@qSTq2V} z4;hbTP~Wborf>*mk`v=326dgCdt%%V0_)R@>(tcQ7a8;wlko<3lbjek7}SyRtg9g8 zds^&fG9^C-i2%JG|1GO;)}4F zHj82(w>v{O!)Cp8*cWuR3CW-lkE7Ich;qG!tWo0_)S-LYW)kvq_{4yJt-yLRe)Eib zwoEF~l9nN9nM~>&D6#U8bX83IZF8M6Y3>dgJjWf+XK}h*vRsE6$dNOM5D1OY7SGLN1Sr;k z8>BG;Z#|ozg#d_?SdVg!VNgezWm)$E-F*VGw1faf(&xGfQ25*kFe=hsR~pH0Z{7^% zzAOx5xeMZ%gM{;^7l7rVqrC$i?*%&>L9CeK&DiM*5+L^z-U*;vtZhJ#L8va)HlUwG zldTIWi)GImwuu%z2|xD|Nn&}+mqCvX@lJ;zJ!KF7`-nI50izqb61Q|3e}u&Y zl;AgOnm+}NZz71?kQ(1(P*==TV>2}N6lCU$wCE{>!y|C~6H<^%0%So#*|ARODf-vM zsnatjQd1uoNd zZ7W2l=FK$n9f0Q18nRE9Lxf}ob?BaUehm4Z0(_B%-%Wv?{lIm$Y%)y*$n7NbQ3x2s2l;62yg7GEJ13Ng;rdDu$Ze@wQtT(R zWuU#OSHUWLA1X>|vbeB+plMM;Rr%t)79orMcACun+F;&I#!1%3wDoDbi82PE_sUA3 zErT&In2sfn^9}v2~sdlITqoAb!wSh;Tud1#))6!9PYr|No z#HVECk0Pc4=620K%XM;bOU2@rdHC5syUEYhZ?qCL0Wo zXO4O?DU2bSQ|xF=rNs_^T3hT$pui$WJ34&O5#q|9DEpi094xe#! z_VVvHarnp=!lF{~Rae9HoSBB}Ge4mB&N%A(bef!*IX)d!saC0@rI|6}DEy409{HVd z1W>;-juc8^$bQ-}Nc>hq)0 zGs*UY1E%`Hv6A9`a105EN{xzQDLQJ>bi$o7PC2?!-uI6676=C6*3)x8I{rf`CmeGv zohJ?-6ZYaLdjDrfms)Z0ty(9vNo?Ehp(L1L_-RKl<(_u5Q?KXbb{si!R7NIJUp?-a zO#Qxhw5L1A9pmY_;|>efgdJU7CyqLL8|lDlM~UV7`=3WbSK4#bQQwsbJ6U_IZ|iH= zgljvly|@Z-9l%wD3%@gOIDxAeR|&2QxWZTq{KY{Xw^C85VJ%pn_f9&3T>VcvR3r5+ za)fwGQ_)wa98D?VC&v)Famvxz(rr{)+IR@;f66hO?8S~@RCL18nQj$3LTK%IyB|$I zkC63GsojT)OYMifn{+qCbTU!NC$;Oku76TH%4izf-q%3Aulo&gWnA++{Djx04!*Rn zoo{_IDBII^XX#@ zPfmP(%=78P)50>xj(QO}y+%!Sg9hmxlvD~=T3_l|=9OJoT*r_#+F~fe#eaTdBR z5I0=TAM(0P=1K%&B8+H%8zGaAM8%?(FHGUMi* zu^3WHYN&r61964m3d0qND+X6{TyeM(aJ9qb zDRX_cHgTLBBa(%l-w1#Wf69Dz34((r{(q%EXn0YdSwe zLAC)4orCAuxaQ)@#g&I^39e32)bU_1KkGv~QEq zh}gn;{qWMlO|2CdYVhnxe?z8=wk@mWbfw&M#KkE8A7keqU-xuB{*#;A=0|V4G`Tl| zYST69vQqN0MZj39`KE~B#`dc(pp3xaTi;;zDisM<<1im=*Zste19%)&Bi zg0Qe42>m`^@Av!MbM9UK_?|v)PoC$;>%7kE{Cxi&%i413?wMZ)_8tgjsrF!&D#AbF zKnBMsedv2xs*bSc`B^GM`SP11w=Bxc^0m+3^5CM(Klx;`S+j4ZGwIym`48@!dAu`G zRj*1{WBkOGpOlVj*Q)Cnuzh<+X6@(Z*>AthMNZn$7f83wOc{CS@QIsd*?-cVX;h|Sbl9<9<^m^8_rZK|#B-^K%y827>? z@rOk*!=`GJw*DOV@9UT>#B}8iGgKEpaYOh;YOZQ_IalhvV{zu<$R7$azZV%_ zoY@=cE6CgwuDkr~nu{*0Kl`%t*PeapPu8BZ_M)Hgbp@rasz2w_b1u2;u-vP3C>zU@ zufAI14rRewdPt$V>az2HN@$G)UQ}0i{)H@4I(6>l=hXd-_3Dy~F1w6%%!E5~&tH4~ zrA`@JW*m^Yf6|t%g_#BWBrV|h+jRwvJ$20$TayGzt6K&e_CWs zM%Es_;j}F`WMpkPVs3X)P>miGRAu;*IG7$(gN6JE>pm`W{3;S}J#NbdtFq=EbD00i zpjvrFP<8T?#AC+=RpI6QVE>IlN40iKHA0p8)~A9uZh7{$tTQVQ>v#>xuLjjJd=XGe zrfoa;5!UgZD`34PV0Sf5sY##edT-0>(X4gJzMbzyj{4Eej>x7T1gCBJ$ynB%eqY50 z5)|Ar_w%e{Pw=&VBJ_&vV4mLc$x*=@rX`OY8j5T>Cz!qEmg9n1Ny(EXp~$D30|&Y? z-YO0L*O#2X!U?O4)ZdBR!m?oHwB(9oLXn?dF!TOMe4HTOK?; z`1*Oi)RVWYdpejNNLEicX?wOD-4{&q`vT8zId*&ShQ-N!FFNTLZ~5-a;7$J2icdqT zp#IBnsg zR>?nM`L}Bxhf&1z@!K_TE~{<2^Vcq%Qr{=;5Psq=;`d{Il>+Rv5AcOT;r7L|)OvoM z{08|A@f+s1livuxQGR0{kK<49oAkIJUlnjLfbYi_MWk3Ms8ou;wH!K2mC96jX@)9c zQ&@J`ELG00f?wrfvsSAr{;1|x!>^WK9lv^hYxy&}{gX6Jgg5hR z;n&KqjbA&z4u0GCZRa=0Z=gNJa_GSD zAhX>tzY%_8{KolB^7FGTk_z=6$k~!p=Bj@Dy47>lPW-%zxoQG`6P&sg!lS4CN> zknjThO8hGPPQvT(d+?j^>j`hiAB4N`x1q>Z{4sbKe*)kC{VcVN@ErX5)nNom02xd0 zTky;9`$$lQUrdHN{CxZ-{384g{959BB%btx_?zGn{8V@nzYRa*v22VWEjP7hl7XE4EW3Nmyxgve-yt7KbLfy@N4jQ;fT!TtDAe06A+Y9+h~zm)tn_~Cs1=-`hwBKq+CB%H+GiJyB|mde2|$FIY0 z#&5&lj6Xv95&R8nK6Fi9D2an+okv?NNiV|LgUk_K}m*Lmr zkKwP!AHeU!@5dir9?nvY2nZfY=Obe&{#GI?@dxqi@mmRR!S5nnCw_qlz#qmR!QW1} zDxt=3F@6L7TKr1#A!*CMAFzZb5+FUDVsUrM?T{6csueg)}<@mIp(3H}HWkb5-a z5x*Qimxvbpm2ek+E4&?lD}pBQN8o~$D2!i)AHZ+J@5JAPA0*!Zex;RRdHCx2kSY@Ygpex3AKx5u)+Sk> zn9+}v@+9oI$cbs8+ro9#GgY6=^`4u9?^BET-#ryjbk0mQsUz{-cs|1G#TTACQ^|H$ zJ2#$Be9k8GVAy1`d) z!Isi_p@qqk<&p)VtR%;_T@=NqYn&!XQb~=o0y7=XB}~*6^-R3ai5C-;@RKU^pk#XK ztUz<}b-tUDk}Hzf{pWb`BQ-Ns9)Fwo<7eo&!1ykK_OtHlhpmsuRf*cq4pPBUMmTmnH0XzYEFr~F@d8b)}@Pe9!rS- zM*^o{Bke@q-U+-Sp3mKNUVOl=>l65Q3H(9=zmdSB2^@^j@No%TlfXB`^NL-ZgNZq*Lw5rI zDS-zP_`S%?V?*;IRmX-h!kK(Nv^6J5$ti6g2VJ@FRyfb&D1QHtJ4wj!T}-@oQuaMS z*Y0`@rz?>sY|9&jwY($a^M@S)+xLhUAZ!bmgtdSu1rAsWof0q5kvF(!ygXrBUg{hz zFPyGK;IJj|!YKqc?G-Oj*cP}6wiO(;JAAxf?Ue^2^TxxMY+yKkEE=?+5h}~wze+!SpMZ||CfwWnEt`R7Gpc2+K%I1Vv zBfQKLxDeiKag^{etHuYpC(sC$jf2}qY8IFZNqNFlAROy-?F8roqhuVn1RjULh9qsm zEOjbeZ?U8oc3qY_Zwdv*78(V_qQZrg?Dzsu$WX^ zv~X`DPb^vu>k5iRo8e`Y?_>~@Zj}I!qxh4S1%6I^i&f+6;3kVDy|C-D)NRCTr<-7C zA7cS&0^zXkM519J-)I;mxBo3O?#1M7R)Nwa9TrP^iFaL=`t1}7H0>L&0343>Vo_id z3ABksV1}iD6xd}IC<;X_mh{4|%TiBIp+I19yaIv6;rKQxCV{O$PJ#kGR)M0xCW}P@ zVXeSlr%<4!AYOr%FadTG^uV?Pxd{sNSp|v$y%viC!diiMr%<5afOrMM5@0J(1=|Xg zSqgleAaFN#Z|Rh$JK45`0)%b(89TEqUQ6DR)`|{qFIBlV7)K9-lgQohwk-7FYorlQMBx z^7cSp(V|(-VUbv55!}e?7epYdVOT9Cpv_tym%|+vM@c9dTo+WQ5U-t9u)Zi>L1C@n zMWm0yC1lbYoMG5inDxJz1pec}qhQ$EazzQSO;}+SusuQGI|(ekK6t1pV1jtzGQvfn8O-93S6F~nh>Z9Y_JMkIYohaC8j{Bk+5Ar1*|J{Hu)Rid{6%3a{2^)$}j7`-jhjy z>)btBr=`GU2#6w}!4vR{DHQ5IDqbOBTOqZ=C=^9bx5$_AFWc&eXtO*Lv_WEldnuq3 zg+!!QP}p@r)is3z#YYR!B-?GwOHD$0oLhf zqkI<%*}Ga{t>B)-Z?fX|hkGnOekWHgRL?^d1Q9uoq5I&p?y;g=O>3Xdu zSw*~d(g`m};BW%pp1?9{T90G>cU`)YO=0VC#sab@+-kAx2}doKJ>eWqX6a$}5Czo2 z+Qeef4vU{5z7H;;K&jvWoErXt=~N#R&;*xz3B=_6ur_(}_h>U%Z!)D<=O1r+^=`xu zF;U4XEfdw=@Br*KQIq-BfSDwXp8&J7Mkg3_5X|v6NI$SobBVVE*ZvS+t~vm9c_{@P;(Z9u(sImd98hF^Tm40?vq_|R=w2=;ru z{R9M5{U}K_BT)L*dGIh?;qeu4(OG62&;~cabzb~Ku;;?m+C8eRJSV=*gbTe4(q=6d zOOy3jEKN27+f7zqxJiZsNKh8w*K=;P`{q^(4?5-2mNE4*i(f@UUV)BBzc$2rmgJr7PfU%)#KO7)j>523 zSS%b4)|*}$rKDl2247B5K+#W40U}UX3zYn0R{nH4-2@6_Sf_&7(h1W|1rNw%{N-2y z$C01_)}8EhxX5BDKm@qXwTKkpx-4}8>9y0HuKQA>u=D|8-3LCL$@nX}%+x?kRteiC zYq8?5nxcT}pP2$=u@Tn!W#VeHSSGGsi~GX_j1iz`E?GW{FE;{Y`K+^8me2LDo~UH? z?6=~biRR~a4Vh?Q-9%EsZdfZAmNp!+60}jnVodDEj(WE&?7EWw7y^gkL9d2B zF6)<~u%0>7;VRf&%-jl%TN6_b@zun)&$bR)MS!mHJ|xgiCK7Ifi8(#7Ox$2>A-^bIeRA86@6yL3Ps<3YE4R zg`|yz?E(v=K*w>W#mn!MuWM9tqY)r&D6HF1ny3%f0;Paai=zncvkK^;fRcm?2)iz* zUUn65>#r3UyU8dZQ>n1-1J1;8vtb!l2$-lDQ;^VLu6^lXCyBsy01*AY>yORa(H3hy-{#vU^q6ik&^4})Dkn|M< zNQJ|b1Tzu7L&dhu6NUi zoiiJSR)Hr?QDE}}ra(C*E3C)4OjP}_o~UGTseI5BD80VX;u_>f;aulPh~xiD3Fz%I z=L^LG?Uq84&pi~Ac-OhBpTxV4TlOpk!n#d!9yJO`jf8c9|0Kg&tH3Cdgk2X@pIHL6 zyo`-To>WNKu22b#e0TkyhRJFO=tiP+q9}fq+)M7bfXWe*dQKP6@>qNU!n%OdN#6tK zlSj*|vE(fwehwB1$o!)(L`Z<^vecnd6u5GeDbSfpVQrz4N#BS?{0PvyW?|R4P0(d2 zP(k|E$CFf-jDOLvdWr&zerF1lxm?(u%UfYvfu;lnglz?S5)|0BnUmHHo&pV1C{X-_ zQ9x=etP5--eKl;G*tz}RDKVgaOMDCQn?3nY!TDIY1lAUZrT*fw)Jsz+kh3LT0b#8` zlw7GiW8k`g8k!>ikdTP>FJc4M%vaWd9zPB6K! z>w;<)f3{fhvU_W_cptbeYy})lK)c0CY3#6^RuRj zWZZYbS^+WnpcP+nHsf!S04=bJ8g*cSR0`0;NZ55jRjdCbK-ajTH@?QgxjI+GG-&nmo2L-i{h}q1{KB@pA=sAJV#y2NKZU^JKgSCcwguL~w!m$cz%9sY zBVKRG{xXI9x|d?|!_J050_+0T!*&6+*3>N5kcItNMkbPjn^@1SQ6m-;c3n`t&7YkK z@wsvd&2eG(qH`|=P7oly`dytD)}5?(drV{XCGoC_Tn;;@Zsqv@L;_j~DE9I&-CZ-~bqeM5wY7|h?1jVl#g=8LS z8%$E|p8N|aFYoW}Ce}IrUrRvu8%b)Xm!JvmLBP1jH^bU#i?krXb?#Mh;YtF;f>MED zSXaQgE#OUaJ>Y)imBP%V?)v`}0bL1gB<#AN>LtG0ivJV5$zs`Z)gzD&;V3XbeEmPn z35S2fBk)#F!7t&?A-T_h@h=(&c4x!!7VEK>U@zGJwh^!tZiE9~0a4QRFVUSSppGG4 zJ8hxhyT(G&CxmsM5DTq@W9R?H0-LQGMUf`#x}cI8){;R_L=vBIfKlL#DHOV*VU+HYC)76?O6x#HWQAp&& z=KQ~0vK=OYJqij(O@Y5aU=J2SahloPQVF{*sAOMXL3&OLIZb*8>4!c}avtK4ChnR- zp{mbV|6S)Exm0}?U*lF-TjUu8^d&TruvTCj@!PHVm*9SjMZrzT8{=+uQD_J8(eM{W z!#4;RurmA$)=qb-s;?3hhV3S5g|)(8Op(9)8e5#d)gyi z$w9fWJ}8%l45k@*(iNA%>}I9fVW%&SrTb!MYJNRM0U0xmp=I<6+g+~&)(S^asFSw< zxh~)g&KlzT36Q=Yjg!x_2y(TqVzwzj`lhh%nsQEi2-Zi4aw4WB(-%7t^CSXC;gwzi zqF`Z`QBW@4Ra-0;X@ebwWf7KZRXNLMd0WqbdKrNuw0sbOGTjWrlW?lXpThpV45#eL zy+;=3z(I?nH|^|UYWxuKe znp`7LOxgn5eWJ&Tk0Q5{=j~k=P}Aoql}jJ!9pI5WI`wRL_|u%Qs{YV)5=r2?pvonI zcG^Na7Z?TQ%8qaYiDX=h#i|w>1rH{F$TfL2k!K1J3kcg5 z$lco%AlsIe7K;M47Dp**t!07JDX^Xug@Xtb%2MaUf$%?fkB##+1mwbcoUeoPEf#?R z*mdqO5`nJEQa6%bJ8iLvMW#Zcps?L1!i|=O4)%Z&U3hOpY+UtJ{C|GO?5EBb)d2+I?9M*kE+geCTKpK?N|WSLfIgci z>0ReqKwj+Ox`1;+SJJyKOZ|cT+HvQwyZ(F_qa2H!&=OBE13Jtd%YAhlGg>8Z3u(nVX#k(zw1b794c6t~N93C%E z7%Tu@a6wIVLCD zS_shVzck5KD}yvi>T)AMnxq1@n`8*q7Lg`VM;aCj=D}KFsc?nF%ls#8F88)HSP7zp z2ju2y#|4~G(nb6nj3DWkPGOl)}1`OB1xO^qGTjX@YGe&`lsUn6Lzh$?}geHTbdA z0M-gz23J_~M3kBc>k3^rMTPo~4VxNC0e#1s0z^QD)dU?=2*@in`DG>)t|6Z^kyM}s z)(XmTK##>z-bk1L-6oGvK$TSkX`>p8MIniIT~IwWg+e9A8-+vxVXc4^SP$#+UL}8Q z{nf*4*b*SUTv+#V>D9F-7y(j1D{L3A)ryyiW*BZqW3k8w2@3NDu~_-edcE5`uX1>z z(2@i`CV|gR;42dNmIQt%f$x#>Q&+{COxUi`GFY1|ielwfpXf~x_<915Ca^CT0iKfz zhQhfT-5Td7@Zkx356Pnm3Jco`_a`V^XDKXAGEV$j;R?&ApU7XKvIi^9_ibewF`Hxn@|aN_!_KvkyTK54oy(QH?$>o( zems@Mg#uE&4b3gN3_B~oG(q(#9GptLtp8CGXs2t~f2ygW)JRy@NNS)?(=|wU)`mQ| zhG1#aD1MEl&=a`?cuwR;`CHykEROykP=CsG{&xP;33y?#7Z_0QOc5{dEOuQ${dbCZ z-eDYzpFZFF6Hm+M&BhW)`6!SzMFQS+ED3M{HFt`5UViLG6TftdcwS~4i$8LT_!3?M z=gofsb=njOc1rIl{J_GT6xB z;%VgZUxoGzCoS;Q!_%Xy{D<+Pgl$nxuofj}=W4J)2KnVY>u$(vrOrWzc|-<|S}Zq` zZ#FjK0Ed9BCcr%)fnDd$A5wtp0_xx?6dtXLS6J9qc+QWE!qQ0FV&z3{xFM7oK4A)( z>MSEuR(fH*(#s)63#^SG9i)|N1F( z$vemBB4r8NWmUjh7pdQnk*)4V-Wc;WTlClp#y56aX#4x8C~D5R@kI&SMU}yJQ9G@o zq?*F63#y(eWmlOJ{llmuvj^1S6}fd zsy%E4^c`k{7Z?Skb%kw{r(S3Zlmc?C0uDf6KI}dY>5dkO*99JC7wFy5mS5|Z$Kv8{ z!zI8juoAXw*k#qQdd4q4DEU9 zjZ>5ry*R!sVY{q;SeGTnT4{-nVwIJVlWz*mTe7JxUW~9UW*9bNBG29wTE60L#AU28 z>ExKM#9}$7tG8HgJ&(dv)u}1(_+5T;Xh~9T(3+%7Iht{m25~9D1moRFv*m7A*a?Ivp%UX-YmK_$$@o^ul zkB=Xk75VONG^Xxz*N2ukiWJ>oD-tskC5`@AHzwV) z_lM>#lBO9YTvtI(-YnytYnr2Ux8b3PuQPO)qs*pXnz+AZL^gJY<~llbwwSVz{dQ+) zjT2sUXI%JM51_M@sqQv%sMN*>=s+tZVkO@Y(uIpMZ5B(VhG2W;tr?B0)OQbt?mbv; z{qcY1kKOts`MO}8PmawC|7-F|B}axLul+W3Wwxa44NQxrmAJ{FE$4QHj`#U4-ZQfC zVXp?M1=B1=Bi}t7TIV|;6j}F3XvtC$(ter|A_dMleOk==QqdCFR5bGTBcUC>FL&Fr zu{$(3$@gw%WXGeJRuWd+9GCFBM?L{T zP-+L9kSKn&)#&oRiF{r^xQuiXUx**P)71AUxQ6rv zP5}XR3fxbDdiQ=Iywl<<;R)D1!r>9URsz&JIv}WaeM$>mhC+M01>P(1p3^;`5(Ql6 z-l!_9$Emd7Ff0}og{8s#cbNvu`#CL6d5(qUy>d|!N`UKl&)hv65yK)tOfDuYgtZC9 zBDDydm}V?8`k1kZRABROuozoWQQ)yDDp0t|t$?$El0GT{b`5%9T?3g2hAfsEsmD!? zq>I(SD+!hYrG`bihF%4QUFTMCnOnhdtcFzyHS9uQKaHf@yzn(sfYfm0>FKJJotVTw zH$@HClHPUcs&f$sFK`V3l3pf(LAXrLPDp^v*?E66qhH{&$0pu5!vm>IwaRe0ppAqmHov+?v*+^}&_$6vQX7M|4;p-;< z&HIqw;)jvH$>Q%MKO8Rg6xe+^^UfQX)Z@e8CX1`!K8x4E8H2`Rcfx~1jB9RIk_tTl z2X``xXa(WTaL?z{oev@jzW{f9IGqD1-i##yuM)6qV!9gk6o}#%ePP=09=O!vf0AB1 zJuGv+GU=twh3z(Pgw5(jg}w@#0BQ5!*T#g>rWF=To3&dkO*UY$G+FQ)lV6&w!eVK% zc8jIS2EtZ=v{`V{2#_|bu=oRN+z#s|nt3F{(&8dG?^_dJ0oPglb9ghXEpQJUR^OR` zZ3L8A{65@a@r)9tQj3?sg}Y4tli^0VfOe54{Sn+{@kQ_eTCM}!i%2+2Q1zP&aikX z9JE;G4V7n1egyH2C#R^QBxypf4y-1iU>_1V0qN>|xVA8bj^(!T6>vpmO6=a>>*1j@ zQero-N8q0QQ`9D(oBk2F;rl77+~XJFf&)`xw{uPjO76_BKts{hJ{a0k1tKMXmML|NY=e3aIoda5P+T5S`lNDmYq{5__P% z9_~6a#d-9HtE1^^9RaP&>GfWQyWz&x0G%K-q23+Pa<%@ujsO~u#cJmW>aRMKdz{LrCOah;rz@qTZA5tNy zzgVC-*9~Ad?eqcJq2yN4h91iXrOsmKFay?y8M1h#@(%10FTE^Yr50E1FQ2oi_+b>TEs}iplzea#|dOdHy*0i}C9t(GR3W^1m{lZv4o|I{6 zPKiAmB%4}4-|%Yj(#s~c70&ZGir+9CHZ#>fP(VK6D0+y7pYVr##<4cpayb6UaF zUmFFb$%J*2$!nS_;C%9kyeNJfaOorCBJjBsHR*LqnMeX#OedUn9G7waXx5H7uy$JhkR?C7w`AxubM-Q~=6!R# zA0@Z2>w?ZyJLk_S9`k%zLII*s<4Y!9o-G!xBwQ*WqhbK|Mnzajubm`7oA?X_F8fRT zIGwOGzJT~59*NNDuT4-ONGQ+Huc=`6JUY?_{d93hrRe6uy(qQE8Z~{5CvOCjDmw!`q(4-tsj~Ma_A&n zLq1&QoRqOq3`XfZ%3gmfBv`XRwscL+h8~-D?=crV^2ZkU~ z;6ef_Gx=txcQo7pm+XUvUWOL9YOhqaGtEuE9$vOTHS*#gf-9D$Dh>x7c~8RSho-99 zRF|a!{l^22bGKkSVQ>BCEEXBG)2mzl2_{1+mk)%SNF)l&(QFr7;;}U0h{ba7nqOw( zqomtzHAz3^@fqK+YWGb1J^}Jg-<6)pzJ=x6zAat>vv|YJ1x!RlJB!a8xHe&;y3m@a zq`(@!6YM&-#?2NlCV%7U+#RXM`EdmFpPuTyyTeD29ED419FDkaRK!t3dy&cx5853D{w!oE%Ii9!co%Y zTlq(b=X1_V!X>{tfx~y=H^H*)&pd(tFW+A*_X74MfqcVJCpa9I?=|Z9)v$cuQO92Z z%eNeLd>EE*KT4mH0&a!nJC6%Ij^d|UN5c5@e~z;R1_gqA9u9G{GN;Z@Vlt zwoUJchv7VL)QrN~=_d65%;cBlSXeK|;V7wV(9m@O^)&+c>ag>LTQQl$ms=WV(wM?? z2$#DFbK$yQF!Ol|90-p!F;NjO`B%U-*QUnqm0JxLFzU9s>BH(`0tT*0jlEI30oG2} zAoaTV8VKteL`mFbRp2)A^QC0PE9D#oB)-n7zy{)l3kl~D*PP2*2D>{R_iT7GTtg=3_+O4(MhFl& zQUf{IEV|jS9C|guonE{gTIIDF3(5N54VQTFvi^@+EbD*4Ehb*p|2mi5_5Uc2WV)>c zQ3Q>AWK5iTA`0t3&b1+aYKdNUJ@D10#dV{2_V9`1b5tR|X zWN-a%B4FE{sj)+`8{qD{NZ^^c1Fna)3A^Bt-=@YM*?$_ILxC-xf)CxuDA-6Z_xNSv z$GfnAH+2ufo%8|re{QGvkbnmXpggCOd9#B`jh;cA((*o(Q0X6U- z5qwa7jQ%716)Wgi7GM4(Shy6HkIm7z2-w<`Dp#4Gu-?qoU9J= zzEtl9tX|9gqO^{fOAi=f*9BFj6fl3wxqk~?nUpCLUiVHj;cToEWRIZhxLej67I10CHO8Y{Sds~ikI_OVpo=Z_gLFv`IG`$DZn0~t&vy%FZ7?Iq6xvm+7M5&cj<(6J^SDXEPey-iX^=e zy4ZJeR%Fc^-i;+SbJAi%je9SieIvAbcDGeb%c*SdBl8DC^OCwFM-7H9ULi&fE>DZC zD$XfQSfA2#?wlw|i_NK0-!@p+mm79I84N95+BZMT`E;sun^!4i41Si{2)`wzF5r92 z9!K%}=0{flJ+#VKx-hcg@1ajJ%NAx4DlVY9PGd+c+_K@#(0)ltr$%1+N9bnfrlf6M z@p_a!6sO0!q0oZa{yZZminPs<$A>}(_!{ye?+k@*IJJu=!B1KI9sG3U zH*baJ&E9GS&#z=oj=cI-=z#g#2-XdI9>IMUOX1H%=D!^}e1V*8>3luS8Aj4)B&yNU zbJ{Um*1avaY{?Q`^J$tYA%P_1M!jeM8M@WioEKR=9D3My!-D;C>vD_{{vg%##T@w8>O6>e>Q!SRm)i#UYIU=(0-OvKxVBVJ3-VGg=l(Z`{^S#hX zPGirxB0bjWJ*Oa9HBY@$zRG+v*b z7E}Jc|K{dDZd-g7KU>bt=58?Qqm);&cjU8whkoos=2`DE%tU5Ow@E`AZG4~Gt>g?^ z>7(hf%?YyaMXMNz^e%L(}C;eW+ zMZ+B*cpB!vZQ@14K1%p7bbL}#GFhO0pI5J@-I|!fLUb%l&tZsaFJKwv)_x%{m8xLayh4b|SzE9}9cSMf(By{#{ zIsP9yVTRi2?Q)NdJo-uKF{hH%XT?cd^PkY%rE(i{?%BpT@?w#}VI6c1` z!}wi`B1=D|lkJLJ{b}fIr~fuLnc^57pM2`gAPv{XWk2gP*J$g+$7uI{hPZ7s-e;k^ zq%Hp&I{REXAYS|IjM(9%wCQ$O*IH^b4%@ZKd(N=brov)r(`H!rQQCCLIBhC@wXoOZ z`!+3d*0?w9g3rgPeJ|-3$i zNn0W(e!)kPUpkMj7+D?-tLickLA)uxpn5Cg?jM~9EzImiy6fD7uHk(m?+_hsE->2< zDJ5e!mL|ugtBmuxgpm`7T}4^|Yp1(I=^T@OG4aCPB$DPo5*~+3Jzg#H_6o%2)vEJ3 zkf|r2%1dw&3AV#^9*5y&a|5x{RxNM~T<^u-4Nt&2e-~UaFA%#H`vlwpYk7Z!{kehI zeLGP(kSZj=b?&*%rul}WM3nDuEc6Bf$`=G;2lyYufjr(NMLMam{{k8wuJ^f|4G;58 z#GQVZ^Wkla11c}sk)+;$7z|SS{aFbX>YRO$2@)1-t@l z;hhJuBPCuG`Z?TM6i~}NdB247mjz-QlkhDB5J%NwHJx6h5=B1C~*Pmb%ls?sC@n+JmUy`L*av3^-uzH1n=95jE{u^$opc74Z zYajy0Vb{49C_UA%#P?PimI8x6GCYpF;JJa=3*cv5$Uz!B>QsOWd3%0MKy4s)LVtG> zu#sg4;3}{XZvC-oQz@_ycAcxxsKp16zJE$Jd2%_dz3T^=UzwG@#4RQ zH#G%RuE#00G(G7#fv)7J>dn{HDXRaPKu0> z+$rSO9^=VBlwK_+k^sAaUf3>R*|G5jjINGc>&speUU`aX3OVT~yo)xMc|%V6t=vjH zYoxH8~N_ZXdu5-^!3g0x>C^`9Aypofj$19ofMZA*r7NcaA>L&m%rX|NX?<8ZL z;j~~(vENOhSTJ2|Av2Z0m!?o`jCj|%iV2r2F^Uyl9^6zh7=DE4K7 zVzPDVUu{P0<@qj`5H8Dx9C~M*GBdV+mP758Fv*10mw;sw3B?W-rMsI>&?`LU{2xkrxPGPu+u`6Pd=SRV{~%mB%dZN&^v}U#-(yQN&CUM`T(rjw z=g>>?|8tFs+!x3`IkSR2HphdG1%CJw>F^QX%ZH z3pjU$1Xl7Td5krw_dyz@$TOD+5+EH{GA@#KF57q6b{l^0ZB!yVJzE7Fqwdp~6g z>2MEVg%9~D>qP#Uk=3*4Z)HR7-GdTw4+$%)BDc=UUK8$Fz(=LMW>|eGi~d(LREEd3 zaP#HK&N&0gcO@J>Vit|NYv=nIyhrSBi6lE$Lr@*S;CaoSnTSYk>7pH{EeRW5+oyHFC=A?ERC+ z%YBhw%+5a1*OVQ3ZFcsW%(`pcV{Vb~@a6Qp+as$pv(NIiyb<|bX7=*1+{vjWJ@s=A zH`b)Z+UP~Nnb$IRd#3q2+#D0b5!H8Sa%>`OCqXX@wQj+O;2NvISKytCvSRW7 zgk{y}qOeRKFVus_PI6mRjQ?MFa8JMU>32~esgcR^Yxg9+@SMiTlI-jSOE<4@-@Gqj zXOm`YCXd~DQ_)(ueRV*M(-I={TDbGx$hFzoOOkpck7sAk^JV-n@>+KGa^E-$&TdQ; zqt^KP>}u-V=YGCg#QWEwf>out@V23uv58_iJm_<8@|W}#uy$h89}3G#&CDV;TniWf z#k~|M=|gX-$gbV85A-#jA6c4{y~I~|M&w-l%#kXyNUUW)kpD;CR|qfR zf<+D^srftiaTXD}s0o|=!_0~$@SN>w&gpiEKLai~(5E(ey|o_h`BjoC@%R^TIX4s3 zDYs+Y4Od(eP>r5VH^Ot6bW(|zNlbTQ}1n&YxkfBaM^l~?0G9%xrKg?SJneW*PcAr*`Chk zcw-aX!?cm_mHo72|C$!_#JvpXtc_&unSEBW`f5hxvOTjeS>9Qms>Zwo{${4|ech|X zqTKFq!#O_ZRsi7wc;c<(7!Uj>GIvh)rM{ZGBDc=TzA4;tyLQPN*PFb$MmYhPTu5wVtvCaQ!N?kQNJDbL=X3+ac~Z2Sxt5aNvA*Z!RNp&GoGQ zdu7k}72FtkVXy4vS$R*nk7r2Yg}88>gH8b2!{bwRL|ehkGljwpYz+c>BsZhc-{xgs9nL+Ksn4^^&u<{5=VTbJgPSjO_m!EP8f=DBzch_{FI<18SzDfm zJAUTgjv|`>1C}@T6|&k%kN*^I{!6mjjBJSy{8~l+nwx!1a`g;fCIZMG|C zw^F^UBd^V87u--eGq$EaNqCPnx%>src`LGD0pq-!a~wG+EYc6Wk?x)sId?(!SxdXm zHEZ%V!Z-cNjO!t|&d;5Io>pJLWo?nq7G$qmFu2&KM!fJ%H!=R7G5L1DRjVSmE@b}c zI4<(S!tCV>2F^6QpG$6LIN#x3-V=SUg{#kuEXd1VvTz$OLe$OlCgBswW-|O5E*;8@ zT$Y!8L~`k?DUsjjW$(XYU@tB|`(S!4Kxa1e^r4uQ61XgE9 z7VMq9Dx72W>!04jevGF;O1&C3!R4GSuYzS2xB(t!vF`KY*TWf~r^Z&AKfsN(KId$@ zl=BL_)oPh9VR_-9UJ6rg|{OtXGtt%p{^Rt&6UCUcin>?NSNl?BdEq3_x4qU}>DfZ$&hufG_{jkib zNq5jOuS|{n5rIkNkx%opk31-!tz3zH@=pv8a!+M2W~;3^Y%p{ z|7hgaec3~{?}~i3FUN~L-tES-Ig#;Rg!%16_gbE``={{Mz5KDsd}aqFcuwYp5Zq`T zKNP~F*SXJ6iLs7?3s0KsDHc@a@cO6C$f@aI<;!W{zAjXk4406g_D3^gmiQIiSjI(4 z&$Rc$+KB>>!#i*AIWLHj{4c_Fd(MchjBmijerrg<@)(64AnA9q2gu=2Oq+7=JF$Re zk;CBj&&~1UYPjf!Txz3j#6o8YTTAO@@UYeMqj1M*Gh-{v@8F$Q{6Lt1>L;)OwUHWp z3~y#fwanx6yXenbjmdV0+kZLDxppY&_l3vrGJT;89{J3y_0^L9+hjAe;g0*cktev#^!*K1NjYa+p_neZft)=_k)YspG|lQ91d$I7Fr?c)6C(~`EbUqW?R$;D$ zSAOk_DR3s77nu>8SAHt_{buJGhWlA@Yr>wuHUh>MPglbpcfqMv8@>z=&YI@jbRY`; z6Rx=^DYl3IuP}T1VlRJs6a}*V3eU0dp7Mw1q>n6!GRmj`4|%%1{4@ea+tQtn7D)lM zaKWBw&dW!IuZOi00j*K?z8rLX;w`Kj;GSQ&ua%VaTj0*exvkV&gm%C~oH5bEdKg~2 zYOcKG$yH#CfPR+LGFSwt-!K)g^T!m(hU=_j%>r1vT>dB(p68FP1!u!u)>*Hc;5O@2 z;KT6d$9R59o9r0^b~5gpkR!`(za*IM-Zdc#{R19Z@4hWn_zSq7$DNx=FFb7n%j`?$ z;B*e`-`%|}L*n<2+<0L2yzmbQ&ZwemdD$+88?NE}sK@Kzu68qe?}V#2xVLbMm~MEP zH9fry%h~P^&vOUc+i=tKW`3Ly{$Z@Vu*$xl*1IY_b~bK*xO+*mn)K$Yg9BFIzYlJ|n46$|Zu*De=D*K~4dxdl|MDco$tL76;l575`sHRcj=>q1 z^FXR+ff=0$xFR`r2)YLx<%!z0Ui?9D3ul2VJw6^DIU~)vJ>F3mZs4p!wHJRW+}hzO zBopq91k@ZnBi00;K0`{+N#EW;{HS#X;5m3OkQO_jejQ%M*+H%F7(B5pS>=*f6i9x6 zHP6~qW(yamxo=gG1bGCE%$}xNy$pxKtwZkHk|e%D;<*&Rd5J6VYPjHLbKdecc(6F2 zO2{wUkO$y09%NtX$$MJjui!eL$1lPDOMLQ7k~`rJ5uiHEHu)oX_-eoNXr3tWr3eU3 zQzO${1%eN<6SS73LRj7{pvTS%xT(*5`GMp=6<+^jdaTJVf};hzFH$~yED0J2$iLWq zGp6uuaB$SUg;=-~&agUJA3XYOs`Gj#iGLfej{0NAwBvBqIy3mxZ!tOd&&Vhf3(bNz zXYuhUo@#RxSU^CYb>MI?TzsrKwm(TS@W{2U;o0!mnXzG{u7cb9Oo6w-K}+GyaOa_Z z=Orhia33sZ@2Ectu%x_BK*LKjVoSz<;hJ)@q$G7oO4muJOodyRU<>(M1nvblB%3B& z4p&F1p)<^QcM%-4c8))WM=s{6Dm`q%1dKD$6nFyfkOVa|orfqy!TaIdz_eH=dJZnQ z(C0kNClC@G+^OmUSe*_P4u4L5X zuzHkX!AVMOvfbc0Rwq0LUYj{BHZS}T?)k=yiUxS)r=|kGf-9`4_ilLmyqSu*$+75G zxG@qtLJX^a6EK#~)fBHyzJ_bRG5eZX8xhD4~TXo-@O=@dkLUwKhBlPdvnh4LX$+FbL;b z7MuAP1#tSY##3kkJn%Pj{q$g1yG)sXk0qe?OTIJiS>OzK{jeE!b#UuZTru%hziZ%8 z9vI0duPAT_T*L_%*}F=cKLnRu6_TUew7-Vis!)tG)b9PU?JRasNv(_rUTp$n@a&zaLaM&s@;=!Ic!hm&G6to&f4Lw z|Mw6e531{l<~ev7cNlDvd4ajV5AL#dFmJ;>`gXbK@QwCm*Zjktn^w>;yFI;G)?}e9f ziMr5}_g6`8ok@Bh&gJ@IN!SzkjRf%2skT7o@7N-7C0`3z3Kv-goCa4~SAi~swaes> zx&&SeFSGKuz@waP4(pli9s(MyiwzqS1Uvx;xfvuE;nF03f|uQBj*o`n)@^2{{01(# z&;8z(^og0j$AUMR^o!uUZ`pt8UVJzKrJNcKP=NH}GPsrvh|58B2E2`_v`Q2}U;}KQ zw7niyr_YRa(hj(jO{yN&_x&I7p3B64l=z7Wo~kYfMBuX$aQzI`<1I#S!UZ+F7tV|S z5+41v`*CQeMlAd-KQa@|Jh<)q?(;bkzZ8~N`{*V;3NE&q;B0tp*s9^B1Pt71&Zl1w zckMMTwyErdE15@ilRXQ!-ff2AAUt?)lJmMoQQ%`Z&pN;9e}W4A#4J9UaJa^5^MwR- zuAdg$Bz_-mI*;|-v%m^rOW{>;xz*+u!g@|LB6@bb^8;n|C4{@JuyUz z#*dL8`q$)Go4pJd{&`w#-~JD{)4G;A4o?o4^BQSSGLKj$J{YdJ$RBIsGC1RAEZ~j0 zGoEDp4_GGs2?=^1H|IaEhy81lop%9?g7?7dxfiC$Q}|JM?cdCL|2$lIuwPAheP{=q zbC@r-DcuPNto_C(VFFfK8c*9ojj!QqoL4{)ZWuP5Xg<7&rBd&B4uPZByH65`f@Sd5 zFWd*5h0lU>Qr#Q>dZBo@bwa49Z1{@w_3gSad>TNT1=tm;neSZYS>foRe1BS z&93<)Sl-g2d;Pa?*KauC$~YDUcJJZH==zvN!fJm4w3AL!1eaPCxd>kND_+Z-;tFVl z$Eh(_5FLRx!ks+r+33yn>)}zWNuGeqEO{08u_xqmMh<1oR=idHWfF|NIU{x;@h`aC z%8>R4nrPfyURnfio@>^M!(i>C0u^xXq}jln53k>0hV4~w>-1^P!x^mq9BdLWS#D;& zPPm^fm+oXw!E?A0H-vC8;a}kl_IwQ<55tOEJKUL)yCvYs-IHQh4yQkb#V*F=Uiv-Y ztxUxguyew72?6DACC4V3BVc)zkDh4C;Z2rFFNFKf=S6j%z!q4(FxHyxPTjY|<+0n3 zf@(cH^xu@&7VYU=`0qG4AE?`%1W*n*sxJ{40yi^#qi$cvN`>+${-qGbdnw2lqF+pDL6l z*a0`K=bVt2{$Fr2CoFXSFJSGYPfWx1CHI(9+8^BH#Zo?Ps?=U2Si8b>k|prU>&!h# zC2%Q6I*m?x&S=0TdnBtiZ=$&b&N$ojktTSVInAc-fg5+FIUo9y3iQBjr?CFB@pU@Y z9|;)c#WvC@B*7r8x7o9OS!yR7I3i8;dhuVug`*Ht?K>Ukmd0na5LNXGLH{{ zw{B!+^rqe)z~!g#RvRtgGy=xBPgnQiTDaCa$#gZmev%uSvRno47PhYZJSqG~W5FG8 z5%;|5ZTS0ekV&K4Q|Jr0qK)$}#kvO1QbX%#cP`v`JbOGZemUH4oj^DfuCh959bE8B zb0l>wocoSBhja%Vv<~$igD1i-8jW8fKwem>o8S#N#~Nl|!@V4?Ym;U?M;qUp9J}zc z2p-Az$F7?c!{yc~mlI&QpLa+q#1^gs4)@;ci`B4>fb~zO#M{&3ZUL|1siS*dw^C8^Uz_D7AYoc+uy<#re-Q)d9c&LCgqdqr&)*opi zYPifRU?E&*nf!FPf?=oUflJ};*j)k~|KCBtve;^srG6t!jr2_RI4qy9ob)9*Chmoc zIXHE0%BEAk1lM2Xi*>qpC4E|2tiZ3}>i=_}vlo-^v5j5wbiaGLU)KMF3D_Jmo6c3R zUE?$1N|skWGhPVi&-cgfk6H&8SzER!9N-(Rc+RN!J%6lkx54cj*->F(XZ;@{ zK;9=chXm4zK8AZI+$a4-;|aKrv)TWzt8)*is>mPsy}UsrN;;rnQIWAlE-0C8XlP_u zlw@paQCHyd%F83TDk<&tfr^BRN*+`+Oxotmj&G(C0qeFEVh;lHw$}CTbeE7b8rIP?<_M$a5H*Bvs(4Q z<|EpP&)^{rG?@|n89qtVevE=A`P_9mT;(llQU*K*Zzn)ELnj-fj?Q|KPpd_;;o0H1kTng1`uAd87d50*mW zdbpiZYqsUBaLC_`i)n@Zb8zb;R%dh&J{cL{n|$`eZbjfBxMjAVZL#!!ST_yVh(P0; z>%zk^!a%cTE;qonih|)zxGBrumqMkm`G^?JgZmg}C-7VNw+cQ)OK1+!{tWN*lhqFU z*WikK>HquX3m;-Ik+wNQ@~3bgs@Nuf!^7X;lkK72>l#G>+CT76QKyfAtA|<3a;b1} zhuybNVkUexYM8B0){0WN*~6!a&3avfLB_Mj^-UsxO>pan>~E9;y#TwFLV5}oUnU+W zhuuHJwaTbB@+}%7x_7sHKLM^DWo1Ds?5SgSnYoc-CI;E7{d~)AMR0|(7v@j!q2F1h zehpkZC)76p*#@^W&Ns`+sxL{=o=OwshS-39?-#AG9E56V=*6=SJGz~ACRia=YdWKS#x zv*`bKN`rB5o04QV!&%QUzc*Vxc=2>=w_rJZmQ}Py`TjD&v7x>~w-%1Mm|7$4Tj4?L zjE2YdR}A|3ilHgt>#$oH<^CIPW{hWc$KSxaf3pUi5$~XeQ-)l>gS(~}pQ#nK=nrtl z>s$yU%hU{b=QNbmUV-TUOE7TeTRWqd!u?-bi^V#;=PmZONDrQdT?fK!=`uzy!*xoM ze+=)xh{bA|Vzzf_M>5#sDcwiH4W@hXRkaihT#6cQD!jPExKB^SU?$wfd6Ot9KslU^ zqA^u^_$N4gI-fq30j?G9p9-+0NcT^{jb?y^cNnef+N(Y!x;^$Go#djSe?-fcqrFe4KIRIX(_3>hQv!T2;eM>Dd844o2{Z|Dc=P*qJB@20v!?@ zY~}ek@P6)e?3ebo4@jZ4hLTA{NP~0UqLu|3z3G*3%Ux`^G6lE~gKXb0Jjzx8_r$W1 zQhK-ucDLJoJ2zLu;!2Xm(tZovs$|(e;5iQk`9{gd;0k}Lgsc_c!5N_*>yX^980?)9 z;@emsMK3e7&hBd&uYd<^LZDp-p+_d7TQnYZgxhc z$pC+a8)i|oRI8zt6bkKJ$M^#{mR-K_9^T-XPwgf8SeWt+iax!=zr3iDur?a9QT0LlHTLNpmhsp z4P^{Vgael2&1$#|?xUKSz2nnx)~$A5TmO=<_h*nQ1MG#*ut1R|`ExjKjx|8}1wQ$r zHJ$fF^buo4!UQ<+pyl9ZxQ?x(<`^*tHXo74`EZv~D;B~h*v@D64Qt^(MJ@L<+-`;E zvGrikVHwzt!MhcXIYPCgVKO_P>)@shQGpt z94|3Dr{I54=+GaSd;z>?DH@Ov1QhfCSYe>Vcs#t9^LVu~z}tlfivfU+}*P zfILl;_LsuuBg({eaQiQQzNuISoGu3(A`1%O9tvqZ;d}FF0S0>&)#`e<mG3pYK(LL~D~uK<_* z%}hYSIL1N1p%7o^@h#j+hMVm`*hjSWN`NEbSZ=W}3)xk0hLWW>!-o|2_reJ%sXXL? zR{$3VAygLBaL&yoK92X5sS zIPu*Y(RZANyYD0Y&3YR3F-gK@LwoT-1aKXk$%R!}0S3>6QiVAZ2Kv=Vx7nM2ABwN9|o>$Ys7N|7B`nL0rn0u2jLn7aO0Vn zy`F>9Ybg-28b%&7gA)SVY#Q9HOyw?z6W>H>CEZ^KXLypWwcOht2SpOy4X1D~ z#eNYWTnL+w2;lD1CK&;iV&9|W@iMsK3e@|u(CYAs0!zX16zs|1?iDl7UcjK=+M8wj z1fFxT)$;uWH$Ld+JO4Dip8%K8*2|_h4i0B3mWfFqfvxob7 zyCxj;|I7G#vhd(pSbQNX)@EeEez;HZ@Gu;Akv04M58TR#Db;2O@C$rYaUXM>EIDJ% z9e;n^g9odv4FRb*n8@}yQ`dVN+{^a4ld{x507w05?d2+i)7V^|E(2T%=di=kr-Rk0nZ{ZNecoi~$pif94B}QZ5L^hx2 zNc&VczMf+W!;JuLg~c~V%oHpU?+37*PYU2!h{4&jmfCC`oN)t1)X(ss10J}+xH?c2 z%2(kwWrgHDI3&=T2b_ivtq=1JVuL@WP%8@(W8emsdWQ^wJ+|vH=%Ct}d6os&vZ7&1 zd_UYDAK)8g&V$WI)PhEM&L`F~Tbp2I*UL+A8p29EqB4qV#a$|G2*qj=43|rxCdmZc@76TzJ+)c4KF>=>ID* zIHBZe6Fd}R>HVIDyN_7g@n3<>M+n#jN3tLh$8XUv^}-3t`G8Y!)CueC%ujG;i?JB( z?f1nw+5-Fo1tzzCQzwXUJs=cIgzpug5d+tFZgi2n4GtMV1>#nf1lFoWjKq=>&$BXzTgMBE-XWO@FfQ3Ba+Pj3!38Fs7A7- zihxf(WNlWDgX=;9ZRQ4sDR2+x*N;&s#b7%N&gfxpd7zPk<@)V~oOfD;ae|_&+6OX!U61EtwjV;bPfym8|=O*=I8LNeygN>4~Lie+niF&;a`$5Ea1k;_Y>e8W*0J= z9@``gk`k1Y9q?X8dQMrHa^SSeNL$?qBOdeMi3k0?pScnO)x))y1bA=56+D@-Zg*jb zPff8+xc|@^m^}>-F}4=V4(xXtVcU&CuYZtjf0z-&Zn%x2kR~NQ0S72$d-zva$VX(% zrEtt${=Tw44(_2noFyYLz$D&$g!@eFll2WAI7zp)~|1QsfLQ?vzfHS#l0e;N)C7_TKg`Vt~q}#4rxdK|g7B zLD#}3IB1?IJ-h`@*=FtNUI_2KGt@UHUJi#dy6=LWZ zOT2K2wb*t99^^)fa9$8)_Y=4y$SRcQ;I==?E`u1JcZ$}4LMvhreTzX9Yh7tFfS_;qAs zJRGm=V|*K)xW-hZZ~>HfUp^uQ2jL#djG5x$-!eG&+-j|BjKrW=>7$b2ifPtX(#dc? ztAlBHCXzS@7FR`HO^YrHSrvSAnYBn?3!9G+;8D1@!OG(8uqT^M31&TQ#~`yMz_;^r zAME;;!MAKU-hsQ6>Br}A$FpqEmMz^`xUIxmx*GN!p(;A@ad3^Y9y;wi>fZn{JP~BX z=pGzYPq(h-dk}76v8)(>M2r^+59MPHT~V4t!{MfUl5N z!R<`w%sj7$W0XSpxCetF6eDJ@{|fA4hnD%m2e6x?CuYla5^mzIAM>uB@H12+>U{+< z662iF@Ibz`qw5-Yah0D>VVefWDgt;)F$hss?H9pKb}Po4;TolLc@BxTK3Bj15zIg-{T1vmx|G;?K)961icNxt{l?>zDc^5`{+xGcq2!(EEzCk;;I zw!#aj@&%mupclsurqiE^BfYaIQk|P5Q9ir4H1lbZ~5?=}r{?+*GrKn~TVDWi8(Hjb$ z2`8Z8&o=x2d<>$R*{v%bEP*RnXfacu6?UN>OppP-0>^e+Bc3C0-=h(>{TCQ5;Q*Yb zq?q;-g?69iek@$KQOx+{d?yQoNDkGXkOABer?EV^NAi5wd`NH_OBZxaDK6 z?Zi&R@Hx0wQQ*DycGPqx9dMCkIN;M6c zVu~jO;A%c$uav1uI7TUyjd1oa%y-(Q#M?1AiiTG-Bq9bc!s!RB!#BbA{C5Bcu@|2{ zHLLB{aM$QiU(0F#<-gsB!inp-npKvOi{R!h0Ulq=l!!q(`yM-_0Mp<>E){V~o(0D! z%D5HqStUib!I_GN_wR5#6OD=T{kP#(X6QSm0H4F@YX2W_mKd;Q%&fQZaNj-FV%fED z<~gen&VWzYt(_bWIGnAiy)wXZI72CfTjBn-!+blwcf*+;#lgE6ROr_F=2vi%62pLV z^aYt5A(ORa6g=>wb)@_%cvg?UZ^3pN+{L8boQUMW<|E2TKJ2ND3-x{rRpj}646+n~ zI^dmXZsKq(?DxWbiYloWw$?94*!~TtEBC^jg1c>2Uoz}hT2A)LWJ>=o`jz^Ztps=@ z4t6uiHph7P!F>-{`n=We0u@z;QFR+X@3}=rGyvo8}+YW zS=);FjsAQbclXF7PK5g>FvturQs727fc;CU(!-f>s%A{3L<;4>O*FOf(%uapMYUuW z;;q8{pIEe)eNBf4gA6YJamyFp5+1CwwhtYLYp+0uCGEe1%}3FEZtFl|v!0HE2WU#oBuj;RI4WZD z9C!~(kO3Lvhu|!w-miu`w))%Zr2UsVKjb4)Y!~)(d}rp+cXnZrqZEpd;W(BB%q06- zyr9^RW>x=eJ*#yx53hvJ_;X4S&qWqYh8wx>WR~PR;ge(0@CF<94%mEH@8oO27&IwU zt0iy(rPfT54X}B?xX@>AhjTdP*UGyhz)rYD(S;p`k52OUjdH((&)ghpJ0S%Q@<-uL zJ3PeuhQ1SnZl+RZ(|a|Xb=cbXI9X$(@(@t7X&7A$XC7cqVnZu^>imu+`1T&rv+Ite?KEQ)1yzJIc{ zIyfGVi?kY^o8dtwD!vP*W@BJJLgEMDNalKGDSilU`^;MLT?=<9qvmaJu_8bh-0~?U zUkdyo>{9H9;1fc_Eb?DetKdMo+8NeKjW{@`FUU91xJIzjpZ@{w9cgVx&VdJ}T1xi$ z@DRKF)9_!Ek#+DKR{72D{m-yiJ@jM<2PE-s3_2nLY!hW6c>^9$7FdtL6-o?F!%eqZ z`<+9Ah`@KjJ}rMd+|5<)X2Wy?9I%Sr#U!*4=ngpcO1`4)k_Ju;oJ_@Hq{I)wDZ$3- zpLk&@yw|I7WbqN~V>7A$Nq`98YYZ}| zHfFW+59U%N+JQ`IKLVb`sMwogxE~Ae|Hhi_-UL@G0hYt!gK_5C)~|7NIx+7XpSu+i z)ZmO&fxeq<6JbvU2dw7E048D3j9#x%avnUOwA~BgaOJwi$KWJ1B+W8LJK>3WET~HU zJKToA*;2rF;2I@`J`3T#fs76`8S;gIP`fQr*jC9(>S=59&N8@zP5QE__t@585T|U2+6fQ#SR1Um z;f$@QXk?NdgKf&@nf7L;)AKn0Ec5ir^$f8_;Km!n5ZjNqKg(vF-7?03;f9@IPx~8= zfqJ>dNH|ux_vk7(n?a{rdXNUU)9M_Rd^eo>87m~Rlotv69d=uv6nG)rprq(pc#ru} z3!(pi8iR`8hWWb3-LP%5HR3r1*ZpKQM1I3bp>nI~jfPVLt?70WoS~@QvS9Ox<3};v zCyWy)MD+DX?J6sN(0szq ze?7HQ8zr0!)4yJ+U1XTOw@SNIf22;kTEC`F({$%5JUv`%n1$$ftkNzN?k+bRhppn* z55fs%Ua120sVnd*ZaLxBFE@UL>9;M%$tfXP-bw^NAY}H+_gWq9j*2(D0=-;|ZLq+z zLV9mJ4lYLwukAIf@Zk7zWbxwEm0Fwk8i+_gxSV%mR%%fZ zX1XpYaTXQ0_2gP@Y*D z_6hB>E6Vd8bd;9QFK`r=R~8gimd`75+Vb3Gj{GvWtvlkTTr)y>-z<9qKSu4) z_zd|ITF~eTNKYE%Ru<$sNB~E1K}k`uTNqX3yY<_i&@T5Ug8I5vEk@sUP79Ajc41rP zDsdK6@`~Q1lj~Rw*C)1W@%C~0>ebq)356vtH}=k|d`C%HiPyvYa#wk!BhQtaKUer5 z1h1q>Bf2;F9Jwx+{>kIS_5OMiy|JFunpcmo+gtgypjA6xf3}rm+)y2TzTUA$3mjpI zQHA7{xn)Rm947(Qj{H1**miBU-mr<%GhqY%6>ry$=yO}CS+kq9v2lhl3yMqJ1>We0 z40H%@Dsr7(NgDr3)=YYXtZDiymG${%ZMuGKvzDlTv_<=aUbF>&-`PloY22dSu3vLc zbg;f}lNJ+LT0pe*T7dQ&$7nHe4 zLtB2aH&MN%=ZSSB!+Be@>-4TBEmOZ{EFRyng{m-elQukXZl2AZ>n&zs?t*H!w>*v0 zFW;m&^j$65MgNmm^K)JE3iLNywCjiGQ}Wy-y|+B+OIx(x>z)=`y{<-WWUQ3YOsUd> zvU%RL%%cqHQ?`rbZ`48qgt@+BtyZh&G-@d!qIDF`^xHRU2QDTKm2NY6O`iv!wgG(DzCyFM&Gw`_iHl{YinS8K^( z`FZmSWpgsDg%0BOjas68ynf?G%H+cvNu7fWqC+=s)?&iv7Cf9+o?GekzI)R;?FK#8 z9v!ZSBZ2;Z$NAJ-e)s)dwv@-Y!mqJZQ8Jnj~hQm>E}I8-OLh}H$F}SmGd}$P7{AB zAIGO=@n^aC^B3{PH}!N$lOILKck4<1iyzhEqC~^uZFxy0x5HV!po}yEz0yJi~!#+)7KF(sdO@&3v)EXhCrSRVmkSn_E^=iib%_q9_c`jlNJ+k14^?(RW!M zRf^V5zE4?~zKS4R1%;*sjnsoBj%gE;$z_|*S0S$tlJ(>rv{HX*)2`Atwo!7bchKw~ z*uk&EZDh*LJ17IY1ctY1cNmTbc97yp!fbXMJ$c>^O|wtvJpW0pHztrORGF)v|BCiR zq|rEvMwI4~7D|8i6)pD)I;YYCM_w+Y0py@HF$_w}%gPNdbQM(VgHLKv!J8cS_g~d6zN^aZEHU4yVr1cV6ZICMOl$BE_W92OTT2FHmWmopH}P_Av!T^-25YT_4D>=T1a7GX+;61`W*+g z<@!f&X;*h{eO>#&t_K~YWm)qNEm@B^q+Q!N=`}6We!i&AMv&f`$ZN9?XeUBx#pah3 zEC8p!r8Vg{9iqAX;GmY=S<|guVAluV)Ee|p@}n=*m%pjGI_tZ%^X+=WA+1K=_YbXD zpV6buh^2HCmX}G-NjfT%5m0GWQRki>Eh{R*8+~Wxe9Wtpll7NA(`I_6q&wW@^YV*@ z*<4X7XJuuNOG@!}r6qYKWfk*;mqqTm#(Q|-Fn;~#*~>^ zj@zeaiQjjnCtp876szf()2|<&BJ5{QNWONW_)(bWED%D>FDcWff2NHd@2o5Z@5;XJ z+6gpGwyIKr3vwTJ8Kt<&>7u(SB~ro?D5{v(dEMvQ_v1Qm92s5r`@kyNoI-v3{n0U< z-(^G}kM8_xPV`6dogXcaJ`&t{cy;t?|IRnpML%Nqi_FreZHT@&NPOC*wn%?)L-b`~ zV(GW3h>`CaeWS1wp9hTmL4OguAo1zHm>)Vn*$}KIj;Y&2z^AY+Qe#gAJ QHrl%c$LQJnn6rZa2Xx4!h5!Hn delta 197042 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%0Dld4K^|1$1bp9)T|SdAiY83*g8|$o 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

    >!D!SzAbu>2#zN)CJvyD+7Sk^bi%Ap(kmqTflH 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 diff --git a/resource/jetkvm_native.sha256 b/resource/jetkvm_native.sha256 index b540b94..ceba8b2 100644 --- a/resource/jetkvm_native.sha256 +++ b/resource/jetkvm_native.sha256 @@ -1 +1 @@ -4b925c7aa73d2e35a227833e806658cb17e1d25900611f93ed70b11ac9f1716d +6dabd0e657dd099280d9173069687786a4a8c9c25cf7f9e7ce2f940cab67c521 From 2aa7b8569fbfc097b6b9c0ee87fb1b7eeae6d098 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 14 May 2025 04:17:29 -0500 Subject: [PATCH 054/165] feat: Reset optionally reset USB HID in dev-deploy (#440) Adds `--reset-usb-hid` command to delete the configured USB HID device before running. --- dev_deploy.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dev_deploy.sh b/dev_deploy.sh index d0ccaf2..62e2c4f 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -24,6 +24,7 @@ show_help() { REMOTE_USER="root" REMOTE_PATH="/userdata/jetkvm/bin" SKIP_UI_BUILD=false +RESET_USB_HID_DEVICE=false LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}" # Parse command line arguments @@ -41,6 +42,10 @@ while [[ $# -gt 0 ]]; do SKIP_UI_BUILD=true shift ;; + --reset-usb-hid) + RESET_USB_HID_DEVICE=true + shift + ;; --help) show_help exit 0 @@ -74,6 +79,12 @@ ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" # Copy the binary to the remote host ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app +if [ "$RESET_USB_HID_DEVICE" = true ]; then + # Remove the old USB gadget configuration + ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" + ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" +fi + # Deploy and run the application on the remote host ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF set -e From 340babac24628649f958e6edf0e76546671c23c8 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Wed, 14 May 2025 17:25:56 +0200 Subject: [PATCH 055/165] feat(network): enhance network settings UI (#364) * feat(network): enhance network settings UI with domain management and improved layout - Added custom domain input and selection options for DHCP and local domains. - Improved layout for displaying network settings, including DHCP lease information and IPv6 addresses. - Refactored state management for network settings and added handlers for hostname and domain changes. - Updated the display of network settings to enhance user experience and accessibility. * Re-add save button * fix: add ConfirmDialog for renewing DHCP lease and improve network settings layout - Integrated ConfirmDialog component to confirm DHCP lease renewal. - Enhanced the layout of network settings, including better organization of IPv4 and IPv6 information. - Updated state management for displaying network settings and lease information. - Improved user experience with clearer descriptions and structured UI elements. * Fix lint errors * fix: useRef TS2554 --------- Co-authored-by: Siyuan Miao --- ui/src/components/Combobox.tsx | 105 ++- ui/src/components/ConfirmDialog.tsx | 34 +- ui/src/components/MacroForm.tsx | 103 +- ui/src/components/VideoOverlay.tsx | 6 +- ui/src/components/WebRTCVideo.tsx | 2 +- ui/src/hooks/stores.ts | 113 ++- ui/src/routes/devices.$id.settings.macros.tsx | 428 +++++---- .../routes/devices.$id.settings.network.tsx | 885 ++++++++++++------ 8 files changed, 1045 insertions(+), 631 deletions(-) diff --git a/ui/src/components/Combobox.tsx b/ui/src/components/Combobox.tsx index 8055043..661b77b 100644 --- a/ui/src/components/Combobox.tsx +++ b/ui/src/components/Combobox.tsx @@ -1,7 +1,14 @@ import { useRef } from "react"; import clsx from "clsx"; -import { Combobox as HeadlessCombobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react"; +import { + Combobox as HeadlessCombobox, + ComboboxInput, + ComboboxOption, + ComboboxOptions, +} from "@headlessui/react"; + import { cva } from "@/cva.config"; + import Card from "./Card"; export interface ComboboxOption { @@ -22,7 +29,7 @@ const comboboxVariants = cva({ type BaseProps = React.ComponentProps; -interface ComboboxProps extends Omit { +interface ComboboxProps extends Omit { displayValue: (option: ComboboxOption) => string; onInputChange: (option: string) => void; options: () => ComboboxOption[]; @@ -48,72 +55,68 @@ export function Combobox({ const classes = comboboxVariants({ size }); return ( - + {() => ( <> onInputChange(event.target.value)} - disabled={disabled} + ref={inputRef} + className={clsx( + classes, + + // General styling + "block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300", + + // Hover + "hover:bg-blue-50/80 active:bg-blue-100/60", + + // Dark mode + "dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700 dark:active:bg-slate-800/60", + + // Focus + "focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500", + + // Disabled + disabled && + "pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-slate-400/80 dark:disabled:hover:bg-slate-800", + )} + placeholder={disabled ? disabledMessage : placeholder} + displayValue={displayValue} + onChange={event => onInputChange(event.target.value)} + disabled={disabled} /> - + {options().length > 0 && ( - - {options().map((option) => ( - + {options().map(option => ( + {option.label} ))} )} - + {options().length === 0 && inputRef.current?.value && ( -

    -
    - {emptyMessage} -
    +
    +
    {emptyMessage}
    )} )} ); -} \ No newline at end of file +} diff --git a/ui/src/components/ConfirmDialog.tsx b/ui/src/components/ConfirmDialog.tsx index 57391e2..3771096 100644 --- a/ui/src/components/ConfirmDialog.tsx +++ b/ui/src/components/ConfirmDialog.tsx @@ -1,4 +1,9 @@ -import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from "@heroicons/react/24/outline"; +import { + ExclamationTriangleIcon, + CheckCircleIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; + import { cx } from "@/cva.config"; import { Button } from "@/components/Button"; import Modal from "@/components/Modal"; @@ -42,12 +47,15 @@ const variantConfig = { iconBgClass: "bg-blue-100", buttonTheme: "primary", }, -} as Record; + } +>; export function ConfirmDialog({ open, @@ -65,13 +73,18 @@ export function ConfirmDialog({ return (
    -
    +
    -
    +
    -
    +

    {title}

    @@ -83,12 +96,7 @@ export function ConfirmDialog({
    {cancelText && ( -
    ); -} \ No newline at end of file +} diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 0d71dcd..ada8f20 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -3,11 +3,11 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; import { LuPlay } from "react-icons/lu"; +import { BsMouseFill } from "react-icons/bs"; import { Button, LinkButton } from "@components/Button"; import LoadingSpinner from "@components/LoadingSpinner"; import Card, { GridCard } from "@components/Card"; -import { BsMouseFill } from "react-icons/bs"; interface OverlayContentProps { children: React.ReactNode; @@ -242,8 +242,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) { Ensure source device is powered on and outputting a signal
  • - If using an adapter, ensure it's compatible and - functioning correctly + If using an adapter, ensure it's compatible and functioning + correctly
  • diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 8ebe257..cea39da 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -151,7 +151,7 @@ export default function WebRTCVideo() { const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock"); if (isKeyboardLockGranted) { if ("keyboard" in navigator) { - // @ts-ignore + // @ts-expect-error - keyboard lock is not supported in all browsers await navigator.keyboard.lock(); } } diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index c100d88..5e066ca 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -1,6 +1,11 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; -import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros"; + +import { + MAX_STEPS_PER_MACRO, + MAX_TOTAL_MACROS, + MAX_KEYS_PER_STEP, +} from "@/constants/macros"; // Define the JsonRpc types for better type checking interface JsonRpcResponse { @@ -571,12 +576,12 @@ export interface UpdateState { setOtaState: (state: UpdateState["otaState"]) => void; setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void; modalView: - | "loading" - | "updating" - | "upToDate" - | "updateAvailable" - | "updateCompleted" - | "error"; + | "loading" + | "updating" + | "upToDate" + | "updateAvailable" + | "updateCompleted" + | "error"; setModalView: (view: UpdateState["modalView"]) => void; setUpdateErrorMessage: (errorMessage: string) => void; updateErrorMessage: string | null; @@ -640,12 +645,12 @@ export const useUsbConfigModalStore = create(set => ({ interface LocalAuthModalState { modalView: - | "createPassword" - | "deletePassword" - | "updatePassword" - | "creationSuccess" - | "deleteSuccess" - | "updateSuccess"; + | "createPassword" + | "deletePassword" + | "updatePassword" + | "creationSuccess" + | "deleteSuccess" + | "updateSuccess"; setModalView: (view: LocalAuthModalState["modalView"]) => void; } @@ -726,12 +731,23 @@ export interface NetworkState { setDhcpLeaseExpiry: (expiry: Date) => void; } - -export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown"; +export type IPv6Mode = + | "disabled" + | "slaac" + | "dhcpv6" + | "slaac_and_dhcpv6" + | "static" + | "link_local" + | "unknown"; export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; export type LLDPMode = "disabled" | "basic" | "all" | "unknown"; export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; -export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown"; +export type TimeSyncMode = + | "ntp_only" + | "ntp_and_http" + | "http_only" + | "custom" + | "unknown"; export interface NetworkSettings { hostname: string; @@ -756,7 +772,7 @@ export const useNetworkStateStore = create((set, get) => ({ lease.lease_expiry = expiry; set({ dhcp_lease: lease }); - } + }, })); export interface KeySequenceStep { @@ -778,8 +794,20 @@ export interface MacrosState { initialized: boolean; loadMacros: () => Promise; saveMacros: (macros: KeySequence[]) => Promise; - sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void) | null; - setSendFn: (sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void)) => void; + sendFn: + | (( + method: string, + params: unknown, + callback?: ((resp: JsonRpcResponse) => void) | undefined, + ) => void) + | null; + setSendFn: ( + sendFn: ( + method: string, + params: unknown, + callback?: ((resp: JsonRpcResponse) => void) | undefined, + ) => void, + ) => void; } export const generateMacroId = () => { @@ -792,7 +820,7 @@ export const useMacrosStore = create((set, get) => ({ initialized: false, sendFn: null, - setSendFn: (sendFn) => { + setSendFn: sendFn => { set({ sendFn }); }, @@ -809,7 +837,7 @@ export const useMacrosStore = create((set, get) => ({ try { await new Promise((resolve, reject) => { - sendFn("getKeyboardMacros", {}, (response) => { + sendFn("getKeyboardMacros", {}, response => { if (response.error) { console.error("Error loading macros:", response.error); reject(new Error(response.error.message)); @@ -829,7 +857,7 @@ export const useMacrosStore = create((set, get) => ({ set({ macros: sortedMacros, - initialized: true + initialized: true, }); resolve(); @@ -856,15 +884,23 @@ export const useMacrosStore = create((set, get) => ({ for (const macro of macros) { if (macro.steps.length > MAX_STEPS_PER_MACRO) { - console.error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`); - throw new Error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`); + console.error( + `Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`, + ); + throw new Error( + `Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`, + ); } for (let i = 0; i < macro.steps.length; i++) { const step = macro.steps[i]; if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) { - console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); - throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`); + console.error( + `Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`, + ); + throw new Error( + `Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`, + ); } } } @@ -874,20 +910,25 @@ export const useMacrosStore = create((set, get) => ({ try { const macrosWithSortOrder = macros.map((macro, index) => ({ ...macro, - sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index + sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index, })); - const response = await new Promise((resolve) => { - sendFn("setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, (response) => { - resolve(response); - }); + const response = await new Promise(resolve => { + sendFn( + "setKeyboardMacros", + { params: { macros: macrosWithSortOrder } }, + response => { + resolve(response); + }, + ); }); if (response.error) { console.error("Error saving macros:", response.error); - const errorMessage = typeof response.error.data === 'string' - ? response.error.data - : response.error.message || "Failed to save macros"; + const errorMessage = + typeof response.error.data === "string" + ? response.error.data + : response.error.message || "Failed to save macros"; throw new Error(errorMessage); } @@ -899,5 +940,5 @@ export const useMacrosStore = create((set, get) => ({ } finally { set({ loading: false }); } - } -})); \ No newline at end of file + }, +})); diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx index f809f57..ba1a2ba 100644 --- a/ui/src/routes/devices.$id.settings.macros.tsx +++ b/ui/src/routes/devices.$id.settings.macros.tsx @@ -1,6 +1,15 @@ import { useEffect, Fragment, useMemo, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; -import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu"; +import { + LuPenLine, + LuCopy, + LuMoveRight, + LuCornerDownRight, + LuArrowUp, + LuArrowDown, + LuTrash2, + LuCommand, +} from "react-icons/lu"; import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; import { SettingsPageHeader } from "@/components/SettingsPageheader"; @@ -26,10 +35,10 @@ export default function SettingsMacrosRoute() { const [actionLoadingId, setActionLoadingId] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [macroToDelete, setMacroToDelete] = useState(null); - - const isMaxMacrosReached = useMemo(() => - macros.length >= MAX_TOTAL_MACROS, - [macros.length] + + const isMaxMacrosReached = useMemo( + () => macros.length >= MAX_TOTAL_MACROS, + [macros.length], ); useEffect(() => { @@ -38,75 +47,83 @@ export default function SettingsMacrosRoute() { } }, [initialized, loadMacros]); - const handleDuplicateMacro = useCallback(async (macro: KeySequence) => { - if (!macro?.id || !macro?.name) { - notifications.error("Invalid macro data"); - return; - } - - if (isMaxMacrosReached) { - notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`); - return; - } - - setActionLoadingId(macro.id); - - const newMacroCopy: KeySequence = { - ...JSON.parse(JSON.stringify(macro)), - id: generateMacroId(), - name: `${macro.name} ${COPY_SUFFIX}`, - sortOrder: macros.length + 1, - }; - - try { - await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); - notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`); - } catch (error: unknown) { - if (error instanceof Error) { - notifications.error(`Failed to duplicate macro: ${error.message}`); - } else { - notifications.error("Failed to duplicate macro"); + const handleDuplicateMacro = useCallback( + async (macro: KeySequence) => { + if (!macro?.id || !macro?.name) { + notifications.error("Invalid macro data"); + return; } - } finally { - setActionLoadingId(null); - } - }, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]); - const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => { - if (!Array.isArray(macros) || macros.length === 0) { - notifications.error("No macros available"); - return; - } - - const newIndex = direction === 'up' ? index - 1 : index + 1; - if (newIndex < 0 || newIndex >= macros.length) return; - - setActionLoadingId(macroId); - - try { - const newMacros = [...macros]; - [newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]]; - const updatedMacros = normalizeSortOrders(newMacros); - - await saveMacros(updatedMacros); - notifications.success("Macro order updated successfully"); - } catch (error: unknown) { - if (error instanceof Error) { - notifications.error(`Failed to reorder macros: ${error.message}`); - } else { - notifications.error("Failed to reorder macros"); + if (isMaxMacrosReached) { + notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`); + return; } - } finally { - setActionLoadingId(null); - } - }, [macros, saveMacros, setActionLoadingId]); + + setActionLoadingId(macro.id); + + const newMacroCopy: KeySequence = { + ...JSON.parse(JSON.stringify(macro)), + id: generateMacroId(), + name: `${macro.name} ${COPY_SUFFIX}`, + sortOrder: macros.length + 1, + }; + + try { + await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); + notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to duplicate macro: ${error.message}`); + } else { + notifications.error("Failed to duplicate macro"); + } + } finally { + setActionLoadingId(null); + } + }, + [isMaxMacrosReached, macros, saveMacros, setActionLoadingId], + ); + + const handleMoveMacro = useCallback( + async (index: number, direction: "up" | "down", macroId: string) => { + if (!Array.isArray(macros) || macros.length === 0) { + notifications.error("No macros available"); + return; + } + + const newIndex = direction === "up" ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= macros.length) return; + + setActionLoadingId(macroId); + + try { + const newMacros = [...macros]; + [newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]]; + const updatedMacros = normalizeSortOrders(newMacros); + + await saveMacros(updatedMacros); + notifications.success("Macro order updated successfully"); + } catch (error: unknown) { + if (error instanceof Error) { + notifications.error(`Failed to reorder macros: ${error.message}`); + } else { + notifications.error("Failed to reorder macros"); + } + } finally { + setActionLoadingId(null); + } + }, + [macros, saveMacros, setActionLoadingId], + ); const handleDeleteMacro = useCallback(async () => { if (!macroToDelete?.id) return; setActionLoadingId(macroToDelete.id); try { - const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id)); + const updatedMacros = normalizeSortOrders( + macros.filter(m => m.id !== macroToDelete.id), + ); await saveMacros(updatedMacros); notifications.success(`Macro "${macroToDelete.name}" deleted successfully`); setShowDeleteConfirm(false); @@ -122,135 +139,168 @@ export default function SettingsMacrosRoute() { } }, [macroToDelete, macros, saveMacros]); - const MacroList = useMemo(() => ( -
    - {macros.map((macro, index) => ( - -
    -
    -
    + const MacroList = useMemo( + () => ( +
    + {macros.map((macro, index) => ( + +
    +
    +
    -
    -

    - {macro.name} -

    -

    - - {macro.steps.map((step, stepIndex) => { - const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight; +

    +

    + {macro.name} +

    +

    + + {macro.steps.map((step, stepIndex) => { + const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight; - return ( - - - - {(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? ( - <> - {Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => ( - - - {modifierDisplayMap[modifier] || modifier} - - {idx < step.modifiers.length - 1 && ( - + + return ( + + + + {(Array.isArray(step.modifiers) && + step.modifiers.length > 0) || + (Array.isArray(step.keys) && step.keys.length > 0) ? ( + <> + {Array.isArray(step.modifiers) && + step.modifiers.map((modifier, idx) => ( + + + {modifierDisplayMap[modifier] || modifier} + + {idx < step.modifiers.length - 1 && ( + + {" "} + +{" "} + + )} + + ))} + + {Array.isArray(step.modifiers) && + step.modifiers.length > 0 && + Array.isArray(step.keys) && + step.keys.length > 0 && ( + + {" "} + +{" "} + )} - - ))} - {Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && ( - + - )} - - {Array.isArray(step.keys) && step.keys.map((key, idx) => ( - - - {keyDisplayMap[key] || key} - - {idx < step.keys.length - 1 && ( - + - )} - - ))} - - ) : ( - Delay only - )} - {step.delay !== DEFAULT_DELAY && ( - ({step.delay}ms) - )} + {Array.isArray(step.keys) && + step.keys.map((key, idx) => ( + + + {keyDisplayMap[key] || key} + + {idx < step.keys.length - 1 && ( + + {" "} + +{" "} + + )} + + ))} + + ) : ( + + Delay only + + )} + {step.delay !== DEFAULT_DELAY && ( + + ({step.delay}ms) + + )} + - - ); - })} - -

    -
    + ); + })} + +

    +
    -
    -
    -
    - - ))} + + ))} - { - setShowDeleteConfirm(false); - setMacroToDelete(null); - }} - title="Delete Macro" - description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`} - variant="danger" - confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"} - onConfirm={handleDeleteMacro} - isConfirming={actionLoadingId === macroToDelete?.id} - /> -
    - ), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]); + { + setShowDeleteConfirm(false); + setMacroToDelete(null); + }} + title="Delete Macro" + description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`} + variant="danger" + confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"} + onConfirm={handleDeleteMacro} + isConfirming={actionLoadingId === macroToDelete?.id} + /> +
    + ), + [ + macros, + showDeleteConfirm, + macroToDelete?.name, + macroToDelete?.id, + actionLoadingId, + handleDeleteMacro, + handleMoveMacro, + handleDuplicateMacro, + navigate, + ], + ); return (
    @@ -259,7 +309,7 @@ export default function SettingsMacrosRoute() { title="Keyboard Macros" description={`Combine keystrokes into a single action for faster workflows.`} /> - { macros.length > 0 && ( + {macros.length > 0 && (
    ); diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 59d52ef..f48a7bd 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -1,18 +1,30 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { ArrowPathIcon } from "@heroicons/react/24/outline"; -import { SelectMenuBasic } from "../components/SelectMenuBasic"; -import { SettingsPageHeader } from "../components/SettingsPageheader"; - -import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores"; +import { + IPv4Mode, + IPv6Mode, + LLDPMode, + mDNSMode, + NetworkSettings, + NetworkState, + TimeSyncMode, + useNetworkStateStore, +} from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { Button } from "@components/Button"; import { GridCard } from "@components/Card"; import InputField from "@components/InputField"; -import { SettingsItem } from "./devices.$id.settings"; -import dayjs from 'dayjs'; -import relativeTime from 'dayjs/plugin/relativeTime'; +import { SettingsPageHeader } from "../components/SettingsPageheader"; +import { SelectMenuBasic } from "../components/SelectMenuBasic"; +import Fieldset from "../components/Fieldset"; +import { ConfirmDialog } from "../components/ConfirmDialog"; + +import { SettingsItem } from "./devices.$id.settings"; dayjs.extend(relativeTime); @@ -25,13 +37,9 @@ const defaultNetworkSettings: NetworkSettings = { lldp_tx_tlvs: [], mdns_mode: "unknown", time_sync_mode: "unknown", -} +}; export function LifeTimeLabel({ lifetime }: { lifetime: string }) { - if (lifetime == "") { - return N/A; - } - const [remaining, setRemaining] = useState(null); useEffect(() => { @@ -43,46 +51,87 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { return () => clearInterval(interval); }, [lifetime]); - return <> - {dayjs(lifetime).format()} - {remaining && <> - {" "} - ({remaining}) - - } - + return ( + <> + {dayjs(lifetime).format("YYYY-MM-DD HH:mm")} + {remaining && ( + <> + {" "} + + ({remaining}) + + + )} + + ); } export default function SettingsNetworkRoute() { const [send] = useJsonRpc(); - const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]); + const [networkState, setNetworkState] = useNetworkStateStore(state => [ + state, + state.setNetworkState, + ]); + + const [networkSettings, setNetworkSettings] = + useState(defaultNetworkSettings); + + // We use this to determine whether the settings have changed + const firstNetworkSettings = useRef(undefined); - const [networkSettings, setNetworkSettings] = useState(defaultNetworkSettings); const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); + const [customDomain, setCustomDomain] = useState(""); + const [selectedDomainOption, setSelectedDomainOption] = useState("dhcp"); + + useEffect(() => { + if (networkSettings.domain && networkSettingsLoaded) { + // Check if the domain is one of the predefined options + const predefinedOptions = ["dhcp", "local"]; + if (predefinedOptions.includes(networkSettings.domain)) { + setSelectedDomainOption(networkSettings.domain); + } else { + setSelectedDomainOption("custom"); + setCustomDomain(networkSettings.domain); + } + } + }, [networkSettings.domain, networkSettingsLoaded]); + const getNetworkSettings = useCallback(() => { setNetworkSettingsLoaded(false); send("getNetworkSettings", {}, resp => { if ("error" in resp) return; console.log(resp.result); setNetworkSettings(resp.result as NetworkSettings); + + if (!firstNetworkSettings.current) { + firstNetworkSettings.current = resp.result as NetworkSettings; + } setNetworkSettingsLoaded(true); }); }, [send]); - const setNetworkSettingsRemote = useCallback((settings: NetworkSettings) => { - setNetworkSettingsLoaded(false); - send("setNetworkSettings", { settings }, resp => { - if ("error" in resp) { - notifications.error("Failed to save network settings: " + (resp.error.data ? resp.error.data : resp.error.message)); + const setNetworkSettingsRemote = useCallback( + (settings: NetworkSettings) => { + setNetworkSettingsLoaded(false); + send("setNetworkSettings", { settings }, resp => { + if ("error" in resp) { + notifications.error( + "Failed to save network settings: " + + (resp.error.data ? resp.error.data : resp.error.message), + ); + setNetworkSettingsLoaded(true); + return; + } + // We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed + firstNetworkSettings.current = resp.result as NetworkSettings; + setNetworkSettings(resp.result as NetworkSettings); setNetworkSettingsLoaded(true); - return; - } - setNetworkSettings(resp.result as NetworkSettings); - setNetworkSettingsLoaded(true); - notifications.success("Network settings saved"); - }); - }, [send]); + notifications.success("Network settings saved"); + }); + }, + [send], + ); const getNetworkState = useCallback(() => { send("getNetworkState", {}, resp => { @@ -90,7 +139,7 @@ export default function SettingsNetworkRoute() { console.log(resp.result); setNetworkState(resp.result as NetworkState); }); - }, [send]); + }, [send, setNetworkState]); const handleRenewLease = useCallback(() => { send("renewDHCPLease", {}, resp => { @@ -131,278 +180,520 @@ export default function SettingsNetworkRoute() { setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode }); }; - const filterUnknown = useCallback((options: { value: string; label: string; }[]) => { - if (!networkSettingsLoaded) return options; - return options.filter(option => option.value !== "unknown"); - }, [networkSettingsLoaded]); + const handleHostnameChange = (value: string) => { + setNetworkSettings({ ...networkSettings, hostname: value }); + }; + + const handleDomainChange = (value: string) => { + setNetworkSettings({ ...networkSettings, domain: value }); + }; + + const handleDomainOptionChange = (value: string) => { + setSelectedDomainOption(value); + if (value !== "custom") { + handleDomainChange(value); + } + }; + + const handleCustomDomainChange = (value: string) => { + setCustomDomain(value); + handleDomainChange(value); + }; + + const filterUnknown = useCallback( + (options: { value: string; label: string }[]) => { + if (!networkSettingsLoaded) return options; + return options.filter(option => option.value !== "unknown"); + }, + [networkSettingsLoaded], + ); + + const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false); return ( -
    - -
    - } - > - - {networkState?.mac_address} - - -
    -
    - - Hostname for the device -
    - - Leave blank for default - - - } - > - { - setNetworkSettings({ ...networkSettings, hostname: e.target.value }); - }} - disabled={!networkSettingsLoaded} - /> -
    -
    -
    - - Domain for the device -
    - - Leave blank to use DHCP provided domain, if there is no domain, use local - - - } - > - { - setNetworkSettings({ ...networkSettings, domain: e.target.value }); - }} - disabled={!networkSettingsLoaded} - /> -
    -
    -
    - - handleIpv4ModeChange(e.target.value)} - disabled={!networkSettingsLoaded} - options={filterUnknown([ - { value: "dhcp", label: "DHCP" }, - // { value: "static", label: "Static" }, - ])} - /> - - {networkState?.dhcp_lease && ( - -
    -
    -
    -

    - Current DHCP Lease -

    -
    -
      - {networkState?.dhcp_lease?.ip &&
    • IP: {networkState?.dhcp_lease?.ip}
    • } - {networkState?.dhcp_lease?.netmask &&
    • Subnet: {networkState?.dhcp_lease?.netmask}
    • } - {networkState?.dhcp_lease?.broadcast &&
    • Broadcast: {networkState?.dhcp_lease?.broadcast}
    • } - {networkState?.dhcp_lease?.ttl &&
    • TTL: {networkState?.dhcp_lease?.ttl}
    • } - {networkState?.dhcp_lease?.mtu &&
    • MTU: {networkState?.dhcp_lease?.mtu}
    • } - {networkState?.dhcp_lease?.hostname &&
    • Hostname: {networkState?.dhcp_lease?.hostname}
    • } - {networkState?.dhcp_lease?.domain &&
    • Domain: {networkState?.dhcp_lease?.domain}
    • } - {networkState?.dhcp_lease?.routers &&
    • Gateway: {networkState?.dhcp_lease?.routers.join(", ")}
    • } - {networkState?.dhcp_lease?.dns &&
    • DNS: {networkState?.dhcp_lease?.dns.join(", ")}
    • } - {networkState?.dhcp_lease?.ntp_servers &&
    • NTP Servers: {networkState?.dhcp_lease?.ntp_servers.join(", ")}
    • } - {networkState?.dhcp_lease?.server_id &&
    • Server ID: {networkState?.dhcp_lease?.server_id}
    • } - {networkState?.dhcp_lease?.bootp_next_server &&
    • BootP Next Server: {networkState?.dhcp_lease?.bootp_next_server}
    • } - {networkState?.dhcp_lease?.bootp_server_name &&
    • BootP Server Name: {networkState?.dhcp_lease?.bootp_server_name}
    • } - {networkState?.dhcp_lease?.bootp_file &&
    • Boot File: {networkState?.dhcp_lease?.bootp_file}
    • } - {networkState?.dhcp_lease?.lease_expiry &&
    • - Lease Expiry: -
    • } - {/* {JSON.stringify(networkState?.dhcp_lease)} */} -
    -
    -
    -
    -
    -
    + <> +
    + +
    + + + +
    +
    + +
    +
    + { + handleHostnameChange(e.target.value); + }} + />
    - - )} -
    -
    - - +
    + +
    +
    + +
    + handleDomainOptionChange(e.target.value)} + options={[ + { value: "dhcp", label: "DHCP provided" }, + { value: "local", label: ".local" }, + { value: "custom", label: "Custom" }, + ]} + /> +
    +
    + {selectedDomainOption === "custom" && ( +
    + setCustomDomain(e.target.value)} + /> +
    + )} +
    +
    + + handleMdnsModeChange(e.target.value)} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "auto", label: "Auto" }, + { value: "ipv4_only", label: "IPv4 only" }, + { value: "ipv6_only", label: "IPv6 only" }, + ])} + /> + +
    + +
    + + { + handleTimeSyncModeChange(e.target.value); + }} + options={filterUnknown([ + { value: "unknown", label: "..." }, + // { value: "auto", label: "Auto" }, + { value: "ntp_only", label: "NTP only" }, + { value: "ntp_and_http", label: "NTP and HTTP" }, + { value: "http_only", label: "HTTP only" }, + // { value: "custom", label: "Custom" }, + ])} + /> + +
    + +
    +
    +
    +
    + )} +
    +
    + + handleIpv6ModeChange(e.target.value)} + options={filterUnknown([ + // { value: "disabled", label: "Disabled" }, + { value: "slaac", label: "SLAAC" }, + // { value: "dhcpv6", label: "DHCPv6" }, + // { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" }, + // { value: "static", label: "Static" }, + // { value: "link_local", label: "Link-local only" }, + ])} + /> + + {networkState?.ipv6_addresses && ( + +
    +

    IPv6 Information

    -
    -
    -

    - IPv6 Link-local -

    -

    - {networkState?.ipv6_link_local} -

    -
    -
    -

    - IPv6 Addresses -

    -
      - {networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => ( -
    • - {addr.address} - {addr.valid_lifetime && <> -
      - - valid_lft: {" "} - - - - } - {addr.preferred_lifetime && <> -
      - - pref_lft: {" "} - - - - } -
    • - ))} -
    -
    + +
    + {networkState?.dhcp_lease?.ip && ( +
    + + Link-local + + + {networkState?.ipv6_link_local} + +
    + )} +
    + +
    + {networkState?.ipv6_addresses && + networkState?.ipv6_addresses.length > 0 && ( +
    +

    IPv6 Addresses

    + {networkState.ipv6_addresses.map(addr => ( +
    +
    +
    + + Address + + + {addr.address} + +
    + + {addr.valid_lifetime && ( +
    + + Valid Lifetime + + + {addr.valid_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
    + )} + {addr.preferred_lifetime && ( +
    + + Preferred Lifetime + + + {addr.preferred_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
    + )} +
    +
    + ))} +
    + )}
    -
    -
    - )} -
    -
    - - handleLldpModeChange(e.target.value)} - disabled={!networkSettingsLoaded} - options={filterUnknown([ - { value: "disabled", label: "Disabled" }, - { value: "basic", label: "Basic" }, - { value: "all", label: "All" }, - ])} - /> - -
    -
    - - handleMdnsModeChange(e.target.value)} - disabled={!networkSettingsLoaded} - options={filterUnknown([ - { value: "disabled", label: "Disabled" }, - { value: "auto", label: "Auto" }, - { value: "ipv4_only", label: "IPv4 only" }, - { value: "ipv6_only", label: "IPv6 only" }, - ])} - /> - -
    -
    - - handleTimeSyncModeChange(e.target.value)} - disabled={!networkSettingsLoaded} - options={filterUnknown([ - { value: "unknown", label: "..." }, - // { value: "auto", label: "Auto" }, - { value: "ntp_only", label: "NTP only" }, - { value: "ntp_and_http", label: "NTP and HTTP" }, - { value: "http_only", label: "HTTP only" }, - // { value: "custom", label: "Custom" }, - ])} - /> - -
    -
    -
    -
    + + )} +
    +
    + + handleLldpModeChange(e.target.value)} + options={filterUnknown([ + { value: "disabled", label: "Disabled" }, + { value: "basic", label: "Basic" }, + { value: "all", label: "All" }, + ])} + /> + +
    + + setShowRenewLeaseConfirm(false)} + title="Renew DHCP Lease" + description="This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process." + variant="danger" + confirmText="Renew Lease" + onConfirm={() => { + handleRenewLease(); + setShowRenewLeaseConfirm(false); + }} + /> + ); } From 7ccb8e617cab0725492fc7903757744b50bd6b71 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Thu, 15 May 2025 07:21:03 -0500 Subject: [PATCH 056/165] chore: Upgrade UI vite and tailwind packages (#443) * chore: Upgrade UI vite and tailwind packages Vite 5.2.0 -> 6.3.5 @vitejs/plugin-basic-ssl 1.2.0 -> 2.0.0 cva: 1.0.0-beta.1 -> 1.0.0-beta.3 focus-trap-react 10.2.3 -> 11.0.3 framer-motion 11.15.0 -> 12.11.0 @tailwindcss/postcss 4.1.6 @tailwindcss/vite 4.1.6 tailwind 3.4.17 -> 4.1.6 tailwind-merge 2.5.5 -> 3.3.0 Minor updates: @headlessui/react 2.2.2 -> 2.2.3 @types/react 19.1.3 -> 19.1.4 @types/react-dom 19.1.3 -> 19.1.5 @typescript-eslint/eslint-plugin 8.32.0 -> 8.32.1 @typescript-eslint/parser 8.32.0 -> 8.32.1 react-simple-keyboard 3.8.71 -> 3.8.72 The new version of vite required an Node 22.15 (since that's current LTS and node 21.x is EOL) The changes to css due to the tailwind 3 to 4 upgrade were done following [the upgrade guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3) Done in this order (important): `shadow-sm` -> `shadow-xs` `shadow` -> `shadown-sm` `rounded` -> `rounded-sm` `outline-none` -> `outline-hidden` `32rem_32rem_at_center` -> `center_at_32rem_32rem` (revised order of gradient props) `ring-1 ring-black ring-opacity-5` -> `ring-1 ring-black/50` `flex-shrink-0` -> `shrink-0` `flex-grow-0` -> `grow-0` `outline outline-1` -> `outline-1` ALSO removed the **extra** `opacity-0` on the video element (trips up latest tailwind causing the video to be invisible) FocusTrap is now not exported as the default, so change those imports headlessui's Menu completely changed, so upgrade to the new syntax which necessitated a reorganization of the Header.tsx to enable the "menu" to still work * Update eslint config and fix errors --- ui/.eslintrc.cjs | 66 - ui/eslint.config.cjs | 93 + ui/package-lock.json | 2790 ++++++++++------- ui/package.json | 34 +- ui/postcss.config.js | 1 - ui/src/components/Button.tsx | 22 +- ui/src/components/Card.tsx | 2 +- ui/src/components/Checkbox.tsx | 2 +- ui/src/components/Combobox.tsx | 2 +- ui/src/components/GridBackground.tsx | 2 +- ui/src/components/Header.tsx | 156 +- ui/src/components/InputField.tsx | 2 +- ui/src/components/KvmCard.tsx | 2 +- ui/src/components/LoadingSpinner.tsx | 2 +- ui/src/components/SelectMenuBasic.tsx | 4 +- ui/src/components/StepCounter.tsx | 2 +- ui/src/components/TextArea.tsx | 4 +- ui/src/components/VideoOverlay.tsx | 4 +- ui/src/components/WebRTCVideo.tsx | 6 +- .../components/extensions/ATXPowerControl.tsx | 2 +- .../components/extensions/DCPowerControl.tsx | 2 +- .../components/extensions/SerialConsole.tsx | 2 +- .../components/popovers/ExtensionPopover.tsx | 4 +- ui/src/components/popovers/MountPopover.tsx | 4 +- ui/src/components/popovers/PasteModal.tsx | 4 +- .../popovers/WakeOnLan/AddDeviceForm.tsx | 4 +- .../popovers/WakeOnLan/DeviceList.tsx | 4 +- .../popovers/WakeOnLan/EmptyStateCard.tsx | 4 +- ui/src/components/sidebar/connectionStats.tsx | 2 +- ui/src/index.css | 9 +- ui/src/main.tsx | 15 +- ui/src/routes/devices.$id.deregister.tsx | 1 + ui/src/routes/devices.$id.mount.tsx | 38 +- ui/src/routes/devices.$id.rename.tsx | 1 + .../devices.$id.settings.access._index.tsx | 4 +- ui/src/routes/devices.$id.settings.macros.tsx | 4 +- .../routes/devices.$id.settings.network.tsx | 17 +- ui/src/routes/devices.$id.settings.tsx | 11 +- ui/src/routes/devices.$id.tsx | 4 +- ui/src/routes/devices.tsx | 14 +- ui/src/routes/welcome-local.mode.tsx | 12 +- ui/src/routes/welcome-local.password.tsx | 12 +- ui/src/routes/welcome-local.tsx | 10 +- ui/vite.config.ts | 7 +- 44 files changed, 2006 insertions(+), 1381 deletions(-) delete mode 100644 ui/.eslintrc.cjs create mode 100644 ui/eslint.config.cjs diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs deleted file mode 100644 index 568fbd9..0000000 --- a/ui/.eslintrc.cjs +++ /dev/null @@ -1,66 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/stylistic", - "plugin:react-hooks/recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:import/recommended", - "prettier", - ], - ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"], - parser: "@typescript-eslint/parser", - plugins: ["react-refresh"], - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: ["./tsconfig.json", "./tsconfig.node.json"], - tsconfigRootDir: __dirname, - }, - rules: { - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - "import/order": [ - "error", - { - /** - * @description - * - * This keeps imports separate from one another, ensuring that imports are separated - * by their relative groups. As you move through the groups, imports become closer - * to the current file. - * - * @example - * ``` - * import fs from 'fs'; - * - * import package from 'npm-package'; - * - * import xyz from '~/project-file'; - * - * import index from '../'; - * - * import sibling from './foo'; - * ``` - */ - groups: ["builtin", "external", "internal", "parent", "sibling"], - "newlines-between": "always", - }, - ], - }, - settings: { - "import/resolver": { - alias: { - map: [ - ["@components", "./src/components"], - ["@routes", "./src/routes"], - ["@assets", "./src/assets"], - ["@", "./src"], - ], - extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], - }, - }, - }, -}; diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs new file mode 100644 index 0000000..a6c0c1f --- /dev/null +++ b/ui/eslint.config.cjs @@ -0,0 +1,93 @@ +const { + defineConfig, + globalIgnores, +} = require("eslint/config"); + +const globals = require("globals"); + +const { + fixupConfigRules, +} = require("@eslint/compat"); + +const tsParser = require("@typescript-eslint/parser"); +const reactRefresh = require("eslint-plugin-react-refresh"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = defineConfig([{ + languageOptions: { + globals: { + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.node.json"], + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true + } + }, + }, + + extends: fixupConfigRules(compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/stylistic", + "plugin:react-hooks/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:import/recommended", + "prettier", + )), + + plugins: { + "react-refresh": reactRefresh, + }, + + rules: { + "react-refresh/only-export-components": ["warn", { + allowConstantExport: true, + }], + + "import/order": ["error", { + groups: ["builtin", "external", "internal", "parent", "sibling"], + "newlines-between": "always", + }], + }, + + settings: { + "react": { + "version": "detect" + }, + "import/resolver": { + alias: { + map: [ + ["@components", "./src/components"], + ["@routes", "./src/routes"], + ["@assets", "./src/assets"], + ["@", "./src"], + ], + + extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], + }, + }, + }, +}, globalIgnores([ + "**/dist", + "**/.eslintrc.cjs", + "**/tailwind.config.js", + "**/postcss.config.js", +])]); diff --git a/ui/package-lock.json b/ui/package-lock.json index d3c964b..7817253 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,21 +8,21 @@ "name": "kvm-ui", "version": "0.0.0", "dependencies": { - "@headlessui/react": "^2.2.2", + "@headlessui/react": "^2.2.3", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", - "@vitejs/plugin-basic-ssl": "^1.2.0", + "@vitejs/plugin-basic-ssl": "^2.0.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", - "cva": "^1.0.0-beta.1", + "cva": "^1.0.0-beta.3", "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", - "focus-trap-react": "^10.2.3", - "framer-motion": "^11.15.0", + "focus-trap-react": "^11.0.3", + "framer-motion": "^12.11.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", @@ -31,24 +31,29 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.71", + "react-simple-keyboard": "^3.8.72", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", - "tailwind-merge": "^2.5.5", + "tailwind-merge": "^3.3.0", "usehooks-ts": "^3.1.1", "validator": "^13.15.0", "zustand": "^4.5.2" }, "devDependencies": { + "@eslint/compat": "^1.2.9", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.26.0", "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/postcss": "^4.1.6", "@tailwindcss/typography": "^0.5.16", - "@types/react": "^19.1.3", - "@types/react-dom": "^19.1.3", + "@tailwindcss/vite": "^4.1.6", + "@types/react": "^19.1.4", + "@types/react-dom": "^19.1.5", "@types/semver": "^7.7.0", "@types/validator": "^13.15.0", - "@typescript-eslint/eslint-plugin": "^8.32.0", - "@typescript-eslint/parser": "^8.32.0", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.21", "eslint": "^9.26.0", @@ -57,22 +62,25 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.1.0", "postcss": "^8.5.3", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.1.6", "typescript": "^5.8.3", - "vite": "^5.2.0", + "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" }, "engines": { - "node": "21.1.0" + "node": "22.15.0" } }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -80,363 +88,434 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ - "x64" + "arm64" ], + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -454,14 +533,34 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.9.tgz", + "integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/config-array": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -471,30 +570,11 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/config-helpers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -503,6 +583,7 @@ "version": "0.13.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -514,6 +595,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -532,30 +614,23 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { "version": "9.26.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -564,6 +639,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -572,6 +648,7 @@ "version": "0.2.8", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" @@ -584,6 +661,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.9" } @@ -592,6 +670,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" @@ -601,6 +680,7 @@ "version": "0.26.28", "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", @@ -615,6 +695,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.0.0" }, @@ -626,16 +707,18 @@ "node_modules/@floating-ui/utils": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" }, "node_modules/@headlessui/react": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.2.tgz", - "integrity": "sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.3.tgz", + "integrity": "sha512-hgOJGXPifPlOczIeSwX8OjLWRJ5XdYApZFf7DeCbCrO1PXHkPhNTRrA9ZwJsgAG7SON1i2JcvIreF/kbgtJeaQ==", + "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.17.1", - "@react-aria/interactions": "^3.21.3", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.6", "use-sync-external-store": "^1.5.0" }, @@ -651,6 +734,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -662,6 +746,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } @@ -670,6 +755,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } @@ -678,6 +764,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -690,6 +777,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -702,6 +790,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -714,6 +803,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -722,26 +812,25 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -755,6 +844,8 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -763,6 +854,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -770,21 +863,26 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", - "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", + "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", + "license": "MIT", "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", @@ -805,6 +903,8 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -817,6 +917,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -825,6 +927,8 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -833,19 +937,11 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@react-aria/focus": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", "integrity": "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==", + "license": "Apache-2.0", "dependencies": { "@react-aria/interactions": "^3.25.0", "@react-aria/utils": "^3.28.2", @@ -862,6 +958,7 @@ "version": "3.25.0", "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.0.tgz", "integrity": "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==", + "license": "Apache-2.0", "dependencies": { "@react-aria/ssr": "^3.9.8", "@react-aria/utils": "^3.28.2", @@ -878,6 +975,7 @@ "version": "3.9.8", "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", + "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" }, @@ -892,6 +990,7 @@ "version": "3.28.2", "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.2.tgz", "integrity": "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==", + "license": "Apache-2.0", "dependencies": { "@react-aria/ssr": "^3.9.8", "@react-stately/flags": "^3.1.1", @@ -909,6 +1008,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", + "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" } @@ -917,6 +1017,7 @@ "version": "3.10.6", "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", + "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" }, @@ -928,6 +1029,7 @@ "version": "3.29.0", "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.0.tgz", "integrity": "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==", + "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } @@ -936,6 +1038,7 @@ "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -947,6 +1050,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -959,6 +1063,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -971,6 +1076,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -983,6 +1089,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -995,6 +1102,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1007,6 +1115,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1019,6 +1128,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1031,6 +1141,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1043,6 +1154,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1055,6 +1167,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1067,6 +1180,7 @@ "cpu": [ "loong64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1079,6 +1193,7 @@ "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1091,6 +1206,7 @@ "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1103,6 +1219,7 @@ "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1115,6 +1232,7 @@ "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1127,6 +1245,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1139,6 +1258,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1151,6 +1271,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1163,6 +1284,7 @@ "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1175,6 +1297,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1183,7 +1306,8 @@ "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "license": "MIT" }, "node_modules/@swc/core": { "version": "1.11.24", @@ -1191,6 +1315,7 @@ "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" @@ -1231,6 +1356,7 @@ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -1247,6 +1373,7 @@ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -1263,6 +1390,7 @@ "arm" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1279,6 +1407,7 @@ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1295,6 +1424,7 @@ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1311,6 +1441,7 @@ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1327,6 +1458,7 @@ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1343,6 +1475,7 @@ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -1359,6 +1492,7 @@ "ia32" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -1375,6 +1509,7 @@ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -1387,12 +1522,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } @@ -1402,6 +1539,7 @@ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } @@ -1411,6 +1549,7 @@ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", "dev": true, + "license": "MIT", "dependencies": { "mini-svg-data-uri": "^1.2.3" }, @@ -1418,11 +1557,288 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", + "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.29.2", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", + "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-x64": "4.1.6", + "@tailwindcss/oxide-freebsd-x64": "4.1.6", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-x64-musl": "4.1.6", + "@tailwindcss/oxide-wasm32-wasi": "4.1.6", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", + "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", + "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", + "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", + "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", + "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", + "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", + "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", + "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", + "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", + "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.9", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", + "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", + "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.6.tgz", + "integrity": "sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.6", + "@tailwindcss/oxide": "4.1.6", + "postcss": "^8.4.41", + "tailwindcss": "4.1.6" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", @@ -1433,10 +1849,26 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz", + "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.6", + "@tailwindcss/oxide": "4.1.6", + "tailwindcss": "4.1.6" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.8", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.8.tgz", "integrity": "sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==", + "license": "MIT", "dependencies": { "@tanstack/virtual-core": "3.13.8" }, @@ -1453,6 +1885,7 @@ "version": "3.13.8", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.8.tgz", "integrity": "sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -1461,22 +1894,26 @@ "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", "dependencies": { "@types/d3-color": "*" } @@ -1484,12 +1921,14 @@ "node_modules/@types/d3-path": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", "dependencies": { "@types/d3-time": "*" } @@ -1498,6 +1937,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", "dependencies": { "@types/d3-path": "*" } @@ -1505,42 +1945,47 @@ "node_modules/@types/d3-time": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", - "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", - "devOptional": true, + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", + "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", - "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", - "dev": true, + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", + "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } @@ -1549,27 +1994,30 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/validator": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -1586,16 +2034,27 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -1611,13 +2070,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1628,13 +2088,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1651,10 +2112,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1664,13 +2126,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1689,16 +2152,43 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1713,12 +2203,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1734,6 +2225,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1742,14 +2234,15 @@ } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", - "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.0.0.tgz", + "integrity": "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==", + "license": "MIT", "engines": { - "node": ">=14.21.3" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^6.0.0" } }, "node_modules/@vitejs/plugin-react-swc": { @@ -1757,6 +2250,7 @@ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", "dev": true, + "license": "MIT", "dependencies": { "@swc/core": "^1.11.21" }, @@ -1768,6 +2262,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.1.0.tgz", "integrity": "sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==", + "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, @@ -1779,6 +2274,7 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1787,6 +2283,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.8.0.tgz", "integrity": "sha512-LxinXu8SC4OmVa6FhgwsVCBZbr8WoSGzBl2+vqe8WcQ6hb1r6Gj9P99qTNdPiFPh4Ceiu2pC8xukZ6+2nnh49Q==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1795,6 +2292,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1803,6 +2301,7 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.18.0.tgz", "integrity": "sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1810,12 +2309,14 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -1828,6 +2329,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1839,6 +2341,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1847,6 +2350,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1858,21 +2362,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1883,37 +2377,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -1929,6 +2403,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1949,6 +2424,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1968,6 +2444,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1988,6 +2465,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -2005,6 +2483,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -2023,6 +2502,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2038,6 +2518,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -2058,6 +2539,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2081,6 +2563,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", @@ -2103,6 +2586,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -2116,23 +2600,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -2149,17 +2624,21 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2186,6 +2665,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -2203,6 +2683,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2211,6 +2692,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -2228,6 +2710,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2240,6 +2723,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -2255,22 +2739,15 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001717", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", - "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -2285,12 +2762,14 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2302,44 +2781,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2348,6 +2804,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2358,25 +2815,20 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "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" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -2388,6 +2840,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2396,6 +2849,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2404,6 +2858,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", "engines": { "node": ">=6.6.0" } @@ -2412,6 +2867,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -2424,6 +2880,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2437,6 +2894,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -2447,12 +2906,14 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/cva": { "version": "1.0.0-beta.3", "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.3.tgz", "integrity": "sha512-CZa8pTkpEygxJRLH9aod/wfnSgK5z/0GJqG/NNehlwam+S8llqCWUXS3eCenvAiW5sTUpwTWE6bJaeeZ/b4pzA==", + "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" }, @@ -2472,6 +2933,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -2483,6 +2945,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2491,6 +2954,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -2499,6 +2963,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2507,6 +2972,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -2518,6 +2984,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2526,6 +2993,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -2541,6 +3009,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -2552,6 +3021,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -2563,6 +3033,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -2574,6 +3045,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2582,6 +3054,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2598,6 +3071,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2614,6 +3088,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -2629,12 +3104,14 @@ "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -2650,17 +3127,20 @@ "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2677,6 +3157,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2693,24 +3174,26 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2722,6 +3205,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -2731,6 +3215,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2740,39 +3225,47 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.151", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", - "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "version": "1.5.152", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", + "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", + "dev": true, + "license": "ISC" }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -2837,6 +3330,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2845,6 +3339,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2854,6 +3349,7 @@ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -2880,6 +3376,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2891,6 +3388,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2905,6 +3403,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2916,6 +3415,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -2929,40 +3429,43 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { @@ -2970,6 +3473,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2977,12 +3481,14 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -2994,6 +3500,7 @@ "version": "9.26.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3056,6 +3563,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3070,6 +3578,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "license": "MIT", "engines": { "node": ">= 4" }, @@ -3081,6 +3590,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -3091,6 +3601,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -3099,6 +3610,7 @@ "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -3115,6 +3627,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -3123,6 +3636,7 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -3151,38 +3665,20 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -3192,6 +3688,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -3224,6 +3721,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3236,37 +3734,17 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=8.40" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -3284,6 +3762,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -3292,6 +3771,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3307,6 +3787,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3314,19 +3795,11 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3334,21 +3807,11 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -3365,6 +3828,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3376,6 +3840,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -3387,6 +3852,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3398,6 +3864,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -3406,6 +3873,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -3414,6 +3882,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3421,12 +3890,14 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" }, @@ -3438,6 +3909,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", "engines": { "node": ">=18.0.0" } @@ -3446,6 +3918,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -3487,6 +3960,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", "engines": { "node": ">= 16" }, @@ -3500,12 +3974,14 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-equals": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -3514,6 +3990,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3529,6 +4007,8 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3539,17 +4019,21 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -3558,6 +4042,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -3569,6 +4054,8 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3580,6 +4067,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -3596,6 +4084,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3611,6 +4100,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -3622,34 +4112,39 @@ "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" }, "node_modules/focus-trap": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "license": "MIT", "dependencies": { "tabbable": "^6.2.0" } }, "node_modules/focus-trap-react": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.3.1.tgz", - "integrity": "sha512-PN4Ya9xf9nyj/Nd9VxBNMuD7IrlRbmaG6POAQ8VLqgtc6IY/Ln1tYakow+UIq4fihYYYFM70/2oyidE6bbiPgw==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-11.0.3.tgz", + "integrity": "sha512-tS1+enWS/gwCHk2WIF3KpM2oz7Y3HsnRImzHZNRgCBLWXzNG4XQVlJgbqdLr4lBKRXGdDBjQYitSh1bf2xe4Ag==", + "license": "MIT", "dependencies": { - "focus-trap": "^7.6.1", + "focus-trap": "^7.6.4", "tabbable": "^6.2.0" }, "peerDependencies": { - "prop-types": "^15.8.1", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -3660,25 +4155,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3688,6 +4169,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -3697,12 +4179,13 @@ } }, "node_modules/framer-motion": { - "version": "11.18.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", - "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.11.1.tgz", + "integrity": "sha512-k8SlrjoDesLTBklizQ2BSW7Pb99cHDoZe51Tff9ZjmWHQ/N5DLly949veBnwNfeH4d0E/h02RKuul/6D+5u9Bg==", + "license": "MIT", "dependencies": { - "motion-dom": "^11.18.1", - "motion-utils": "^11.18.1", + "motion-dom": "^12.11.0", + "motion-utils": "^12.9.4", "tslib": "^2.4.0" }, "peerDependencies": { @@ -3726,6 +4209,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3735,6 +4219,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3747,6 +4232,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3755,6 +4241,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3774,6 +4261,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3782,6 +4270,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3805,6 +4294,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3817,6 +4307,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3829,29 +4320,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3860,9 +4333,11 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -3874,6 +4349,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -3889,12 +4365,14 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", "peerDependencies": { "csstype": "^3.0.10" } @@ -3903,6 +4381,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3910,16 +4389,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3931,6 +4419,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -3939,6 +4428,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3950,6 +4440,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -3964,6 +4455,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3975,6 +4467,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3989,6 +4482,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -4000,6 +4494,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -4015,6 +4510,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -4026,6 +4522,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { "node": ">= 4" } @@ -4034,6 +4531,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -4049,6 +4547,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -4056,12 +4555,14 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -4075,6 +4576,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -4083,6 +4585,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -4091,6 +4594,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4107,6 +4611,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -4125,6 +4630,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -4135,21 +4641,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4165,6 +4661,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4176,6 +4673,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -4190,6 +4688,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -4206,6 +4705,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -4221,6 +4721,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4229,6 +4730,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4239,18 +4741,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -4268,6 +4763,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -4279,6 +4775,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4290,6 +4787,8 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -4298,6 +4797,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4312,12 +4812,14 @@ "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -4335,6 +4837,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4346,6 +4849,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4360,6 +4864,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4375,6 +4880,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -4391,6 +4897,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -4405,6 +4912,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4416,6 +4924,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4430,6 +4939,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -4444,18 +4954,21 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -4468,42 +4981,33 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "devOptional": true, + "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "license": "BSD-3-Clause" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4514,22 +5018,26 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -4542,6 +5050,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -4556,6 +5065,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -4564,6 +5074,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4572,26 +5083,240 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4605,39 +5330,46 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4645,15 +5377,21 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4662,6 +5400,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4670,6 +5409,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -4681,6 +5421,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -4689,6 +5431,8 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4701,6 +5445,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4709,6 +5454,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, @@ -4720,28 +5466,28 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", "bin": { "mini-svg-data-uri": "cli.js" } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4750,37 +5496,61 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/motion-dom": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", - "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", "dependencies": { - "motion-utils": "^11.18.1" + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.11.0.tgz", + "integrity": "sha512-CItkGYJenn5ZsbzTX0D9mE0UWdjdd9r535FrxEXhzR8Kwa9I2dLr1uhEJgQPWbgaIJ6i0sNFnf2T9NvVDWQVBw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.9.4" } }, "node_modules/motion-utils": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", - "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==" + "version": "12.9.4", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.9.4.tgz", + "integrity": "sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg==", + "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "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" - } + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", @@ -4792,6 +5562,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4802,12 +5573,14 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4816,21 +5589,15 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } + "dev": true, + "license": "MIT" }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4839,22 +5606,16 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4866,6 +5627,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4874,6 +5636,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4894,6 +5657,7 @@ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -4908,6 +5672,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4925,6 +5690,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4938,6 +5704,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4955,6 +5722,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -4966,6 +5734,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -4974,6 +5743,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4990,6 +5760,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -5006,6 +5777,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -5020,6 +5792,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -5030,15 +5803,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -5050,6 +5819,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5058,6 +5828,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { "node": ">=8" } @@ -5066,6 +5837,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -5073,27 +5845,14 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", "engines": { "node": ">=16" } @@ -5101,12 +5860,15 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5114,26 +5876,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "engines": { - "node": ">= 6" - } - }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", "engines": { "node": ">=16.20.0" } @@ -5142,6 +5889,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -5164,6 +5912,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -5173,115 +5922,12 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-selector-parser": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5293,12 +5939,15 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -5308,6 +5957,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -5323,6 +5973,7 @@ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.21.3" }, @@ -5400,6 +6051,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5410,6 +6062,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -5422,6 +6075,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5430,6 +6084,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -5444,6 +6099,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -5457,12 +6113,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5471,6 +6129,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -5485,6 +6144,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5493,6 +6153,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/react-animate-height/-/react-animate-height-3.2.3.tgz", "integrity": "sha512-R6DSvr7ud07oeCixScyvXWEMJY/Mt2+GyOWC1KMaRc69gOBw+SsCg4TJmrp4rKUM1hyd6p+YKw90brjPH93Y2A==", + "license": "MIT", "engines": { "node": ">= 12.0.0" }, @@ -5505,6 +6166,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, @@ -5516,6 +6178,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" @@ -5532,6 +6195,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -5539,12 +6203,14 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/react-router": { "version": "6.30.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "license": "MIT", "dependencies": { "@remix-run/router": "1.23.0" }, @@ -5559,6 +6225,7 @@ "version": "6.30.0", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "license": "MIT", "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.0" @@ -5572,9 +6239,10 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.71", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.71.tgz", - "integrity": "sha512-U0f+bRe0wuuzp1gsuHPjjYWv3ZywSr7wDYyBv12d4w5FDXmib7FfyefzHP+yRuW2hH444OpfJvlv4y70+8tEqw==", + "version": "3.8.72", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.72.tgz", + "integrity": "sha512-C34MVOykLrlONImOdJrSaXrNloTxOtUStipQaINR5Bem5JLFHEszNedVBEmxnt7Fqb7CTIIZ7fSgR8FasRIbzw==", + "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -5584,6 +6252,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -5598,6 +6267,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -5612,39 +6282,23 @@ "node_modules/react-use-websocket": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", - "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==" + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==", + "license": "MIT" }, "node_modules/react-xtermjs": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.10.tgz", "integrity": "sha512-+xpKEKbmsypWzRKE0FR1LNIGcI8gx+R6VMHe8IQW7iTbgeqp3Qg7SbiVNOzR+Ovb1QK4DPA3KqsIQV+XP0iRUA==", + "license": "ISC", "peerDependencies": { "@xterm/xterm": "^5.5.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/recharts": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -5667,6 +6321,7 @@ "version": "0.4.5", "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", "dependencies": { "decimal.js-light": "^2.4.1" } @@ -5674,12 +6329,14 @@ "node_modules/recharts/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5701,6 +6358,7 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5720,6 +6378,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -5739,6 +6398,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } @@ -5747,6 +6407,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -5756,6 +6418,7 @@ "version": "4.40.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "license": "MIT", "dependencies": { "@types/estree": "1.0.7" }, @@ -5794,6 +6457,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -5809,6 +6473,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -5823,6 +6488,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -5831,6 +6497,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5862,12 +6529,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -5883,6 +6552,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5898,18 +6568,21 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5921,6 +6594,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -5942,6 +6616,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -5956,6 +6631,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5972,6 +6648,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5986,6 +6663,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -5998,12 +6676,14 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6015,6 +6695,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } @@ -6023,6 +6704,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -6041,6 +6723,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -6056,6 +6739,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -6073,6 +6757,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -6087,21 +6772,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -6110,69 +6785,17 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -6200,6 +6823,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -6209,6 +6833,7 @@ "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -6229,6 +6854,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -6246,6 +6872,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6258,44 +6885,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -6304,6 +6898,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -6311,31 +6906,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "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": ">=16 || 14 >=14.17" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6347,6 +6922,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6357,93 +6933,107 @@ "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" }, "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", + "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", + "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=6" } }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=4" - } - }, - "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": ">=18" } }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -6455,6 +7045,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } @@ -6464,6 +7055,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -6471,16 +7063,12 @@ "typescript": ">=4.8.4" } }, - "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/tsconfck": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", "dev": true, + "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -6500,6 +7088,7 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -6510,12 +7099,14 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -6527,6 +7118,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -6540,6 +7132,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -6553,6 +7146,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -6571,6 +7165,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -6591,6 +7186,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -6611,6 +7207,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6623,6 +7220,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -6640,6 +7238,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6663,6 +7262,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -6678,6 +7278,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -6686,6 +7287,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -6694,6 +7296,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", "dependencies": { "lodash.debounce": "^4.0.8" }, @@ -6707,12 +7310,15 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" }, "node_modules/validator": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -6721,6 +7327,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6729,6 +7336,7 @@ "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -6747,19 +7355,23 @@ } }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6768,19 +7380,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -6801,6 +7419,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -6809,6 +7433,7 @@ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -6823,10 +7448,37 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6841,6 +7493,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -6859,6 +7512,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -6885,6 +7539,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -6902,6 +7557,7 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -6922,111 +7578,32 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "bin": { - "yaml": "bin.mjs" - }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 14" + "node": ">=18" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -7038,6 +7615,7 @@ "version": "3.24.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -7046,6 +7624,7 @@ "version": "3.24.5", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", "peerDependencies": { "zod": "^3.24.1" } @@ -7054,6 +7633,7 @@ "version": "4.5.6", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", "dependencies": { "use-sync-external-store": "^1.2.2" }, diff --git a/ui/package.json b/ui/package.json index ab2e141..e1e8188 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "engines": { - "node": "21.1.0" + "node": "22.15.0" }, "scripts": { "dev": "./dev_device.sh", @@ -19,21 +19,21 @@ "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.2.2", + "@headlessui/react": "^2.2.3", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", - "@vitejs/plugin-basic-ssl": "^1.2.0", + "@vitejs/plugin-basic-ssl": "^2.0.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", - "cva": "^1.0.0-beta.1", + "cva": "^1.0.0-beta.3", "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", - "focus-trap-react": "^10.2.3", - "framer-motion": "^11.15.0", + "focus-trap-react": "^11.0.3", + "framer-motion": "^12.11.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", @@ -42,24 +42,29 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.71", + "react-simple-keyboard": "^3.8.72", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", - "tailwind-merge": "^2.5.5", + "tailwind-merge": "^3.3.0", "usehooks-ts": "^3.1.1", "validator": "^13.15.0", "zustand": "^4.5.2" }, "devDependencies": { + "@eslint/compat": "^1.2.9", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.26.0", "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/postcss": "^4.1.6", "@tailwindcss/typography": "^0.5.16", - "@types/react": "^19.1.3", - "@types/react-dom": "^19.1.3", + "@tailwindcss/vite": "^4.1.6", + "@types/react": "^19.1.4", + "@types/react-dom": "^19.1.5", "@types/semver": "^7.7.0", "@types/validator": "^13.15.0", - "@typescript-eslint/eslint-plugin": "^8.32.0", - "@typescript-eslint/parser": "^8.32.0", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.21", "eslint": "^9.26.0", @@ -68,12 +73,13 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.1.0", "postcss": "^8.5.3", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.1.6", "typescript": "^5.8.3", - "vite": "^5.2.0", + "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" } } diff --git a/ui/postcss.config.js b/ui/postcss.config.js index 2e7af2b..b6dc034 100644 --- a/ui/postcss.config.js +++ b/ui/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, autoprefixer: {}, }, } diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 6085ec7..26470f7 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -16,7 +16,7 @@ const sizes = { const themes = { primary: cx( // Base styles - "bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow", + "bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow-sm", // Hover states "group-hover:bg-blue-800", // Active states @@ -24,7 +24,7 @@ const themes = { ), danger: cx( // Base styles - "bg-red-600 text-white border-red-700 shadow-sm shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20", + "bg-red-600 text-white border-red-700 shadow-xs shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20", // Hover states "group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600", // Active states @@ -34,7 +34,7 @@ const themes = { ), light: cx( // Base styles - "bg-white text-black border-slate-800/30 shadow dark:bg-slate-800 dark:border-slate-300/20 dark:text-white", + "bg-white text-black border-slate-800/30 shadow-xs dark:bg-slate-800 dark:border-slate-300/20 dark:text-white", // Hover states "group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700", // Active states @@ -44,7 +44,7 @@ const themes = { ), lightDanger: cx( // Base styles - "bg-white text-black border-red-400/60 shadow-sm", + "bg-white text-black border-red-400/60 shadow-xs", // Hover states "group-hover:bg-red-50/80", // Active states @@ -56,7 +56,7 @@ const themes = { // Base styles "bg-white/0 text-black border-transparent dark:text-white", // Hover states - "group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600", + "group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow-sm dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600", // Active states "group-active:bg-slate-100/80", ), @@ -65,15 +65,15 @@ const themes = { const btnVariants = cva({ base: cx( // Base styles - "border rounded select-none", + "border rounded-sm select-none", // Size classes "justify-center items-center shrink-0", // Transition classes - "outline-none transition-all duration-200", + "outline-hidden transition-all duration-200", // Text classes "font-display text-center font-medium leading-tight", // States - "group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700", + "group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700", "group-disabled:opacity-50 group-disabled:pointer-events-none", ), @@ -175,7 +175,7 @@ type ButtonPropsType = Pick< export const Button = React.forwardRef( ({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => { const classes = cx( - "group outline-none", + "group outline-hidden", props.fullWidth ? "w-full" : "", loading ? "pointer-events-none" : "", ); @@ -215,7 +215,7 @@ type LinkPropsType = Pick & React.ComponentProps & { disabled?: boolean }; export const LinkButton = ({ to, ...props }: LinkPropsType) => { const classes = cx( - "group outline-none", + "group outline-hidden", props.disabled ? "pointer-events-none !opacity-70" : "", props.fullWidth ? "w-full" : "", props.loading ? "pointer-events-none" : "", @@ -241,7 +241,7 @@ type LabelPropsType = Pick & React.ComponentProps & { disabled?: boolean }; export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => { const classes = cx( - "group outline-none block cursor-pointer", + "group outline-hidden block cursor-pointer", props.disabled ? "pointer-events-none !opacity-70" : "", props.fullWidth ? "w-full" : "", props.loading ? "pointer-events-none" : "", diff --git a/ui/src/components/Card.tsx b/ui/src/components/Card.tsx index 857ed92..bf28e96 100644 --- a/ui/src/components/Card.tsx +++ b/ui/src/components/Card.tsx @@ -30,7 +30,7 @@ const Card = forwardRef(({ children, className },
    diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/Checkbox.tsx index 4c81d86..de368f5 100644 --- a/ui/src/components/Checkbox.tsx +++ b/ui/src/components/Checkbox.tsx @@ -24,7 +24,7 @@ const checkboxVariants = cva({ "active:bg-slate-200 dark:active:bg-slate-700", // Focus - "focus:border-slate-300 dark:focus:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900", + "focus:border-slate-300 dark:focus:border-slate-600 focus:outline-hidden focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900", // Disabled "disabled:pointer-events-none disabled:opacity-30", diff --git a/ui/src/components/Combobox.tsx b/ui/src/components/Combobox.tsx index 661b77b..03b8f0b 100644 --- a/ui/src/components/Combobox.tsx +++ b/ui/src/components/Combobox.tsx @@ -58,7 +58,7 @@ export function Combobox({ {() => ( <> - +
    @@ -78,86 +81,79 @@ export default function DashboardNavbar({
    - {showConnectionStatus && ( -
    -
    - -
    -
    - -
    -
    - )} - {isLoggedIn ? ( - <> -
    - -
    - - + + + + +
    + {userEmail && ( +
    +
    +
    Logged in as
    +
    + {userEmail} +
    +
    + )} +
    +
    +
    - -
    -
    - - - - - ) : null} + + + + +
    + + ) : null} +
    diff --git a/ui/src/components/InputField.tsx b/ui/src/components/InputField.tsx index 1a788ef..ff2ad55 100644 --- a/ui/src/components/InputField.tsx +++ b/ui/src/components/InputField.tsx @@ -44,7 +44,7 @@ const InputField = forwardRef(function InputF "[&:has(:user-invalid)]:ring-2 [&:has(:user-invalid)]:ring-red-600 [&:has(:user-invalid)]:ring-offset-2", // Focus Within - "focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2", + "focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-hidden focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2", // Disabled Within "disabled-within:pointer-events-none disabled-within:select-none disabled-within:bg-slate-50 dark:disabled-within:bg-slate-800 disabled-within:text-slate-500/80", diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index c680a37..4602b35 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -113,7 +113,7 @@ export default function KvmCard({ transition className="data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in" > - +
    diff --git a/ui/src/components/LoadingSpinner.tsx b/ui/src/components/LoadingSpinner.tsx index 27dac76..261d755 100644 --- a/ui/src/components/LoadingSpinner.tsx +++ b/ui/src/components/LoadingSpinner.tsx @@ -7,7 +7,7 @@ export default function LoadingSpinner({ }) { return ( {label && } - +
    {children}
    @@ -377,7 +377,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) { >
    -
    +
    diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index cea39da..0b0db3c 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useResizeObserver } from "usehooks-ts"; import { useDeviceSettingsStore, @@ -10,7 +11,6 @@ import { useVideoStore, } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; -import { useResizeObserver } from "usehooks-ts"; import { cx } from "@/cva.config"; import VirtualKeyboard from "@components/VirtualKeyboard"; import Actionbar from "@components/ActionBar"; @@ -724,7 +724,7 @@ export default function WebRTCVideo() { hdmiError || peerConnectionState !== "connected", "!opacity-60": showPointerLockBar, - "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20": + "animate-slideUpFade border border-slate-800/30 shadow-xs dark:border-slate-300/20": isPlaying, }, )} @@ -732,7 +732,7 @@ export default function WebRTCVideo() { {peerConnection?.connectionState == "connected" && (
    diff --git a/ui/src/components/extensions/ATXPowerControl.tsx b/ui/src/components/extensions/ATXPowerControl.tsx index 0334a18..6a212ea 100644 --- a/ui/src/components/extensions/ATXPowerControl.tsx +++ b/ui/src/components/extensions/ATXPowerControl.tsx @@ -107,7 +107,7 @@ export function ATXPowerControl() { ) : ( - +
    {/* Control Buttons */}
    diff --git a/ui/src/components/extensions/DCPowerControl.tsx b/ui/src/components/extensions/DCPowerControl.tsx index 3fcb7dc..e51b5bd 100644 --- a/ui/src/components/extensions/DCPowerControl.tsx +++ b/ui/src/components/extensions/DCPowerControl.tsx @@ -63,7 +63,7 @@ export function DCPowerControl() { ) : ( - +
    {/* Power Controls */}
    diff --git a/ui/src/components/extensions/SerialConsole.tsx b/ui/src/components/extensions/SerialConsole.tsx index 544d3fd..1c8a16c 100644 --- a/ui/src/components/extensions/SerialConsole.tsx +++ b/ui/src/components/extensions/SerialConsole.tsx @@ -58,7 +58,7 @@ export function SerialConsole() { description="Configure your serial console settings" /> - +
    {/* Open Console Button */}
    diff --git a/ui/src/components/popovers/ExtensionPopover.tsx b/ui/src/components/popovers/ExtensionPopover.tsx index 69d0e70..7bf5aa0 100644 --- a/ui/src/components/popovers/ExtensionPopover.tsx +++ b/ui/src/components/popovers/ExtensionPopover.tsx @@ -92,7 +92,7 @@ export default function ExtensionPopover() { {renderActiveExtension()}
    - +
    {AVAILABLE_EXTENSIONS.map(extension => (
    ((_props, ref) => { ) : null}
    ((_props, ref) => { {!remoteVirtualMediaState && (
    - +
    {storedDevices.map((device, index) => (
    @@ -63,7 +63,7 @@ export default function DeviceList({
    - +
    @@ -35,7 +35,7 @@ export default function EmptyStateCard({
    +
    diff --git a/ui/src/index.css b/ui/src/index.css index 5052657..13f99a8 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -1,6 +1,5 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@config "../tailwind.config.js"; html { @apply scroll-smooth; @@ -50,7 +49,7 @@ video::-webkit-media-controls { } .hg-theme-default .hg-button { - @apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-sm; + @apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-xs; } .hg-theme-default .hg-button span { @@ -174,7 +173,7 @@ video::-webkit-media-controls { } .hg-theme-default .hg-row .combination-key { - @apply inline-flex !h-auto !w-auto flex-grow-0 py-1 text-xs; + @apply inline-flex !h-auto !w-auto grow-0 py-1 text-xs; } .hg-theme-default .hg-row:has(.combination-key) { diff --git a/ui/src/main.tsx b/ui/src/main.tsx index f4bdd34..cbd5e25 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -17,7 +17,7 @@ import AdoptRoute from "@routes/adopt"; import SignupRoute from "@routes/signup"; import LoginRoute from "@routes/login"; import SetupRoute from "@routes/devices.$id.setup"; -import DevicesRoute, { loader as DeviceListLoader } from "@routes/devices"; +import DevicesRoute from "@routes/devices"; import DeviceRoute, { LocalDevice } from "@routes/devices.$id"; import Card from "@components/Card"; import DevicesAlreadyAdopted from "@routes/devices.already-adopted"; @@ -36,7 +36,7 @@ import SettingsKeyboardMouseRoute from "./routes/devices.$id.settings.mouse"; import api from "./api"; import * as SettingsIndexRoute from "./routes/devices.$id.settings._index"; import SettingsAdvancedRoute from "./routes/devices.$id.settings.advanced"; -import * as SettingsAccessIndexRoute from "./routes/devices.$id.settings.access._index"; +import SettingsAccessIndexRoute from "./routes/devices.$id.settings.access._index"; import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware"; import SettingsVideoRoute from "./routes/devices.$id.settings.video"; import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; @@ -166,7 +166,7 @@ if (isOnDevice) { children: [ { index: true, - element: , + element: , loader: SettingsAccessIndexRoute.loader, }, { @@ -291,7 +291,7 @@ if (isOnDevice) { children: [ { index: true, - element: , + element: , loader: SettingsAccessIndexRoute.loader, }, { @@ -341,7 +341,10 @@ if (isOnDevice) { loader: DeviceIdRename.loader, action: DeviceIdRename.action, }, - { path: "devices", element: , loader: DeviceListLoader }, + { + path: "devices", + element: , + loader: DevicesRoute.loader }, ], }, ], @@ -356,7 +359,7 @@ document.addEventListener("DOMContentLoaded", () => { diff --git a/ui/src/routes/devices.$id.deregister.tsx b/ui/src/routes/devices.$id.deregister.tsx index 40cf6a9..55645f9 100644 --- a/ui/src/routes/devices.$id.deregister.tsx +++ b/ui/src/routes/devices.$id.deregister.tsx @@ -77,6 +77,7 @@ export default function DevicesIdDeregister() { primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]} userEmail={user?.email} picture={user?.picture} + kvmName={device?.name} />
    diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/routes/devices.$id.mount.tsx index 4d3369a..eb0a505 100644 --- a/ui/src/routes/devices.$id.mount.tsx +++ b/ui/src/routes/devices.$id.mount.tsx @@ -320,7 +320,7 @@ function ModeSelectionView({ ].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
    0 ? (
    ) : (
    -
    +
    @@ -959,7 +959,7 @@ function DeviceFileView({ {onStorageFiles.length > 0 && (
    Error: {uploadError} @@ -1373,7 +1373,7 @@ function UploadFileView({ )}
    diff --git a/ui/src/routes/devices.$id.rename.tsx b/ui/src/routes/devices.$id.rename.tsx index 8a4b194..d26070b 100644 --- a/ui/src/routes/devices.$id.rename.tsx +++ b/ui/src/routes/devices.$id.rename.tsx @@ -81,6 +81,7 @@ export default function DeviceIdRename() { primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]} userEmail={user?.email} picture={user?.picture} + kvmName={device?.name} />
    diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx index d8eebf9..f21927a 100644 --- a/ui/src/routes/devices.$id.settings.access._index.tsx +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -26,7 +26,7 @@ export interface TLSState { privateKey?: string; } -export const loader = async () => { +const loader = async () => { if (isOnDevice) { const status = await api .GET(`${DEVICE_API}/device`) @@ -468,3 +468,5 @@ export default function SettingsAccessIndexRoute() {
    ); } + +SettingsAccessIndexRoute.loader = loader; \ No newline at end of file diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx index ba1a2ba..8eeaa03 100644 --- a/ui/src/routes/devices.$id.settings.macros.tsx +++ b/ui/src/routes/devices.$id.settings.macros.tsx @@ -175,8 +175,8 @@ export default function SettingsMacrosRoute() { return ( - - + + {(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? ( diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index f48a7bd..b44636c 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -14,15 +14,14 @@ import { useNetworkStateStore, } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import notifications from "@/notifications"; import { Button } from "@components/Button"; import { GridCard } from "@components/Card"; import InputField from "@components/InputField"; - -import { SettingsPageHeader } from "../components/SettingsPageheader"; -import { SelectMenuBasic } from "../components/SelectMenuBasic"; -import Fieldset from "../components/Fieldset"; -import { ConfirmDialog } from "../components/ConfirmDialog"; +import { SelectMenuBasic } from "@/components/SelectMenuBasic"; +import { SettingsPageHeader } from "@/components/SettingsPageheader"; +import Fieldset from "@/components/Fieldset"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; +import notifications from "@/notifications"; import { SettingsItem } from "./devices.$id.settings"; @@ -51,9 +50,13 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { return () => clearInterval(interval); }, [lifetime]); + if (lifetime == "") { + return N/A; + } + return ( <> - {dayjs(lifetime).format("YYYY-MM-DD HH:mm")} + {dayjs(lifetime).format("YYYY-MM-DD HH:mm")} {remaining && ( <> {" "} diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index b75d1e1..641a64c 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -12,15 +12,16 @@ import { LuNetwork, } from "react-icons/lu"; import React, { useEffect, useRef, useState } from "react"; +import { useResizeObserver } from "usehooks-ts"; import Card from "@/components/Card"; +import { LinkButton } from "@/components/Button"; +import LoadingSpinner from "@/components/LoadingSpinner"; +import { useUiStore } from "@/hooks/stores"; +import useKeyboard from "@/hooks/useKeyboard"; -import { LinkButton } from "../components/Button"; import { cx } from "../cva.config"; -import { useUiStore } from "../hooks/stores"; -import useKeyboard from "../hooks/useKeyboard"; -import { useResizeObserver } from "usehooks-ts"; -import LoadingSpinner from "../components/LoadingSpinner"; + /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ export default function SettingsRoute() { diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 161f494..fe13fe2 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -12,7 +12,7 @@ import { useSearchParams, } from "react-router-dom"; import { useInterval } from "usehooks-ts"; -import FocusTrap from "focus-trap-react"; +import { FocusTrap } from "focus-trap-react"; import { motion, AnimatePresence } from "framer-motion"; import useWebSocket from "react-use-websocket"; @@ -809,7 +809,7 @@ export default function KvmIdRoute() {
    {!!ConnectionStatusElement && ConnectionStatusElement} diff --git a/ui/src/routes/devices.tsx b/ui/src/routes/devices.tsx index 391b62a..9090646 100644 --- a/ui/src/routes/devices.tsx +++ b/ui/src/routes/devices.tsx @@ -1,14 +1,14 @@ import { useLoaderData, useRevalidator } from "react-router-dom"; import { LuMonitorSmartphone } from "react-icons/lu"; import { ArrowRightIcon } from "@heroicons/react/16/solid"; +import { useInterval } from "usehooks-ts"; import DashboardNavbar from "@components/Header"; -import { LinkButton } from "@components/Button"; -import KvmCard from "@components/KvmCard"; -import { useInterval } from "usehooks-ts"; -import { checkAuth } from "@/main"; -import { User } from "@/hooks/stores"; import EmptyCard from "@components/EmptyCard"; +import KvmCard from "@components/KvmCard"; +import { LinkButton } from "@components/Button"; +import { User } from "@/hooks/stores"; +import { checkAuth } from "@/main"; import { CLOUD_API } from "@/ui.config"; interface LoaderData { @@ -16,7 +16,7 @@ interface LoaderData { user: User; } -export const loader = async () => { +const loader = async () => { const user = await checkAuth(); try { @@ -101,3 +101,5 @@ export default function DevicesRoute() {
    ); } + +DevicesRoute.loader = loader; \ No newline at end of file diff --git a/ui/src/routes/welcome-local.mode.tsx b/ui/src/routes/welcome-local.mode.tsx index ffe0ead..68babaf 100644 --- a/ui/src/routes/welcome-local.mode.tsx +++ b/ui/src/routes/welcome-local.mode.tsx @@ -61,13 +61,13 @@ export default function WelcomeLocalModeRoute() {
    -
    +

    Local Authentication Method

    @@ -78,7 +78,7 @@ export default function WelcomeLocalModeRoute() {
    {["password", "noPassword"].map(mode => ( @@ -120,7 +120,7 @@ export default function WelcomeLocalModeRoute() { {actionData?.error && (

    {actionData.error} @@ -128,7 +128,7 @@ export default function WelcomeLocalModeRoute() { )}

    + className="right-0 mt-1 w-56 origin-top-right p-px focus:outline-hidden data-closed:opacity-0" + > -
    - {userEmail && ( + {userEmail && ( +
    -
    Logged in as
    -
    +
    + Logged in as +
    +
    {userEmail}
    - )} -
    -
    +
    + )} +
    +
    + Date: Thu, 12 Jun 2025 01:34:19 -0500 Subject: [PATCH 108/165] Upgrade pion modules to fix CVE (#572) Fixes #570 ## Required | Package | From | To | |---|---|---| | github.com/pion/logging | v0.2.2 | v0.2.3 | | github.com/pion/wrbrtc/v4 | v4.0.0 | v4.0.16 | ## Indirect | Package | From | To | |---|---|---| | github.com/pion/datachannel | v1.5.9 | v1.5.10 | | github.com/pion/dtls | v3.0.3 | v3.0.6 | | github.com/pion/ice/v4 | v4.0.2 | v4.0.10 | | github.com/pion/interceptor | v0.1.37 | v0.1.40 | | github.com/pion/rtcp | v1.2.14 | v1.2.15 | | github.com/pion/rtp | v1.8.9 | v1.8.18 | | github.com/pion/sctp | v1.8.33 | v1.8.39 | | github.com/pion/sdp | v3.0.9| v3.0.13 | | github.com/pion/srtp | v3.0.4 | v3.0.5 | | github.com/pion/turn | v4.0.0 | v4.0.2 | --- go.mod | 24 ++++++++++++------------ go.sum | 49 ++++++++++++++++++++++++------------------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index 4abd752..406bd3d 100644 --- a/go.mod +++ b/go.mod @@ -17,9 +17,9 @@ require ( github.com/guregu/null/v6 v6.0.0 github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf github.com/hanwen/go-fuse/v2 v2.5.1 - github.com/pion/logging v0.2.2 + github.com/pion/logging v0.2.3 github.com/pion/mdns/v2 v2.0.7 - github.com/pion/webrtc/v4 v4.0.0 + github.com/pion/webrtc/v4 v4.0.16 github.com/pojntfx/go-nbd v0.3.2 github.com/prometheus/client_golang v1.21.0 github.com/prometheus/common v0.62.0 @@ -64,19 +64,19 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pilebones/go-udev v0.9.0 // indirect - github.com/pion/datachannel v1.5.9 // indirect - github.com/pion/dtls/v3 v3.0.3 // indirect - github.com/pion/ice/v4 v4.0.2 // indirect - github.com/pion/interceptor v0.1.37 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.14 // indirect - github.com/pion/rtp v1.8.9 // indirect - github.com/pion/sctp v1.8.33 // indirect - github.com/pion/sdp/v3 v3.0.9 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtp v1.8.18 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/srtp/v3 v3.0.5 // indirect github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect diff --git a/go.sum b/go.sum index 63ebc96..af17083 100644 --- a/go.sum +++ b/go.sum @@ -102,38 +102,38 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= -github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= -github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= -github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM= -github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU= -github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s= -github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= -github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= -github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= -github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= -github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= -github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU= +github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= +github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo= +github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8= -github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA= +github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -164,7 +164,6 @@ 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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= From abb4350316ae6c12e685edde679eda8d796ed6cc Mon Sep 17 00:00:00 2001 From: Ben Kochie Date: Thu, 12 Jun 2025 08:36:31 +0200 Subject: [PATCH 109/165] chore: enable dependabot (#256) Enable dependabot to auto-update dependencies. * Update montly to avoid too much PR noise. * Enable updates for Go modules. * Enable updates for GitHub Actions. * Enable updates for NPM in /ui. Signed-off-by: SuperQ --- .github/dependabot.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cc36cf7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 10 + - package-ecosystem: npm + directory: /ui + open-pull-requests-limit: 10 + schedule: + interval: monthly From bfbc1a5a575cf4bdca89595651eb44ead5e3fb90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:40:53 +0200 Subject: [PATCH 110/165] build(deps): bump actions/setup-go from 4 to 5 (#577) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/smoketest.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bf4b13..2dcf36f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: cache: "npm" cache-dependency-path: "**/package-lock.json" - name: Set up Golang - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: "1.24.3" - name: Build frontend diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 0a74064..37013c1 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -24,7 +24,7 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1 with: go-version: 1.24.3 - name: Create empty resource directory diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index 46f0198..4b3af1d 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -104,7 +104,7 @@ jobs: EOF ssh jkci "cat /tmp/device-tests.json" > device-tests.json - name: Set up Golang - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: "1.24.3" - name: Golang Test Report From 7ef9a7ba9310bdef9a9bfa76b39b093e4e6ad556 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:42:08 +0200 Subject: [PATCH 111/165] build(deps): bump github.com/gin-contrib/logger from 1.2.5 to 1.2.6 (#589) Bumps [github.com/gin-contrib/logger](https://github.com/gin-contrib/logger) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/gin-contrib/logger/releases) - [Changelog](https://github.com/gin-contrib/logger/blob/master/.goreleaser.yaml) - [Commits](https://github.com/gin-contrib/logger/compare/v1.2.5...v1.2.6) --- updated-dependencies: - dependency-name: github.com/gin-contrib/logger dependency-version: 1.2.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 26 ++++++++++++-------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 406bd3d..0aa1c03 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ require ( github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 github.com/fsnotify/fsnotify v1.9.0 - github.com/gin-contrib/logger v1.2.5 - github.com/gin-gonic/gin v1.10.0 + github.com/gin-contrib/logger v1.2.6 + github.com/gin-gonic/gin v1.10.1 github.com/google/uuid v1.6.0 github.com/guregu/null/v6 v6.0.0 github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf @@ -45,8 +45,8 @@ require ( github.com/cloudwego/base64x v0.1.5 // indirect github.com/creack/goselect v0.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -62,7 +62,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pilebones/go-udev v0.9.0 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect @@ -84,7 +84,7 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/arch v0.15.0 // indirect + golang.org/x/arch v0.17.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect diff --git a/go.sum b/go.sum index af17083..fcd15b7 100644 --- a/go.sum +++ b/go.sum @@ -30,14 +30,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= -github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I= -github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= -github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc= +github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -98,8 +98,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= @@ -157,13 +157,11 @@ github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzr github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -178,8 +176,8 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= +golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= From 9bd587b52e6d1013ec39cd8a7762170bf06f7c4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:42:14 +0200 Subject: [PATCH 112/165] build(deps): bump github.com/prometheus/client_golang (#588) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.22.0. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.22.0) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-version: 1.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 +--- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 0aa1c03..70c11af 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/pion/mdns/v2 v2.0.7 github.com/pion/webrtc/v4 v4.0.16 github.com/pojntfx/go-nbd v0.3.2 - github.com/prometheus/client_golang v1.21.0 + github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.62.0 github.com/prometheus/procfs v0.15.1 github.com/psanford/httpreadat v0.1.0 @@ -52,9 +52,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect diff --git a/go.sum b/go.sum index fcd15b7..56325b7 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uo github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -137,8 +137,8 @@ github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zN github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= -github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= From a40d26ab9bc631fd214304841ca8387d17a86c80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:42:25 +0200 Subject: [PATCH 113/165] build(deps): bump github.com/prometheus/procfs from 0.15.1 to 0.16.1 (#592) Bumps [github.com/prometheus/procfs](https://github.com/prometheus/procfs) from 0.15.1 to 0.16.1. - [Release notes](https://github.com/prometheus/procfs/releases) - [Commits](https://github.com/prometheus/procfs/compare/v0.15.1...v0.16.1) --- updated-dependencies: - dependency-name: github.com/prometheus/procfs dependency-version: 0.16.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 70c11af..9d7e0c3 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/pojntfx/go-nbd v0.3.2 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.62.0 - github.com/prometheus/procfs v0.15.1 + github.com/prometheus/procfs v0.16.1 github.com/psanford/httpreadat v0.1.0 github.com/rs/zerolog v1.34.0 github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f diff --git a/go.sum b/go.sum index 56325b7..c8754b2 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= From 0d955a8d9517ebf883626362bacf8c36fd33bf00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:42:59 +0200 Subject: [PATCH 114/165] build(deps): bump github.com/beevik/ntp from 1.3.1 to 1.4.3 (#585) Bumps [github.com/beevik/ntp](https://github.com/beevik/ntp) from 1.3.1 to 1.4.3. - [Release notes](https://github.com/beevik/ntp/releases) - [Changelog](https://github.com/beevik/ntp/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/beevik/ntp/compare/v1.3.1...v1.4.3) --- updated-dependencies: - dependency-name: github.com/beevik/ntp dependency-version: 1.4.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9d7e0c3..7ceb793 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.3 require ( github.com/Masterminds/semver/v3 v3.3.0 - github.com/beevik/ntp v1.3.1 + github.com/beevik/ntp v1.4.3 github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 diff --git a/go.sum b/go.sum index c8754b2..34deb72 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM= -github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw= +github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= +github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= From 91171d9bf719ce0a841b8f5623302ce4d8f40f03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:45:05 +0200 Subject: [PATCH 115/165] build(deps): bump golang.org/x/net from 0.40.0 to 0.41.0 (#580) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0. - [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 7ceb793..263a88f 100644 --- a/go.mod +++ b/go.mod @@ -30,8 +30,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/vishvananda/netlink v1.3.0 go.bug.st/serial v1.6.2 - golang.org/x/crypto v0.38.0 - golang.org/x/net v0.40.0 + golang.org/x/crypto v0.39.0 + golang.org/x/net v0.41.0 golang.org/x/sys v0.33.0 ) @@ -84,7 +84,7 @@ require ( github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 34deb72..700da9d 100644 --- a/go.sum +++ b/go.sum @@ -178,10 +178,10 @@ go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -193,8 +193,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 89f3bc8c4024f000c5a1bf0268373dd0f1c9e052 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:46:11 +0200 Subject: [PATCH 116/165] build(deps): bump github.com/go-jose/go-jose/v4 in the go_modules group (#596) Bumps the go_modules group with 1 update: [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose). Updates `github.com/go-jose/go-jose/v4` from 4.0.2 to 4.0.5 - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v4.0.2...v4.0.5) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-version: 4.0.5 dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 263a88f..cafc174 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect diff --git a/go.sum b/go.sum index 700da9d..4be7c5c 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= From 4f6026e182b770d9d82ff15f441de263a92c4281 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:49:56 +0200 Subject: [PATCH 117/165] build(deps): bump github.com/Masterminds/semver/v3 from 3.3.0 to 3.3.1 (#593) Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/Masterminds/semver/releases) - [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md) - [Commits](https://github.com/Masterminds/semver/compare/v3.3.0...v3.3.1) --- updated-dependencies: - dependency-name: github.com/Masterminds/semver/v3 dependency-version: 3.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cafc174..940e4ce 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.4 toolchain go1.24.3 require ( - github.com/Masterminds/semver/v3 v3.3.0 + github.com/Masterminds/semver/v3 v3.3.1 github.com/beevik/ntp v1.4.3 github.com/coder/websocket v1.8.13 github.com/coreos/go-oidc/v3 v3.11.0 diff --git a/go.sum b/go.sum index 4be7c5c..25b40a7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= From 0636cc9affad27b2bfc98eeacd9acb5b3168bc80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:50:14 +0200 Subject: [PATCH 118/165] build(deps): bump github.com/hanwen/go-fuse/v2 from 2.5.1 to 2.8.0 (#590) Bumps [github.com/hanwen/go-fuse/v2](https://github.com/hanwen/go-fuse) from 2.5.1 to 2.8.0. - [Commits](https://github.com/hanwen/go-fuse/compare/v2.5.1...v2.8.0) --- updated-dependencies: - dependency-name: github.com/hanwen/go-fuse/v2 dependency-version: 2.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 940e4ce..3e38ac1 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/uuid v1.6.0 github.com/guregu/null/v6 v6.0.0 github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf - github.com/hanwen/go-fuse/v2 v2.5.1 + github.com/hanwen/go-fuse/v2 v2.8.0 github.com/pion/logging v0.2.3 github.com/pion/mdns/v2 v2.0.7 github.com/pion/webrtc/v4 v4.0.16 diff --git a/go.sum b/go.sum index 25b40a7..6e7e5d5 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= -github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= -github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs= +github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -77,7 +77,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -89,8 +88,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -184,8 +183,6 @@ golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 4bfbc66ea7f394d11fae44af06fcb4b4b29e958b Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:53:58 +0200 Subject: [PATCH 119/165] chore: upgrade go from 1.24.3 to 1.24.4 (#600) --- .github/workflows/build.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/smoketest.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dcf36f..c7cbb22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Golang uses: actions/setup-go@v5 with: - go-version: "1.24.3" + go-version: "1.24.4" - name: Build frontend run: | make frontend diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 37013c1..3ce3ef3 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -26,7 +26,7 @@ jobs: - name: Install Go uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1 with: - go-version: 1.24.3 + go-version: 1.24.4 - name: Create empty resource directory run: | mkdir -p static && touch static/.gitkeep diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index 4b3af1d..ebce418 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -106,7 +106,7 @@ jobs: - name: Set up Golang uses: actions/setup-go@v5 with: - go-version: "1.24.3" + go-version: "1.24.4" - name: Golang Test Report uses: becheran/go-testreport@v0.3.2 with: From c494cf26ef04cbff3f912812c393edec52a9b888 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:29:31 +0200 Subject: [PATCH 120/165] chore: disable cgo (#601) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8262698..d4629e8 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ GO_LDFLAGS := \ -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) -GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go +GO_CMD := GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 go BIN_DIR := $(shell pwd)/bin TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) From 3cc119c64658149b6199939917b0efa7e5342448 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Thu, 12 Jun 2025 09:35:34 +0200 Subject: [PATCH 121/165] chore: bump version to 0.4.3 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d4629e8..5f57fb3 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV := 0.4.2-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.4.1 +VERSION_DEV := 0.4.4-dev$(shell date +%Y%m%d%H%M) +VERSION := 0.4.3 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm From 58ade3b551767bec26dac1ef543c1a645a7e5f9f Mon Sep 17 00:00:00 2001 From: Ben Kochie Date: Thu, 12 Jun 2025 13:41:43 +0200 Subject: [PATCH 122/165] fix: Update metric naming (#602) Fix up metric names to follow best practice naming conventions[0]. [0]: https://prometheus.io/docs/practices/naming/ Signed-off-by: SuperQ --- cloud.go | 22 +++++++++++----------- internal/timesync/metrics.go | 26 +++++++++++++------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cloud.go b/cloud.go index fb1998a..cec749e 100644 --- a/cloud.go +++ b/cloud.go @@ -51,34 +51,34 @@ var ( ) metricCloudConnectionEstablishedTimestamp = promauto.NewGauge( prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_established_timestamp", + Name: "jetkvm_cloud_connection_established_timestamp_seconds", Help: "The timestamp when the cloud connection was established", }, ) metricConnectionLastPingTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_ping_timestamp", + Name: "jetkvm_connection_last_ping_timestamp_seconds", Help: "The timestamp when the last ping response was received", }, []string{"type", "source"}, ) metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_ping_received_timestamp", + Name: "jetkvm_connection_last_ping_received_timestamp_seconds", Help: "The timestamp when the last ping request was received", }, []string{"type", "source"}, ) metricConnectionLastPingDuration = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_ping_duration", + Name: "jetkvm_connection_last_ping_duration_seconds", Help: "The duration of the last ping response", }, []string{"type", "source"}, ) metricConnectionPingDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "jetkvm_connection_ping_duration", + Name: "jetkvm_connection_ping_duration_seconds", Help: "The duration of the ping response", Buckets: []float64{ 0.1, 0.5, 1, 10, @@ -88,28 +88,28 @@ var ( ) metricConnectionTotalPingSentCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_total_ping_sent", + Name: "jetkvm_connection_ping_sent_total", Help: "The total number of pings sent to the connection", }, []string{"type", "source"}, ) metricConnectionTotalPingReceivedCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_total_ping_received", + Name: "jetkvm_connection_ping_received_total", Help: "The total number of pings received from the connection", }, []string{"type", "source"}, ) metricConnectionSessionRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_session_total_requests", + Name: "jetkvm_connection_session_requests_total", Help: "The total number of session requests received", }, []string{"type", "source"}, ) metricConnectionSessionRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "jetkvm_connection_session_request_duration", + Name: "jetkvm_connection_session_request_duration_seconds", Help: "The duration of session requests", Buckets: []float64{ 0.1, 0.5, 1, 10, @@ -119,7 +119,7 @@ var ( ) metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_session_request_timestamp", + Name: "jetkvm_connection_last_session_request_timestamp_seconds", Help: "The timestamp of the last session request", }, []string{"type", "source"}, @@ -133,7 +133,7 @@ var ( ) metricCloudConnectionFailureCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_cloud_connection_failure_count", + Name: "jetkvm_cloud_connection_failure_total", Help: "The number of times the cloud connection has failed", }, ) diff --git a/internal/timesync/metrics.go b/internal/timesync/metrics.go index 0e28acb..5aa2e92 100644 --- a/internal/timesync/metrics.go +++ b/internal/timesync/metrics.go @@ -14,44 +14,44 @@ var ( ) metricTimeSyncCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_count", + Name: "jetkvm_timesync_total", Help: "The number of times the timesync has been run", }, ) metricTimeSyncSuccessCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_success_count", + Name: "jetkvm_timesync_success_total", Help: "The number of times the timesync has been successful", }, ) metricRTCUpdateCount = promauto.NewCounter( //nolint:unused prometheus.CounterOpts{ - Name: "jetkvm_timesync_rtc_update_count", + Name: "jetkvm_timesync_rtc_update_total", Help: "The number of times the RTC has been updated", }, ) metricNtpTotalSuccessCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_total_success_count", + Name: "jetkvm_timesync_ntp_total_success_total", Help: "The total number of successful NTP requests", }, ) metricNtpTotalRequestCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_total_request_count", + Name: "jetkvm_timesync_ntp_total_request_total", Help: "The total number of NTP requests sent", }, ) metricNtpSuccessCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_success_count", + Name: "jetkvm_timesync_ntp_success_total", Help: "The number of successful NTP requests", }, []string{"url"}, ) metricNtpRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_request_count", + Name: "jetkvm_timesync_ntp_request_total", Help: "The number of NTP requests sent to the server", }, []string{"url"}, @@ -83,39 +83,39 @@ var ( metricHttpTotalSuccessCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_total_success_count", + Name: "jetkvm_timesync_http_total_success_total", Help: "The total number of successful HTTP requests", }, ) metricHttpTotalRequestCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_total_request_count", + Name: "jetkvm_timesync_http_total_request_total", Help: "The total number of HTTP requests sent", }, ) metricHttpTotalCancelCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_total_cancel_count", + Name: "jetkvm_timesync_http_total_cancel_total", Help: "The total number of HTTP requests cancelled", }, ) metricHttpSuccessCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_success_count", + Name: "jetkvm_timesync_http_success_total", Help: "The number of successful HTTP requests", }, []string{"url"}, ) metricHttpRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_request_count", + Name: "jetkvm_timesync_http_request_total", Help: "The number of HTTP requests sent to the server", }, []string{"url"}, ) metricHttpCancelCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_cancel_count", + Name: "jetkvm_timesync_http_cancel_total", Help: "The number of HTTP requests cancelled", }, []string{"url"}, From b822b73a03fd4429a11512c9e7af216ca1db1ab3 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:04:51 +0200 Subject: [PATCH 123/165] chore: use pure Go resolver and remove CGO_ENABLED=0 (#603) --- Makefile | 9 ++++++--- dev_deploy.sh | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5f57fb3..16994a4 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,8 @@ VERSION := 0.4.3 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm +GO_BUILD_ARGS := -tags netgo +GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) GO_LDFLAGS := \ -s -w \ -X $(PROMETHEUS_TAG).Branch=$(BRANCH) \ @@ -15,7 +17,7 @@ GO_LDFLAGS := \ -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) -GO_CMD := GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 go +GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go BIN_DIR := $(shell pwd)/bin TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) @@ -27,7 +29,7 @@ build_dev: hash_resource @echo "Building..." $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ - -trimpath \ + $(GO_RELEASE_BUILD_ARGS) \ -o $(BIN_DIR)/jetkvm_app cmd/main.go build_test2json: @@ -50,6 +52,7 @@ build_dev_test: build_test2json build_gotestsum test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \ $(GO_CMD) test -v \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ + $(GO_BUILD_ARGS) \ -c -o $(BIN_DIR)/tests/$$test_filename $$test; \ echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \ done; \ @@ -71,7 +74,7 @@ build_release: frontend hash_resource @echo "Building release..." $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \ - -trimpath \ + $(GO_RELEASE_BUILD_ARGS) \ -o bin/jetkvm_app cmd/main.go release: diff --git a/dev_deploy.sh b/dev_deploy.sh index a2d32ed..059e416 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -174,7 +174,7 @@ cd "${REMOTE_PATH}" chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug +PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug EOF echo "Deployment complete." \ No newline at end of file From 19871517ec61a9a288e90c04029f14864f048efc Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:49:26 +0200 Subject: [PATCH 124/165] fix(timesync): queryMultipleHttp hanging if all servers are unreachable (#605) --- internal/timesync/http.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/timesync/http.go b/internal/timesync/http.go index 3a51463..ff0668a 100644 --- a/internal/timesync/http.go +++ b/internal/timesync/http.go @@ -95,16 +95,27 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now } else if errors.Is(err, context.Canceled) { metricHttpCancelCount.WithLabelValues(url).Inc() metricHttpTotalCancelCount.Inc() + results <- nil } else { scopedLogger.Warn(). Str("error", err.Error()). Int("status", status). Msg("failed to query HTTP server") + results <- nil } }(url) } - return <-results + for range urls { + result := <-results + if result == nil { + continue + } + now = result + return + } + + return } func queryHttpTime( From 772527849f8d079db63a9884bdf1824c646e2372 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Fri, 13 Jun 2025 00:51:09 +0200 Subject: [PATCH 125/165] chore: bump version to 0.4.4 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 16994a4..b9dfe62 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV := 0.4.4-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.4.3 +VERSION_DEV := 0.4.5-dev$(shell date +%Y%m%d%H%M) +VERSION := 0.4.4 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm From 1674a6666c5491521784e7cdeec56298f3d16b18 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:42:09 +0200 Subject: [PATCH 126/165] fix(ui/cloud): missing SettingsNetworkRoute (#608) --- ui/src/main.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/src/main.tsx b/ui/src/main.tsx index e3badd1..298a3d8 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -295,6 +295,10 @@ if (isOnDevice) { path: "hardware", element: , }, + { + path: "network", + element: , + }, { path: "access", children: [ @@ -350,10 +354,11 @@ if (isOnDevice) { loader: DeviceIdRename.loader, action: DeviceIdRename.action, }, - { - path: "devices", + { + path: "devices", element: , - loader: DevicesRoute.loader }, + loader: DevicesRoute.loader + }, ], }, ], From a1ed28c676927d055cf81b6923c06a23a83b0b8d Mon Sep 17 00:00:00 2001 From: Caedis Date: Mon, 16 Jun 2025 04:30:57 -0500 Subject: [PATCH 127/165] build: allow the versions in the Makefile to be overwritten with ENV variables (#619) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b9dfe62..2f3c74a 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV := 0.4.5-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.4.4 +VERSION_DEV ?= 0.4.5-dev$(shell date +%Y%m%d%H%M) +VERSION ?= 0.4.4 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm From ffeaf8cced5e2c0c4f7d4b862111a74e229ebf3e Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Thu, 19 Jun 2025 00:35:17 +0200 Subject: [PATCH 128/165] ui(actionBar): remove Ctrl + Alt + Del in favor of Keyboard Macros --- ui/src/components/ActionBar.tsx | 10 +++---- .../routes/devices.$id.settings.hardware.tsx | 27 +++---------------- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 7de4571..4289e42 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -1,6 +1,6 @@ import { MdOutlineContentPasteGo } from "react-icons/md"; import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; -import { FaKeyboard, FaLock} from "react-icons/fa6"; +import { FaKeyboard } from "react-icons/fa6"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { Fragment, useCallback, useRef } from "react"; import { CommandLineIcon } from "@heroicons/react/20/solid"; @@ -19,8 +19,6 @@ import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; import MountPopopover from "@/components/popovers/MountPopover"; import ExtensionPopover from "@/components/popovers/ExtensionPopover"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; -import useKeyboard from "@/hooks/useKeyboard"; -import { keys, modifiers } from "@/keyboardMappings"; export default function Actionbar({ requestFullscreen, @@ -58,8 +56,6 @@ export default function Actionbar({ [setDisableFocusTrap], ); - const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); - return (
    - {useSettingsStore().actionBarCtrlAltDel && ( + {/* {useSettingsStore().actionBarCtrlAltDel && (
    - )} + )} */}
    - )} */}
    +
    diff --git a/ui/src/routes/devices.$id.settings.general.reboot.tsx b/ui/src/routes/devices.$id.settings.general.reboot.tsx new file mode 100644 index 0000000..c6889f6 --- /dev/null +++ b/ui/src/routes/devices.$id.settings.general.reboot.tsx @@ -0,0 +1,66 @@ +import { useNavigate } from "react-router-dom"; +import { useCallback } from "react"; + +import { useJsonRpc } from "@/hooks/useJsonRpc"; +import { Button } from "@components/Button"; + +export default function SettingsGeneralRebootRoute() { + const navigate = useNavigate(); + const [send] = useJsonRpc(); + + const onConfirmUpdate = useCallback(() => { + // This is where we send the RPC to the golang binary + send("reboot", {force: true}); + }, [send]); + + { + /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ + } + return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; +} + +export function Dialog({ + onClose, + onConfirmUpdate, +}: { + onClose: () => void; + onConfirmUpdate: () => void; +}) { + + return ( +
    +
    + +
    +
    + ); +} + +function ConfirmationBox({ + onYes, + onNo, +}: { + onYes: () => void; + onNo: () => void; +}) { + return ( +
    +
    +

    + Reboot JetKVM +

    +

    + Do you want to proceed with rebooting the system? +

    + +
    +
    +
    +
    + ); +} From 584768bacfdc4b990f648272ab42007056206026 Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:04:47 +0200 Subject: [PATCH 149/165] chore: remove /device/ui-config.js endpoint (#678) --- web.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/web.go b/web.go index 059915c..21e17e7 100644 --- a/web.go +++ b/web.go @@ -97,9 +97,6 @@ func setupRouter() *gin.Engine { // We use this to determine if the device is setup r.GET("/device/status", handleDeviceStatus) - // We use this to provide the UI with the device configuration - r.GET("/device/ui-config.js", handleDeviceUIConfig) - // We use this to setup the device in the welcome page r.POST("/device/setup", handleSetup) @@ -694,21 +691,6 @@ func handleCloudState(c *gin.Context) { c.JSON(http.StatusOK, response) } -func handleDeviceUIConfig(c *gin.Context) { - config, _ := json.Marshal(gin.H{ - "CLOUD_API": config.CloudURL, - "DEVICE_VERSION": builtAppVersion, - }) - if config == nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"}) - return - } - - response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config) - - c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response)) -} - func handleSetup(c *gin.Context) { // Check if the device is already set up if config.LocalAuthMode != "" || config.HashedPassword != "" { From 11a095c0f672fc18e73ed3e2686cf6918632038a Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 11 Jul 2025 01:04:19 -0500 Subject: [PATCH 150/165] feat(ntp): enhances time sync with DHCP NTP and custom servers (#625) * Ensure the mDNS mode is set every time network state changes Eliminates (mostly) duplicate code * Add custom NTP and HTTP time sync servers Since the ordering may have been previously defaulted and saved as "ntp,http", but that was being ignored and fallback-defaults were being used, in Ordering, `ntp` means use the fallback NTP servers, and `http` means use the fallback HTTP URLs. Thus `ntp_user_provided` and `http_user_provided` are the user specified static lists. * Add support for using DHCP-provided NTP server --- internal/confparser/confparser_test.go | 4 +- internal/network/config.go | 4 +- internal/network/netif.go | 44 +++++++++- internal/timesync/http.go | 6 +- internal/timesync/metrics.go | 1 + internal/timesync/ntp.go | 30 +++++-- internal/timesync/timesync.go | 111 ++++++++++++++++++------- network.go | 8 ++ 8 files changed, 166 insertions(+), 42 deletions(-) diff --git a/internal/confparser/confparser_test.go b/internal/confparser/confparser_test.go index 07d057e..e14a1ea 100644 --- a/internal/confparser/confparser_test.go +++ b/internal/confparser/confparser_test.go @@ -43,9 +43,11 @@ type testNetworkConfig struct { LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` - TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"` TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` + TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"` + TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` } func TestValidateConfig(t *testing.T) { diff --git a/internal/network/config.go b/internal/network/config.go index 74ddf19..c8fe582 100644 --- a/internal/network/config.go +++ b/internal/network/config.go @@ -45,9 +45,11 @@ type NetworkConfig struct { LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` - TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"` TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` + TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"` + TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` } func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { diff --git a/internal/network/netif.go b/internal/network/netif.go index c5db806..5a8dab6 100644 --- a/internal/network/netif.go +++ b/internal/network/netif.go @@ -21,6 +21,7 @@ type NetworkInterfaceState struct { ipv6Addr *net.IP ipv6Addresses []IPv6Address ipv6LinkLocal *net.IP + ntpAddresses []*net.IP macAddr *net.HardwareAddr l *zerolog.Logger @@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS onInitialCheck: opts.OnInitialCheck, cbConfigChange: opts.OnConfigChange, config: opts.NetworkConfig, + ntpAddresses: make([]*net.IP, 0), } // create the dhcp client @@ -89,7 +91,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS opts.Logger.Error().Err(err).Msg("failed to update network state") return } - + _ = s.updateNtpServersFromLease(lease) _ = s.setHostnameIfNotSame() opts.OnDhcpLeaseChange(lease) @@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() string { return s.ipv6Addr.String() } +func (s *NetworkInterfaceState) NtpAddresses() []*net.IP { + return s.ntpAddresses +} + +func (s *NetworkInterfaceState) NtpAddressesString() []string { + ntpServers := []string{} + + if s != nil { + s.l.Debug().Any("s", s).Msg("getting NTP address strings") + + if len(s.ntpAddresses) > 0 { + for _, server := range s.ntpAddresses { + s.l.Debug().IPAddr("server", *server).Msg("converting NTP address") + ntpServers = append(ntpServers, server.String()) + } + } + } + + return ntpServers +} + func (s *NetworkInterfaceState) MAC() *net.HardwareAddr { return s.macAddr } @@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) { return dhcpTargetState, nil } +func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error { + if lease != nil && len(lease.NTPServers) > 0 { + s.l.Info().Msg("lease found, updating DHCP NTP addresses") + s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers)) + + for _, ntpServer := range lease.NTPServers { + if ntpServer != nil { + s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease") + s.ntpAddresses = append(s.ntpAddresses, &ntpServer) + } + } + } else { + s.l.Info().Msg("no NTP servers found in lease") + s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers)) + } + + return nil +} + func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error { dhcpTargetState, err := s.update() if err != nil { diff --git a/internal/timesync/http.go b/internal/timesync/http.go index ff0668a..703308c 100644 --- a/internal/timesync/http.go +++ b/internal/timesync/http.go @@ -19,9 +19,9 @@ var defaultHTTPUrls = []string{ // "http://www.msftconnecttest.com/connecttest.txt", } -func (t *TimeSync) queryAllHttpTime() (now *time.Time) { - chunkSize := 4 - httpUrls := t.httpUrls +func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) { + chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4)) + t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs") // shuffle the http urls to avoid always querying the same servers rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] }) diff --git a/internal/timesync/metrics.go b/internal/timesync/metrics.go index 5aa2e92..1c27c8b 100644 --- a/internal/timesync/metrics.go +++ b/internal/timesync/metrics.go @@ -73,6 +73,7 @@ var ( }, []string{"url"}, ) + metricNtpServerInfo = promauto.NewGaugeVec( prometheus.GaugeOpts{ Name: "jetkvm_timesync_ntp_server_info", diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go index d45112c..c32de2a 100644 --- a/internal/timesync/ntp.go +++ b/internal/timesync/ntp.go @@ -1,6 +1,7 @@ package timesync import ( + "context" "math/rand/v2" "strconv" "time" @@ -21,9 +22,9 @@ var defaultNTPServers = []string{ "3.pool.ntp.org", } -func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) { - chunkSize := 4 - ntpServers := t.ntpServers +func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) { + chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4)) + t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers") // shuffle the ntp servers to avoid always querying the same servers rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] }) @@ -46,6 +47,10 @@ type ntpResult struct { func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) { results := make(chan *ntpResult, len(servers)) + + _, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + for _, server := range servers { go func(server string) { scopedLogger := t.l.With(). @@ -66,15 +71,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no return } + if response.IsKissOfDeath() { + scopedLogger.Warn(). + Str("kiss_code", response.KissCode). + Msg("ignoring NTP server kiss of death") + results <- nil + return + } + + rtt := float64(response.RTT.Milliseconds()) + // set the last RTT metricNtpServerLastRTT.WithLabelValues( server, - ).Set(float64(response.RTT.Milliseconds())) + ).Set(rtt) // set the RTT histogram metricNtpServerRttHistogram.WithLabelValues( server, - ).Observe(float64(response.RTT.Milliseconds())) + ).Observe(rtt) // set the server info metricNtpServerInfo.WithLabelValues( @@ -91,10 +106,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no scopedLogger.Info(). Str("time", now.Format(time.RFC3339)). Str("reference", response.ReferenceString()). - Str("rtt", response.RTT.String()). + Float64("rtt", rtt). Str("clockOffset", response.ClockOffset.String()). Uint8("stratum", response.Stratum). Msg("NTP server returned time") + + cancel() + results <- &ntpResult{ now: now, offset: &response.ClockOffset, diff --git a/internal/timesync/timesync.go b/internal/timesync/timesync.go index e956cf9..db1c96e 100644 --- a/internal/timesync/timesync.go +++ b/internal/timesync/timesync.go @@ -28,9 +28,8 @@ type TimeSync struct { syncLock *sync.Mutex l *zerolog.Logger - ntpServers []string - httpUrls []string - networkConfig *network.NetworkConfig + networkConfig *network.NetworkConfig + dhcpNtpAddresses []string rtcDevicePath string rtcDevice *os.File //nolint:unused @@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync { } t := &TimeSync{ - syncLock: &sync.Mutex{}, - l: opts.Logger, - rtcDevicePath: rtcDevice, - rtcLock: &sync.Mutex{}, - preCheckFunc: opts.PreCheckFunc, - ntpServers: defaultNTPServers, - httpUrls: defaultHTTPUrls, - networkConfig: opts.NetworkConfig, + syncLock: &sync.Mutex{}, + l: opts.Logger, + dhcpNtpAddresses: []string{}, + rtcDevicePath: rtcDevice, + rtcLock: &sync.Mutex{}, + preCheckFunc: opts.PreCheckFunc, + networkConfig: opts.NetworkConfig, } if t.rtcDevicePath != "" { @@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync { return t } +func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) { + t.dhcpNtpAddresses = addresses +} + func (t *TimeSync) getSyncMode() SyncMode { syncMode := SyncMode{ + Ntp: true, + Http: true, + Ordering: []string{"ntp_dhcp", "ntp", "http"}, NtpUseFallback: true, HttpUseFallback: true, } - var syncModeString string if t.networkConfig != nil { - syncModeString = t.networkConfig.TimeSyncMode.String + switch t.networkConfig.TimeSyncMode.String { + case "ntp_only": + syncMode.Http = false + case "http_only": + syncMode.Ntp = false + } + if t.networkConfig.TimeSyncDisableFallback.Bool { syncMode.NtpUseFallback = false syncMode.HttpUseFallback = false } + + var syncOrdering = t.networkConfig.TimeSyncOrdering + if len(syncOrdering) > 0 { + syncMode.Ordering = syncOrdering + } } - switch syncModeString { - case "ntp_only": - syncMode.Ntp = true - case "http_only": - syncMode.Http = true - default: - syncMode.Ntp = true - syncMode.Http = true - } + t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode") return syncMode } - func (t *TimeSync) doTimeSync() { metricTimeSyncStatus.Set(0) for { @@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error { offset *time.Duration ) - syncMode := t.getSyncMode() - metricTimeSyncCount.Inc() - if syncMode.Ntp { - now, offset = t.queryNetworkTime() - } + syncMode := t.getSyncMode() - if syncMode.Http && now == nil { - now = t.queryAllHttpTime() +Orders: + for _, mode := range syncMode.Ordering { + switch mode { + case "ntp_user_provided": + if syncMode.Ntp { + t.l.Info().Msg("using NTP custom servers") + now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers) + if now != nil { + t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "ntp_dhcp": + if syncMode.Ntp { + t.l.Info().Msg("using NTP servers from DHCP") + now, offset = t.queryNetworkTime(t.dhcpNtpAddresses) + if now != nil { + t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "ntp": + if syncMode.Ntp && syncMode.NtpUseFallback { + t.l.Info().Msg("using NTP fallback") + now, offset = t.queryNetworkTime(defaultNTPServers) + if now != nil { + t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "http_user_provided": + if syncMode.Http { + t.l.Info().Msg("using HTTP custom URLs") + now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls) + if now != nil { + t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "http": + if syncMode.Http && syncMode.HttpUseFallback { + t.l.Info().Msg("using HTTP fallback") + now = t.queryAllHttpTime(defaultHTTPUrls) + if now != nil { + t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained") + break Orders + } + } + default: + t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping") + } } if now == nil { diff --git a/network.go b/network.go index 2208a47..211b860 100644 --- a/network.go +++ b/network.go @@ -19,6 +19,14 @@ func networkStateChanged() { // do not block the main thread go waitCtrlAndRequestDisplayUpdate(true) + if timeSync != nil { + if networkState != nil { + timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString()) + } + + timeSync.Sync() + } + // always restart mDNS when the network state changes if mDNS != nil { _ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode()) From 4a23f22a557e9ac551cd8e338e879c7dca919b4d Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 11 Jul 2025 01:06:17 -0500 Subject: [PATCH 151/165] chore: upgrade ui packages (#571) Move to current on all non-major upgrades Fixes the tainted hardware WebGL video renderer if video settings are at default (1.0) values ## Runtime | Package | From | To | |---|---|---| | @headlessui/react | 2.2.3 | 2.2.4 | | @vitejs/plugin-basic-ssl | 2.0.0 | 2.1.0 | | cva | 1.0.0-beta.3 | 1.0.0-beta.4 | | focus-trap-react | 11.0.3 | 11.0.4 | | framer-motion | 12.11.5 | 12.23.0 | | react-simple-keyboard | 3.8.72 | 3.8.89 | | tailwind-merge | 3.3.0 | 3.3.1 | | validator | 13.15.0 | 13.15.15 | ## Dev | Package | From | To | |---|---|---| | @eslint/compat | 1.2.9 | 1.3.1 | | @eslint/js | 9.26.0 | 9.30.1 | | @tailwindcss/postcss | 4.1.7 | 4.1.11 | | @tailwindcss/vite | 4.1.8 | 4.1.10 | | @types/react | 19.1.4 | 19.1.8 | | @types/react-dom | 19.1.5 | 19.1.6 | | @types/validator | 13.15.0 | 13.15.2 | | @typescript-eslint/eslint-plugin | 8.32.1 | 8.34.0 | | @typescript-eslint/parser | 8.32.1 | 8.35.1 | | @vitejs/plugin-react-swc | 3.9.0 | 3.10.2 | | eslint | 9.26.0 | 9.30.1 | | globals | 16.1.0 | 16.3.0 | | postcss | 8.5.3 | 8.5.6 | | prettier | 3.5.3 | 3.6.2 | | prettier-plugin-tailwindcss | 0.6.11 | 0.6.13 | | tailwindcss | 4.1.7 | 4.1.11 | --- ui/package-lock.json | 1385 +++++++++++++++-------------- ui/package.json | 50 +- ui/src/components/WebRTCVideo.tsx | 18 +- 3 files changed, 769 insertions(+), 684 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 8ac57a1..6380015 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,21 +8,21 @@ "name": "kvm-ui", "version": "0.0.0", "dependencies": { - "@headlessui/react": "^2.2.3", + "@headlessui/react": "^2.2.4", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", - "@vitejs/plugin-basic-ssl": "^2.0.0", + "@vitejs/plugin-basic-ssl": "^2.1.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", - "cva": "^1.0.0-beta.3", + "cva": "^1.0.0-beta.4", "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", - "focus-trap-react": "^11.0.3", - "framer-motion": "^12.11.4", + "focus-trap-react": "^11.0.4", + "framer-motion": "^12.23.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", @@ -31,42 +31,42 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.72", + "react-simple-keyboard": "^3.8.89", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", - "tailwind-merge": "^3.3.0", + "tailwind-merge": "^3.3.1", "usehooks-ts": "^3.1.1", - "validator": "^13.15.0", + "validator": "^13.15.15", "zustand": "^4.5.2" }, "devDependencies": { - "@eslint/compat": "^1.2.9", + "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.26.0", + "@eslint/js": "^9.30.1", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.1.7", + "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/typography": "^0.5.16", - "@tailwindcss/vite": "^4.1.7", - "@types/react": "^19.1.4", - "@types/react-dom": "^19.1.5", + "@tailwindcss/vite": "^4.1.11", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", - "@types/validator": "^13.15.0", - "@typescript-eslint/eslint-plugin": "^8.32.1", - "@typescript-eslint/parser": "^8.32.1", - "@vitejs/plugin-react-swc": "^3.9.0", + "@types/validator": "^13.15.2", + "@typescript-eslint/eslint-plugin": "^8.35.1", + "@typescript-eslint/parser": "^8.35.1", + "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.26.0", + "eslint": "^9.30.1", "eslint-config-prettier": "^10.1.5", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.1.0", - "postcss": "^8.5.3", - "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^4.1.7", + "globals": "^16.3.0", + "postcss": "^8.5.6", + "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.13", + "tailwindcss": "^4.1.11", "typescript": "^5.8.3", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" @@ -103,18 +103,18 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -128,9 +128,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -144,9 +144,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -208,9 +208,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -224,9 +224,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -288,9 +288,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -336,9 +336,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -368,9 +368,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -416,9 +416,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -448,9 +448,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -464,9 +464,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -480,9 +480,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -496,9 +496,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -539,16 +539,16 @@ } }, "node_modules/@eslint/compat": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.9.tgz", - "integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^9.10.0" + "eslint": "^8.40 || 9" }, "peerDependenciesMeta": { "eslint": { @@ -557,9 +557,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -571,9 +571,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -627,9 +627,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -648,35 +648,47 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react": { @@ -695,12 +707,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.2" }, "peerDependencies": { "react": ">=16.8.0", @@ -708,21 +720,21 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@headlessui/react": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.3.tgz", - "integrity": "sha512-hgOJGXPifPlOczIeSwX8OjLWRJ5XdYApZFf7DeCbCrO1PXHkPhNTRrA9ZwJsgAG7SON1i2JcvIreF/kbgtJeaQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", + "integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", - "@tanstack/react-virtual": "^3.13.6", + "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "engines": { @@ -829,18 +841,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -853,27 +861,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -920,14 +918,14 @@ } }, "node_modules/@react-aria/focus": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.3.tgz", - "integrity": "sha512-rR5uZUMSY4xLHmpK/I8bP1V6vUNHFo33gTvrvNUsAKKqvMfa7R2nu5A6v97dr5g6tVH6xzpdkPsOJCWh90H2cw==", + "version": "3.20.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz", + "integrity": "sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==", "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.25.1", - "@react-aria/utils": "^3.29.0", - "@react-types/shared": "^3.29.1", + "@react-aria/interactions": "^3.25.3", + "@react-aria/utils": "^3.29.1", + "@react-types/shared": "^3.30.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, @@ -937,15 +935,15 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.1.tgz", - "integrity": "sha512-ntLrlgqkmZupbbjekz3fE/n3eQH2vhncx8gUp0+N+GttKWevx7jos11JUBjnJwb1RSOPgRUFcrluOqBp0VgcfQ==", + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz", + "integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.8", - "@react-aria/utils": "^3.29.0", - "@react-stately/flags": "^3.1.1", - "@react-types/shared": "^3.29.1", + "@react-aria/ssr": "^3.9.9", + "@react-aria/utils": "^3.29.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.30.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -954,9 +952,9 @@ } }, "node_modules/@react-aria/ssr": { - "version": "3.9.8", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", - "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", + "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -969,15 +967,15 @@ } }, "node_modules/@react-aria/utils": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.0.tgz", - "integrity": "sha512-jSOrZimCuT1iKNVlhjIxDkAhgF7HSp3pqyT6qjg/ZoA0wfqCi/okmrMPiWSAKBnkgX93N8GYTLT3CIEO6WZe9Q==", + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz", + "integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.8", - "@react-stately/flags": "^3.1.1", - "@react-stately/utils": "^3.10.6", - "@react-types/shared": "^3.29.1", + "@react-aria/ssr": "^3.9.9", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.7", + "@react-types/shared": "^3.30.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, @@ -987,18 +985,18 @@ } }, "node_modules/@react-stately/flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", - "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" } }, "node_modules/@react-stately/utils": { - "version": "3.10.6", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", - "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz", + "integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -1008,9 +1006,9 @@ } }, "node_modules/@react-types/shared": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.1.tgz", - "integrity": "sha512-KtM+cDf2CXoUX439rfEhbnEdAgFZX20UP2A35ypNIawR7/PFFPjQDWyA2EnClCcW/dLWJDEPX2U8+EJff8xqmQ==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz", + "integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==", "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" @@ -1025,10 +1023,17 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", + "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", - "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", + "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", "cpu": [ "arm" ], @@ -1039,9 +1044,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", - "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", + "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", "cpu": [ "arm64" ], @@ -1052,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", - "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", + "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", "cpu": [ "arm64" ], @@ -1065,9 +1070,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", - "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", + "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", "cpu": [ "x64" ], @@ -1078,9 +1083,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", - "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", + "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", "cpu": [ "arm64" ], @@ -1091,9 +1096,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", - "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", + "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", "cpu": [ "x64" ], @@ -1104,9 +1109,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", - "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", + "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", "cpu": [ "arm" ], @@ -1117,9 +1122,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", - "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", + "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", "cpu": [ "arm" ], @@ -1130,9 +1135,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", - "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", + "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", "cpu": [ "arm64" ], @@ -1143,9 +1148,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", + "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", "cpu": [ "arm64" ], @@ -1156,9 +1161,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", - "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", + "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", "cpu": [ "loong64" ], @@ -1169,9 +1174,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", - "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", + "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", "cpu": [ "ppc64" ], @@ -1182,9 +1187,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", - "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", + "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", "cpu": [ "riscv64" ], @@ -1195,9 +1200,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", - "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", + "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", "cpu": [ "riscv64" ], @@ -1208,9 +1213,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", - "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", + "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", "cpu": [ "s390x" ], @@ -1221,9 +1226,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", + "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", "cpu": [ "x64" ], @@ -1234,9 +1239,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", + "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", "cpu": [ "x64" ], @@ -1247,9 +1252,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", - "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", + "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", "cpu": [ "arm64" ], @@ -1260,9 +1265,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", - "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", + "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", "cpu": [ "ia32" ], @@ -1273,9 +1278,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", - "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", + "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", "cpu": [ "x64" ], @@ -1292,15 +1297,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", - "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.9.tgz", + "integrity": "sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.21" + "@swc/types": "^0.1.23" }, "engines": { "node": ">=10" @@ -1310,16 +1315,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.24", - "@swc/core-darwin-x64": "1.11.24", - "@swc/core-linux-arm-gnueabihf": "1.11.24", - "@swc/core-linux-arm64-gnu": "1.11.24", - "@swc/core-linux-arm64-musl": "1.11.24", - "@swc/core-linux-x64-gnu": "1.11.24", - "@swc/core-linux-x64-musl": "1.11.24", - "@swc/core-win32-arm64-msvc": "1.11.24", - "@swc/core-win32-ia32-msvc": "1.11.24", - "@swc/core-win32-x64-msvc": "1.11.24" + "@swc/core-darwin-arm64": "1.12.9", + "@swc/core-darwin-x64": "1.12.9", + "@swc/core-linux-arm-gnueabihf": "1.12.9", + "@swc/core-linux-arm64-gnu": "1.12.9", + "@swc/core-linux-arm64-musl": "1.12.9", + "@swc/core-linux-x64-gnu": "1.12.9", + "@swc/core-linux-x64-musl": "1.12.9", + "@swc/core-win32-arm64-msvc": "1.12.9", + "@swc/core-win32-ia32-msvc": "1.12.9", + "@swc/core-win32-x64-msvc": "1.12.9" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1331,9 +1336,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", - "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz", + "integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==", "cpu": [ "arm64" ], @@ -1348,9 +1353,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", - "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.9.tgz", + "integrity": "sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==", "cpu": [ "x64" ], @@ -1365,9 +1370,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", - "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.9.tgz", + "integrity": "sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==", "cpu": [ "arm" ], @@ -1382,9 +1387,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", - "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz", + "integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==", "cpu": [ "arm64" ], @@ -1399,9 +1404,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", - "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.9.tgz", + "integrity": "sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==", "cpu": [ "arm64" ], @@ -1416,9 +1421,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", - "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz", + "integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==", "cpu": [ "x64" ], @@ -1433,9 +1438,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", - "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz", + "integrity": "sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==", "cpu": [ "x64" ], @@ -1450,9 +1455,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", - "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.9.tgz", + "integrity": "sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==", "cpu": [ "arm64" ], @@ -1467,9 +1472,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", - "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.9.tgz", + "integrity": "sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==", "cpu": [ "ia32" ], @@ -1484,9 +1489,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", - "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.9.tgz", + "integrity": "sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==", "cpu": [ "x64" ], @@ -1517,9 +1522,9 @@ } }, "node_modules/@swc/types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", - "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1540,9 +1545,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", - "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1552,13 +1557,13 @@ "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.7" + "tailwindcss": "4.1.11" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", - "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1570,24 +1575,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-x64": "4.1.7", - "@tailwindcss/oxide-freebsd-x64": "4.1.7", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-x64-musl": "4.1.7", - "@tailwindcss/oxide-wasm32-wasi": "4.1.7", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", - "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", "cpu": [ "arm64" ], @@ -1602,9 +1607,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", - "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", "cpu": [ "arm64" ], @@ -1619,9 +1624,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", - "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", "cpu": [ "x64" ], @@ -1636,9 +1641,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", - "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", "cpu": [ "x64" ], @@ -1653,9 +1658,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", - "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", "cpu": [ "arm" ], @@ -1670,9 +1675,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", - "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", "cpu": [ "arm64" ], @@ -1687,9 +1692,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", - "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", "cpu": [ "arm64" ], @@ -1704,9 +1709,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", - "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", "cpu": [ "x64" ], @@ -1721,9 +1726,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", - "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", "cpu": [ "x64" ], @@ -1738,9 +1743,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", - "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1759,7 +1764,7 @@ "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", + "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, @@ -1768,9 +1773,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", - "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", "cpu": [ "arm64" ], @@ -1785,9 +1790,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", - "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", "cpu": [ "x64" ], @@ -1802,17 +1807,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.7.tgz", - "integrity": "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", - "tailwindcss": "4.1.7" + "tailwindcss": "4.1.11" } }, "node_modules/@tailwindcss/typography": { @@ -1832,27 +1837,27 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", - "integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "tailwindcss": "4.1.7" + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" }, "peerDependencies": { - "vite": "^5.2.0 || ^6" + "vite": "^5.2.0 || ^6 || ^7" } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.8", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.8.tgz", - "integrity": "sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==", + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.8" + "@tanstack/virtual-core": "3.13.12" }, "funding": { "type": "github", @@ -1864,9 +1869,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.8", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.8.tgz", - "integrity": "sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==", + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", "license": "MIT", "funding": { "type": "github", @@ -1937,9 +1942,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1955,18 +1960,18 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -1980,24 +1985,24 @@ "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", + "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/type-utils": "8.35.1", + "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2011,15 +2016,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.35.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2027,16 +2032,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", + "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4" }, "engines": { @@ -2051,15 +2056,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", + "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.35.1", + "@typescript-eslint/types": "^8.35.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", + "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2069,15 +2096,32 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", + "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", + "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/utils": "8.35.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2094,9 +2138,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", + "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", "dev": true, "license": "MIT", "engines": { @@ -2108,14 +2152,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", + "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.35.1", + "@typescript-eslint/tsconfig-utils": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2135,9 +2181,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2161,16 +2207,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", + "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2185,14 +2231,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", + "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.35.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2203,9 +2249,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2216,28 +2262,29 @@ } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.0.0.tgz", - "integrity": "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", "license": "MIT", "engines": { "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "peerDependencies": { - "vite": "^6.0.0" + "vite": "^6.0.0 || ^7.0.0" } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", - "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", + "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==", "dev": true, "license": "MIT", "dependencies": { - "@swc/core": "^1.11.21" + "@rolldown/pluginutils": "1.0.0-beta.11", + "@swc/core": "^1.11.31" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6" + "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" } }, "node_modules/@xterm/addon-clipboard": { @@ -2295,9 +2342,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2369,17 +2416,19 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2573,9 +2622,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2596,9 +2645,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -2616,8 +2665,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2685,9 +2734,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "dev": true, "funding": [ { @@ -2798,9 +2847,9 @@ "license": "MIT" }, "node_modules/cva": { - "version": "1.0.0-beta.3", - "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.3.tgz", - "integrity": "sha512-CZa8pTkpEygxJRLH9aod/wfnSgK5z/0GJqG/NNehlwam+S8llqCWUXS3eCenvAiW5sTUpwTWE6bJaeeZ/b4pzA==", + "version": "1.0.0-beta.4", + "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.4.tgz", + "integrity": "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ==", "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" @@ -3105,16 +3154,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.179", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", + "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3126,27 +3175,27 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -3158,21 +3207,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -3181,7 +3233,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -3293,9 +3345,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3305,31 +3357,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { @@ -3355,18 +3407,18 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3378,9 +3430,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3463,9 +3515,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "license": "MIT", "dependencies": { "debug": "^3.2.7" @@ -3489,29 +3541,29 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -3624,9 +3676,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -3652,9 +3704,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3664,14 +3716,14 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3681,9 +3733,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3868,21 +3920,21 @@ "license": "ISC" }, "node_modules/focus-trap": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", - "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", + "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", "license": "MIT", "dependencies": { "tabbable": "^6.2.0" } }, "node_modules/focus-trap-react": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-11.0.3.tgz", - "integrity": "sha512-tS1+enWS/gwCHk2WIF3KpM2oz7Y3HsnRImzHZNRgCBLWXzNG4XQVlJgbqdLr4lBKRXGdDBjQYitSh1bf2xe4Ag==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-11.0.4.tgz", + "integrity": "sha512-tC7jC/yqeAqhe4irNIzdyDf9XCtGSeECHiBSYJBO/vIN0asizbKZCt8TarB6/XqIceu42ajQ/U4lQJ9pZlWjrg==", "license": "MIT", "dependencies": { - "focus-trap": "^7.6.4", + "focus-trap": "^7.6.5", "tabbable": "^6.2.0" }, "peerDependencies": { @@ -3922,13 +3974,13 @@ } }, "node_modules/framer-motion": { - "version": "12.12.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.12.1.tgz", - "integrity": "sha512-PFw4/GCREHI2suK/NlPSUxd+x6Rkp80uQsfCRFSOQNrm5pZif7eGtmG1VaD/UF1fW9tRBy5AaS77StatB3OJDg==", + "version": "12.23.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz", + "integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==", "license": "MIT", "dependencies": { - "motion-dom": "^12.12.1", - "motion-utils": "^12.12.1", + "motion-dom": "^12.22.0", + "motion-utils": "^12.19.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -4067,9 +4119,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -4474,6 +4526,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5175,18 +5239,18 @@ } }, "node_modules/motion-dom": { - "version": "12.12.1", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.12.1.tgz", - "integrity": "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ==", + "version": "12.22.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz", + "integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==", "license": "MIT", "dependencies": { - "motion-utils": "^12.12.1" + "motion-utils": "^12.19.0" } }, "node_modules/motion-utils": { - "version": "12.12.1", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz", - "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==", + "version": "12.19.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", + "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", "license": "MIT" }, "node_modules/ms": { @@ -5481,9 +5545,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5500,7 +5564,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5539,9 +5603,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -5555,9 +5619,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", - "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", + "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", "dev": true, "license": "MIT", "engines": { @@ -5741,9 +5805,9 @@ "license": "MIT" }, "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.0" @@ -5756,13 +5820,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.0", - "react-router": "6.30.0" + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -5773,9 +5837,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.72", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.72.tgz", - "integrity": "sha512-C34MVOykLrlONImOdJrSaXrNloTxOtUStipQaINR5Bem5JLFHEszNedVBEmxnt7Fqb7CTIIZ7fSgR8FasRIbzw==", + "version": "3.8.89", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.89.tgz", + "integrity": "sha512-F3a29Uvp7Zf92YM84DidqBTGQjk78Yzqm7xZBjsW7rGXis5Yhakw+7CWeBQjHAbwNMdYAMoBq34AjGYemlXGSQ==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -5829,9 +5893,9 @@ } }, "node_modules/recharts": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", - "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", @@ -5949,12 +6013,12 @@ } }, "node_modules/rollup": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", - "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", + "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5964,26 +6028,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.0", - "@rollup/rollup-android-arm64": "4.41.0", - "@rollup/rollup-darwin-arm64": "4.41.0", - "@rollup/rollup-darwin-x64": "4.41.0", - "@rollup/rollup-freebsd-arm64": "4.41.0", - "@rollup/rollup-freebsd-x64": "4.41.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", - "@rollup/rollup-linux-arm-musleabihf": "4.41.0", - "@rollup/rollup-linux-arm64-gnu": "4.41.0", - "@rollup/rollup-linux-arm64-musl": "4.41.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-musl": "4.41.0", - "@rollup/rollup-linux-s390x-gnu": "4.41.0", - "@rollup/rollup-linux-x64-gnu": "4.41.0", - "@rollup/rollup-linux-x64-musl": "4.41.0", - "@rollup/rollup-win32-arm64-msvc": "4.41.0", - "@rollup/rollup-win32-ia32-msvc": "4.41.0", - "@rollup/rollup-win32-x64-msvc": "4.41.0", + "@rollup/rollup-android-arm-eabi": "4.44.1", + "@rollup/rollup-android-arm64": "4.44.1", + "@rollup/rollup-darwin-arm64": "4.44.1", + "@rollup/rollup-darwin-x64": "4.44.1", + "@rollup/rollup-freebsd-arm64": "4.44.1", + "@rollup/rollup-freebsd-x64": "4.44.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", + "@rollup/rollup-linux-arm-musleabihf": "4.44.1", + "@rollup/rollup-linux-arm64-gnu": "4.44.1", + "@rollup/rollup-linux-arm64-musl": "4.44.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-musl": "4.44.1", + "@rollup/rollup-linux-s390x-gnu": "4.44.1", + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1", + "@rollup/rollup-win32-arm64-msvc": "4.44.1", + "@rollup/rollup-win32-ia32-msvc": "4.44.1", + "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" } }, @@ -6230,6 +6294,19 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6377,9 +6454,9 @@ "license": "MIT" }, "node_modules/tailwind-merge": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", - "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "license": "MIT", "funding": { "type": "github", @@ -6387,15 +6464,15 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", - "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -6427,9 +6504,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -6443,9 +6520,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -6495,9 +6572,9 @@ } }, "node_modules/tsconfck": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", - "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "dev": true, "license": "MIT", "bin": { @@ -6723,9 +6800,9 @@ "license": "MIT" }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -6848,9 +6925,9 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" diff --git a/ui/package.json b/ui/package.json index eb9a9a3..8723767 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,21 +19,21 @@ "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.2.3", + "@headlessui/react": "^2.2.4", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", - "@vitejs/plugin-basic-ssl": "^2.0.0", + "@vitejs/plugin-basic-ssl": "^2.1.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", - "cva": "^1.0.0-beta.3", + "cva": "^1.0.0-beta.4", "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", - "focus-trap-react": "^11.0.3", - "framer-motion": "^12.11.4", + "focus-trap-react": "^11.0.4", + "framer-motion": "^12.23.0", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", @@ -42,42 +42,42 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.72", + "react-simple-keyboard": "^3.8.89", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", - "tailwind-merge": "^3.3.0", + "tailwind-merge": "^3.3.1", "usehooks-ts": "^3.1.1", - "validator": "^13.15.0", + "validator": "^13.15.15", "zustand": "^4.5.2" }, "devDependencies": { - "@eslint/compat": "^1.2.9", + "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.26.0", + "@eslint/js": "^9.30.1", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.1.7", + "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/typography": "^0.5.16", - "@tailwindcss/vite": "^4.1.7", - "@types/react": "^19.1.4", - "@types/react-dom": "^19.1.5", + "@tailwindcss/vite": "^4.1.11", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", - "@types/validator": "^13.15.0", - "@typescript-eslint/eslint-plugin": "^8.32.1", - "@typescript-eslint/parser": "^8.32.1", - "@vitejs/plugin-react-swc": "^3.9.0", + "@types/validator": "^13.15.2", + "@typescript-eslint/eslint-plugin": "^8.35.1", + "@typescript-eslint/parser": "^8.35.1", + "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.26.0", + "eslint": "^9.30.1", "eslint-config-prettier": "^10.1.5", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.1.0", - "postcss": "^8.5.3", - "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^4.1.7", + "globals": "^16.3.0", + "postcss": "^8.5.6", + "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.13", + "tailwindcss": "^4.1.11", "typescript": "^5.8.3", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 571fac8..4312c91 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -657,6 +657,16 @@ export default function WebRTCVideo() { return true; }, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]); + // Conditionally set the filter style so we don't fallback to software rendering if these values are default of 1.0 + const videoStyle = useMemo(() => { + const isDefault = videoSaturation === 1.0 && videoBrightness === 1.0 && videoContrast === 1.0; + return isDefault + ? {} // No filter if all settings are default (1.0) + : { + filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`, + }; + }, [videoSaturation, videoBrightness, videoContrast]); + return (
    @@ -691,17 +701,15 @@ export default function WebRTCVideo() {
    +
    + +
    +
    + { + handleProxyChange(e.target.value); + }} + /> +
    +
    +
    +
    From 3359f8fca4fe7ee69207400e60e995e5412fa99e Mon Sep 17 00:00:00 2001 From: rmschooley Date: Fri, 11 Jul 2025 10:43:37 -0500 Subject: [PATCH 157/165] Remove Out Endpoint Descriptors from Absolute Mouse and Relative Mouse (#542) * Update hid_mouse_absolute.go Added attribute to remove unnecessary out endpoint. * Update hid_mouse_relative.go Added attribute to remove unnecessary out endpoint. * Update hid_keyboard.go Added attribute to explicitly keep currently needed out endpoint and to make listed attributes consistent across the keyboard and mouse devices. --------- Co-authored-by: Aveline <352441+ym@users.noreply.github.com> --- internal/usbgadget/hid_keyboard.go | 7 ++++--- internal/usbgadget/hid_mouse_absolute.go | 7 ++++--- internal/usbgadget/hid_mouse_relative.go | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index dbd5d5a..a087145 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -14,9 +14,10 @@ var keyboardConfig = gadgetConfigItem{ path: []string{"functions", "hid.usb0"}, configPath: []string{"hid.usb0"}, attrs: gadgetAttributes{ - "protocol": "1", - "subclass": "1", - "report_length": "8", + "protocol": "1", + "subclass": "1", + "report_length": "8", + "no_out_endpoint": "0", }, reportDesc: keyboardReportDesc, } diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index 7ba9958..2718f20 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -11,9 +11,10 @@ var absoluteMouseConfig = gadgetConfigItem{ path: []string{"functions", "hid.usb1"}, configPath: []string{"hid.usb1"}, attrs: gadgetAttributes{ - "protocol": "2", - "subclass": "0", - "report_length": "6", + "protocol": "2", + "subclass": "0", + "report_length": "6", + "no_out_endpoint": "1", }, reportDesc: absoluteMouseCombinedReportDesc, } diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index c59fda3..786f265 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -11,9 +11,10 @@ var relativeMouseConfig = gadgetConfigItem{ path: []string{"functions", "hid.usb2"}, configPath: []string{"hid.usb2"}, attrs: gadgetAttributes{ - "protocol": "2", - "subclass": "1", - "report_length": "4", + "protocol": "2", + "subclass": "1", + "report_length": "4", + "no_out_endpoint": "1", }, reportDesc: relativeMouseCombinedReportDesc, } From 5fb8d866ba5138af1a3b8ed43f07fe1c9ddf2459 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 11 Jul 2025 10:49:06 -0500 Subject: [PATCH 158/165] refactor(ui): Refactor the keyboardLayouts (#497) Add missing keyboard mappings for most layouts Change pasteModel.tsx to use the new structure and vastly clarified the way that keys are emitted. Make each layout export just the KeyboardLayout object (which is a package of isoCode, name, and chars) Made keyboardLayouts.ts export a function to select keyboard by `isoCode`, export the keyboards as label . value pairs (for a select list) and the list of keyboards. Changed devices.$id.settings.keyboard.tsx use the exported keyboard option list. --- ui/src/components/Terminal.tsx | 14 ++-- ui/src/components/popovers/PasteModal.tsx | 73 ++++++++++--------- .../components/popovers/WakeOnLan/Index.tsx | 12 +-- ui/src/hooks/stores.ts | 2 +- ui/src/keyboardLayouts.ts | 69 +++++++----------- ui/src/keyboardLayouts/cs_CZ.ts | 12 ++- ui/src/keyboardLayouts/de_CH.ts | 12 ++- ui/src/keyboardLayouts/de_DE.ts | 12 ++- ui/src/keyboardLayouts/en_UK.ts | 12 ++- ui/src/keyboardLayouts/en_US.ts | 12 ++- ui/src/keyboardLayouts/es_ES.ts | 12 ++- ui/src/keyboardLayouts/fr_BE.ts | 12 ++- ui/src/keyboardLayouts/fr_CH.ts | 16 ++-- ui/src/keyboardLayouts/fr_FR.ts | 12 ++- ui/src/keyboardLayouts/it_IT.ts | 12 ++- ui/src/keyboardLayouts/nb_NO.ts | 12 ++- ui/src/keyboardLayouts/sv_SE.ts | 12 ++- ui/src/keyboardMappings.ts | 52 ++++++++----- .../routes/devices.$id.settings.keyboard.tsx | 4 +- ui/src/routes/devices.$id.settings.tsx | 2 +- ui/src/routes/devices.$id.tsx | 4 +- 21 files changed, 232 insertions(+), 148 deletions(-) diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx index 5451afe..f5d662d 100644 --- a/ui/src/components/Terminal.tsx +++ b/ui/src/components/Terminal.tsx @@ -67,19 +67,19 @@ function Terminal({ }) { const enableTerminal = useUiStore(state => state.terminalType == type); const setTerminalType = useUiStore(state => state.setTerminalType); - const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); + const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG }); useEffect(() => { setTimeout(() => { - setDisableKeyboardFocusTrap(enableTerminal); + setDisableVideoFocusTrap(enableTerminal); }, 500); return () => { - setDisableKeyboardFocusTrap(false); + setDisableVideoFocusTrap(false); }; - }, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]); + }, [enableTerminal, setDisableVideoFocusTrap]); const readyState = dataChannel.readyState; useEffect(() => { @@ -116,7 +116,7 @@ function Terminal({ const { domEvent } = e; if (domEvent.key === "Escape") { setTerminalType("none"); - setDisableKeyboardFocusTrap(false); + setDisableVideoFocusTrap(false); domEvent.preventDefault(); } }); @@ -131,7 +131,7 @@ function Terminal({ onDataHandler.dispose(); onKeyHandler.dispose(); }; - }, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]); + }, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]); useEffect(() => { if (!instance) return; @@ -158,7 +158,7 @@ function Terminal({ return () => { window.removeEventListener("resize", handleResize); }; - }, [ref, instance]); + }, [instance]); return (
    { - return { keys, modifier }; +const hidKeyboardPayload = (modifier: number, keys: number[]) => { + return { modifier, keys }; }; const modifierCode = (shift?: boolean, altRight?: boolean) => { @@ -62,49 +62,56 @@ export default function PasteModal() { const onConfirmPaste = useCallback(async () => { setPasteMode(false); setDisableVideoFocusTrap(false); + if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; - if (!safeKeyboardLayout) return; - if (!chars[safeKeyboardLayout]) return; + const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout); + if (!keyboard) return; + const text = TextAreaRef.current.value; try { for (const char of text) { - const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char] + const keyprops = keyboard.chars[char]; + if (!keyprops) continue; + + const { key, shift, altRight, deadKey, accentKey } = keyprops; if (!key) continue; - const keyz = [ keys[key] ]; - const modz = [ modifierCode(shift, altRight) ]; - - if (deadKey) { - keyz.push(keys["Space"]); - modz.push(noModifier); - } + // if this is an accented character, we need to send that accent FIRST if (accentKey) { - keyz.unshift(keys[accentKey.key]) - modz.unshift(modifierCode(accentKey.shift, accentKey.altRight)) + await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] }) } - for (const [index, kei] of keyz.entries()) { - await new Promise((resolve, reject) => { - send( - "keyboardReport", - hidKeyboardPayload([kei], modz[index]), - params => { - if ("error" in params) return reject(params.error); - send("keyboardReport", hidKeyboardPayload([], 0), params => { - if ("error" in params) return reject(params.error); - resolve(); - }); - }, - ); - }); + // now send the actual key + await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]}); + + // if what was requested was a dead key, we need to send an unmodified space to emit + // just the accent character + if (deadKey) { + await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] }); } + + // now send a message with no keys down to "release" the keys + await sendKeystroke({ modifier: 0, keys: [] }); } } catch (error) { - console.error(error); + console.error("Failed to paste text:", error); notifications.error("Failed to paste text"); } - }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]); + + async function sendKeystroke(stroke: KeyStroke) { + await new Promise((resolve, reject) => { + send( + "keyboardReport", + hidKeyboardPayload(stroke.modifier, stroke.keys), + params => { + if ("error" in params) return reject(params.error); + resolve(); + } + ); + }); + } + }, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]); useEffect(() => { if (TextAreaRef.current) { @@ -154,7 +161,7 @@ export default function PasteModal() { // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments [...new Intl.Segmenter().segment(value)] .map(x => x.segment) - .filter(char => !chars[safeKeyboardLayout][char]), + .filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]), ), ]; @@ -175,7 +182,7 @@ export default function PasteModal() {

    - Sending text using keyboard layout: {layouts[safeKeyboardLayout]} + Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}

    diff --git a/ui/src/components/popovers/WakeOnLan/Index.tsx b/ui/src/components/popovers/WakeOnLan/Index.tsx index f4f4951..1cf7f18 100644 --- a/ui/src/components/popovers/WakeOnLan/Index.tsx +++ b/ui/src/components/popovers/WakeOnLan/Index.tsx @@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm"; export default function WakeOnLanModal() { const [storedDevices, setStoredDevices] = useState([]); const [showAddForm, setShowAddForm] = useState(false); - const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); + const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); @@ -24,9 +24,9 @@ export default function WakeOnLanModal() { const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState(null); const onCancelWakeOnLanModal = useCallback(() => { + setDisableVideoFocusTrap(false); close(); - setDisableFocusTrap(false); - }, [close, setDisableFocusTrap]); + }, [close, setDisableVideoFocusTrap]); const onSendMagicPacket = useCallback( (macAddress: string) => { @@ -43,12 +43,12 @@ export default function WakeOnLanModal() { } } else { notifications.success("Magic Packet sent successfully"); - setDisableFocusTrap(false); + setDisableVideoFocusTrap(false); close(); } }); }, - [close, rpcDataChannel?.readyState, send, setDisableFocusTrap], + [close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap], ); const syncStoredDevices = useCallback(() => { @@ -78,7 +78,7 @@ export default function WakeOnLanModal() { } }); }, - [storedDevices, send, syncStoredDevices], + [send, storedDevices, syncStoredDevices], ); const onAddDevice = useCallback( diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 7d65666..aa29528 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -936,5 +936,5 @@ export const useMacrosStore = create((set, get) => ({ } finally { set({ loading: false }); } - }, + } })); diff --git a/ui/src/keyboardLayouts.ts b/ui/src/keyboardLayouts.ts index 3b835b1..4ae3ad9 100644 --- a/ui/src/keyboardLayouts.ts +++ b/ui/src/keyboardLayouts.ts @@ -1,45 +1,32 @@ -import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE" -import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ" -import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK" -import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US" -import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR" -import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE" -import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT" -import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO" -import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES" -import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE" -import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH" -import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH" +export interface KeyStroke { modifier: number; keys: number[]; } +export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean } +export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo } +export interface KeyboardLayout { isoCode: string, name: string, chars: Record } -interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean } -export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo } +// to add a new layout, create a file like the above and add it to the list +import { cs_CZ } from "@/keyboardLayouts/cs_CZ" +import { de_CH } from "@/keyboardLayouts/de_CH" +import { de_DE } from "@/keyboardLayouts/de_DE" +import { en_US } from "@/keyboardLayouts/en_US" +import { en_UK } from "@/keyboardLayouts/en_UK" +import { es_ES } from "@/keyboardLayouts/es_ES" +import { fr_BE } from "@/keyboardLayouts/fr_BE" +import { fr_CH } from "@/keyboardLayouts/fr_CH" +import { fr_FR } from "@/keyboardLayouts/fr_FR" +import { it_IT } from "@/keyboardLayouts/it_IT" +import { nb_NO } from "@/keyboardLayouts/nb_NO" +import { sv_SE } from "@/keyboardLayouts/sv_SE" -export const layouts: Record = { - be_FR: name_fr_BE, - cs_CZ: name_cs_CZ, - en_UK: name_en_UK, - en_US: name_en_US, - fr_FR: name_fr_FR, - de_DE: name_de_DE, - it_IT: name_it_IT, - nb_NO: name_nb_NO, - es_ES: name_es_ES, - sv_SE: name_sv_SE, - fr_CH: name_fr_CH, - de_CH: name_de_CH, -} +export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ]; -export const chars: Record> = { - be_FR: chars_fr_BE, - cs_CZ: chars_cs_CZ, - en_UK: chars_en_UK, - en_US: chars_en_US, - fr_FR: chars_fr_FR, - de_DE: chars_de_DE, - it_IT: chars_it_IT, - nb_NO: chars_nb_NO, - es_ES: chars_es_ES, - sv_SE: chars_sv_SE, - fr_CH: chars_fr_CH, - de_CH: chars_de_CH, +export const selectedKeyboard = (isoCode: string): KeyboardLayout => { + // fallback to original behaviour of en-US if no isoCode given + return keyboards.find(keyboard => keyboard.isoCode == isoCode) + ?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!; }; + +export const keyboardOptions = () => { + return keyboards.map((keyboard) => { + return { label: keyboard.name, value: keyboard.isoCode } + }); +} diff --git a/ui/src/keyboardLayouts/cs_CZ.ts b/ui/src/keyboardLayouts/cs_CZ.ts index a289d75..e4f8822 100644 --- a/ui/src/keyboardLayouts/cs_CZ.ts +++ b/ui/src/keyboardLayouts/cs_CZ.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Čeština"; +const name = "Čeština"; const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter @@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -242,3 +242,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const cs_CZ: KeyboardLayout = { + isoCode: "cs-CZ", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/de_CH.ts b/ui/src/keyboardLayouts/de_CH.ts index 06c0619..4743bcf 100644 --- a/ui/src/keyboardLayouts/de_CH.ts +++ b/ui/src/keyboardLayouts/de_CH.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Schwiizerdütsch"; +const name = "Schwiizerdütsch"; const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -163,3 +163,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const de_CH: KeyboardLayout = { + isoCode: "de-CH", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/de_DE.ts b/ui/src/keyboardLayouts/de_DE.ts index 87a8d2e..89b7eed 100644 --- a/ui/src/keyboardLayouts/de_DE.ts +++ b/ui/src/keyboardLayouts/de_DE.ts @@ -1,12 +1,12 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Deutsch"; +const name = "Deutsch"; const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, "Â": { key: "KeyA", shift: true, accentKey: keyHat }, @@ -150,3 +150,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const de_DE: KeyboardLayout = { + isoCode: "de-DE", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/en_UK.ts b/ui/src/keyboardLayouts/en_UK.ts index ed0c8dd..a5ef779 100644 --- a/ui/src/keyboardLayouts/en_UK.ts +++ b/ui/src/keyboardLayouts/en_UK.ts @@ -1,8 +1,8 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "English (UK)"; +const name = "English (UK)"; -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -105,3 +105,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record + +export const en_UK: KeyboardLayout = { + isoCode: "en-UK", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/en_US.ts b/ui/src/keyboardLayouts/en_US.ts index 592bf27..cd7aaf6 100644 --- a/ui/src/keyboardLayouts/en_US.ts +++ b/ui/src/keyboardLayouts/en_US.ts @@ -1,8 +1,8 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "English (US)"; +const name = "English (US)"; -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -111,3 +111,9 @@ export const chars = { Insert: { key: "Insert", shift: false }, Delete: { key: "Delete", shift: false }, } as Record + +export const en_US: KeyboardLayout = { + isoCode: "en-US", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/es_ES.ts b/ui/src/keyboardLayouts/es_ES.ts index 47fc230..9eb1d6a 100644 --- a/ui/src/keyboardLayouts/es_ES.ts +++ b/ui/src/keyboardLayouts/es_ES.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Español"; +const name = "Español"; const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -166,3 +166,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const es_ES: KeyboardLayout = { + isoCode: "es-ES", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/fr_BE.ts b/ui/src/keyboardLayouts/fr_BE.ts index 2b8b34c..bd417e0 100644 --- a/ui/src/keyboardLayouts/fr_BE.ts +++ b/ui/src/keyboardLayouts/fr_BE.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Belgisch Nederlands"; +const name = "Belgisch Nederlands"; const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter @@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyQ", shift: true }, "Ä": { key: "KeyQ", shift: true, accentKey: keyTrema }, "Â": { key: "KeyQ", shift: true, accentKey: keyHat }, @@ -165,3 +165,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const fr_BE: KeyboardLayout = { + isoCode: "fr-BE", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/fr_CH.ts b/ui/src/keyboardLayouts/fr_CH.ts index cf1d3df..0ba8cb4 100644 --- a/ui/src/keyboardLayouts/fr_CH.ts +++ b/ui/src/keyboardLayouts/fr_CH.ts @@ -1,11 +1,11 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -import { chars as chars_de_CH } from "./de_CH" +import { de_CH } from "./de_CH" -export const name = "Français de Suisse"; +const name = "Français de Suisse"; -export const chars = { - ...chars_de_CH, +const chars = { + ...de_CH.chars, "è": { key: "BracketLeft" }, "ü": { key: "BracketLeft", shift: true }, "é": { key: "Semicolon" }, @@ -13,3 +13,9 @@ export const chars = { "à": { key: "Quote" }, "ä": { key: "Quote", shift: true }, } as Record; + +export const fr_CH: KeyboardLayout = { + isoCode: "fr-CH", + name: name, + chars: chars +}; diff --git a/ui/src/keyboardLayouts/fr_FR.ts b/ui/src/keyboardLayouts/fr_FR.ts index 27a03fd..29d5104 100644 --- a/ui/src/keyboardLayouts/fr_FR.ts +++ b/ui/src/keyboardLayouts/fr_FR.ts @@ -1,11 +1,11 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Français"; +const name = "Français"; const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter -export const chars = { +const chars = { A: { key: "KeyQ", shift: true }, "Ä": { key: "KeyQ", shift: true, accentKey: keyTrema }, "Â": { key: "KeyQ", shift: true, accentKey: keyHat }, @@ -137,3 +137,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const fr_FR: KeyboardLayout = { + isoCode: "fr-FR", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/it_IT.ts b/ui/src/keyboardLayouts/it_IT.ts index 9de61c5..0ff6e24 100644 --- a/ui/src/keyboardLayouts/it_IT.ts +++ b/ui/src/keyboardLayouts/it_IT.ts @@ -1,8 +1,8 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Italiano"; +const name = "Italiano"; -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -111,3 +111,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const it_IT: KeyboardLayout = { + isoCode: "it-IT", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/nb_NO.ts b/ui/src/keyboardLayouts/nb_NO.ts index 83918b2..4dae9c8 100644 --- a/ui/src/keyboardLayouts/nb_NO.ts +++ b/ui/src/keyboardLayouts/nb_NO.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Norsk bokmål"; +const name = "Norsk bokmål"; const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Ä": { key: "KeyA", shift: true, accentKey: keyTrema }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, @@ -165,3 +165,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const nb_NO: KeyboardLayout = { + isoCode: "nb-NO", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/sv_SE.ts b/ui/src/keyboardLayouts/sv_SE.ts index 75197cb..fbde3d0 100644 --- a/ui/src/keyboardLayouts/sv_SE.ts +++ b/ui/src/keyboardLayouts/sv_SE.ts @@ -1,6 +1,6 @@ -import { KeyCombo } from "../keyboardLayouts" +import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" -export const name = "Svenska"; +const name = "Svenska"; const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter @@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter -export const chars = { +const chars = { A: { key: "KeyA", shift: true }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, "Â": { key: "KeyA", shift: true, accentKey: keyHat }, @@ -162,3 +162,9 @@ export const chars = { Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; + +export const sv_SE: KeyboardLayout = { + isoCode: "sv-SE", + name: name, + chars: chars +}; \ No newline at end of file diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 891b96e..bb24fbb 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -1,17 +1,19 @@ // Key codes and modifiers correspond to definitions in the // [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt) +// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf) export const keys = { ArrowDown: 0x51, ArrowLeft: 0x50, ArrowRight: 0x4f, ArrowUp: 0x52, - Backquote: 0x35, + Backquote: 0x35, // aka Grave Backslash: 0x31, Backspace: 0x2a, - BracketLeft: 0x2f, - BracketRight: 0x30, + BracketLeft: 0x2f, // aka LeftBrace + BracketRight: 0x30, // aka RightBrace CapsLock: 0x39, Comma: 0x36, + Compose: 0x65, ContextMenu: 0, Delete: 0x4c, Digit0: 0x27, @@ -40,10 +42,21 @@ export const keys = { F10: 0x43, F11: 0x44, F12: 0x45, - F13: 0x68, + F14: 0x69, + F15: 0x6a, + F16: 0x6b, + F17: 0x6c, + F18: 0x6d, + F19: 0x6e, + F20: 0x6f, + F21: 0x70, + F22: 0x71, + F23: 0x72, + F24: 0x73, Home: 0x4a, + HashTilde: 0x32, // non-US # and ~ Insert: 0x49, - IntlBackslash: 0x64, + IntlBackslash: 0x64, // non-US \ and | KeyA: 0x04, KeyB: 0x05, KeyC: 0x06, @@ -72,30 +85,35 @@ export const keys = { KeyZ: 0x1d, KeypadExclamation: 0xcf, Minus: 0x2d, - NumLock: 0x53, - Numpad0: 0x62, - Numpad1: 0x59, - Numpad2: 0x5a, - Numpad3: 0x5b, - Numpad4: 0x5c, + None: 0x00, + NumLock: 0x53, // and Clear + Numpad0: 0x62, // and Insert + Numpad1: 0x59, // and End + Numpad2: 0x5a, // and Down Arrow + Numpad3: 0x5b, // and Page Down + Numpad4: 0x5c, // and Left Arrow Numpad5: 0x5d, - Numpad6: 0x5e, - Numpad7: 0x5f, - Numpad8: 0x60, - Numpad9: 0x61, + Numpad6: 0x5e, // and Right Arrow + Numpad7: 0x5f, // and Home + Numpad8: 0x60, // and Up Arrow + Numpad9: 0x61, // and Page Up NumpadAdd: 0x57, + NumpadComma: 0x85, + NumpadDecimal: 0x63, NumpadDivide: 0x54, NumpadEnter: 0x58, NumpadEqual: 0x67, + NumpadLeftParen: 0xb6, NumpadMultiply: 0x55, + NumpadRightParen: 0xb7, NumpadSubtract: 0x56, - NumpadDecimal: 0x63, PageDown: 0x4e, PageUp: 0x4b, Period: 0x37, PrintScreen: 0x46, Pause: 0x48, - Quote: 0x34, + Power: 0x66, + Quote: 0x34, // aka Single Quote or Apostrophe ScrollLock: 0x47, Semicolon: 0x33, Slash: 0x38, diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index 6e68f81..e797ce7 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; -import { layouts } from "@/keyboardLayouts"; +import { keyboardOptions } from "@/keyboardLayouts"; import { Checkbox } from "@/components/Checkbox"; import { SelectMenuBasic } from "../components/SelectMenuBasic"; @@ -32,7 +32,7 @@ export default function SettingsKeyboardRoute() { return "en_US"; }, [keyboardLayout]); - const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } }) + const layoutOptions = keyboardOptions(); const ledSyncOptions = [ { value: "auto", label: "Automatic" }, { value: "browser", label: "Browser Only" }, diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index 1e888f6..b729bf0 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -79,7 +79,7 @@ export default function SettingsRoute() { return () => { setDisableVideoFocusTrap(false); }; - }, [setDisableVideoFocusTrap, sendKeyboardEvent]); + }, [sendKeyboardEvent, setDisableVideoFocusTrap]); return (
    diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 8cdb5b3..1785bcd 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -707,7 +707,7 @@ export default function KvmIdRoute() { }, [diskChannel, file]); // System update - const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap); + const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); const [kvmTerminal, setKvmTerminal] = useState(null); const [serialConsole, setSerialConsole] = useState(null); @@ -805,7 +805,7 @@ export default function KvmIdRoute() { )}
    Date: Fri, 11 Jul 2025 10:56:03 -0500 Subject: [PATCH 159/165] chore(ui): Clean new keyboard option (#495) Fixed the Tailwind CSS syntax for `in` (nested) selector Added missing React dependency for `useEffect` --- ui/src/routes/devices.$id.settings.keyboard.tsx | 2 +- ui/src/routes/devices.$id.settings.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index e797ce7..57119ba 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -46,7 +46,7 @@ export default function SettingsKeyboardRoute() { if ("error" in resp) return; setKeyboardLayout(resp.result as string); }); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + }, [send, setKeyboardLayout]); const onKeyboardLayoutChange = useCallback( (e: React.ChangeEvent) => { diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index b729bf0..5075ab5 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -151,7 +151,6 @@ export default function SettingsRoute() { className={({ isActive }) => (isActive ? "active" : "")} >
    -

    Mouse

    @@ -163,7 +162,7 @@ export default function SettingsRoute() { to="keyboard" className={({ isActive }) => (isActive ? "active" : "")} > -
    +

    Keyboard

    From f1953fddbc52487e3c4886bd903be9b54446af17 Mon Sep 17 00:00:00 2001 From: Ben Kochie Date: Fri, 11 Jul 2025 18:14:32 +0200 Subject: [PATCH 160/165] chore: add metrics for configuration and WOL (#193) * Configuration load success/timestamp. * Wake-on-Lan packets/errors. Signed-off-by: SuperQ --- config.go | 23 +++++++++++++++++++++++ wol.go | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/config.go b/config.go index 3e88457..d48e25b 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,8 @@ import ( "github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/usbgadget" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) type WakeOnLanDevice struct { @@ -138,6 +140,21 @@ var ( configLock = &sync.Mutex{} ) +var ( + configSuccess = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_config_last_reload_successful", + Help: "The last configuration load succeeded", + }, + ) + configSuccessTime = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_config_last_reload_success_timestamp_seconds", + Help: "Timestamp of last successful config load", + }, + ) +) + func LoadConfig() { configLock.Lock() defer configLock.Unlock() @@ -153,6 +170,8 @@ func LoadConfig() { file, err := os.Open(configPath) if err != nil { logger.Debug().Msg("default config file doesn't exist, using default") + configSuccess.Set(1.0) + configSuccessTime.SetToCurrentTime() return } defer file.Close() @@ -161,6 +180,7 @@ func LoadConfig() { loadedConfig := *defaultConfig if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil { logger.Warn().Err(err).Msg("config file JSON parsing failed") + configSuccess.Set(0.0) return } @@ -181,6 +201,9 @@ func LoadConfig() { logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel) + configSuccess.Set(1.0) + configSuccessTime.SetToCurrentTime() + logger.Info().Str("path", configPath).Msg("config loaded") } diff --git a/wol.go b/wol.go index 47f921a..02b5c96 100644 --- a/wol.go +++ b/wol.go @@ -4,6 +4,24 @@ import ( "bytes" "encoding/binary" "net" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + wolPackets = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_wol_sent_packets_total", + Help: "Total number of Wake-on-LAN magic packets sent.", + }, + ) + wolErrors = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_wol_sent_packet_errors_total", + Help: "Total number of Wake-on-LAN magic packets errors.", + }, + ) ) // SendWOLMagicPacket sends a Wake-on-LAN magic packet to the specified MAC address @@ -11,6 +29,7 @@ func rpcSendWOLMagicPacket(macAddress string) error { // Parse the MAC address mac, err := net.ParseMAC(macAddress) if err != nil { + wolErrors.Inc() return ErrorfL(wolLogger, "invalid MAC address", err) } @@ -20,6 +39,7 @@ func rpcSendWOLMagicPacket(macAddress string) error { // Set up UDP connection conn, err := net.Dial("udp", "255.255.255.255:9") if err != nil { + wolErrors.Inc() return ErrorfL(wolLogger, "failed to establish UDP connection", err) } defer conn.Close() @@ -27,10 +47,12 @@ func rpcSendWOLMagicPacket(macAddress string) error { // Send the packet _, err = conn.Write(packet) if err != nil { + wolErrors.Inc() return ErrorfL(wolLogger, "failed to send WOL packet", err) } wolLogger.Info().Str("mac", macAddress).Msg("WOL packet sent") + wolPackets.Inc() return nil } From 5a3ce2d6ec4c0b2167fb48fb3eeb6fffd952a45f Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 11 Jul 2025 16:41:01 -0500 Subject: [PATCH 161/165] chore(ui) Upgrade packages (#682) --- ui/package-lock.json | 647 ++++++++++++++++++++++--------------------- ui/package.json | 10 +- 2 files changed, 341 insertions(+), 316 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 6380015..f0531d3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -22,7 +22,7 @@ "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.4", - "framer-motion": "^12.23.0", + "framer-motion": "^12.23.3", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", @@ -31,7 +31,7 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.89", + "react-simple-keyboard": "^3.8.93", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -52,8 +52,8 @@ "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/eslint-plugin": "^8.36.0", + "@typescript-eslint/parser": "^8.36.0", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", "eslint": "^9.30.1", @@ -65,7 +65,7 @@ "globals": "^16.3.0", "postcss": "^8.5.6", "prettier": "^3.6.2", - "prettier-plugin-tailwindcss": "^0.6.13", + "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.11", "typescript": "^5.8.3", "vite": "^6.3.5", @@ -112,9 +112,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "cpu": [ "ppc64" ], @@ -128,9 +128,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "cpu": [ "arm" ], @@ -144,9 +144,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "cpu": [ "arm64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "cpu": [ "x64" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "cpu": [ "x64" ], @@ -208,9 +208,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "cpu": [ "arm64" ], @@ -224,9 +224,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "cpu": [ "arm" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "cpu": [ "arm64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "cpu": [ "ia32" ], @@ -288,9 +288,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "cpu": [ "loong64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "cpu": [ "mips64el" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "cpu": [ "ppc64" ], @@ -336,9 +336,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "cpu": [ "riscv64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "cpu": [ "s390x" ], @@ -368,9 +368,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "cpu": [ "x64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "cpu": [ "x64" ], @@ -416,9 +416,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "cpu": [ "x64" ], @@ -447,10 +447,26 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "cpu": [ "x64" ], @@ -464,9 +480,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "cpu": [ "arm64" ], @@ -480,9 +496,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "cpu": [ "ia32" ], @@ -496,9 +512,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "cpu": [ "x64" ], @@ -1031,9 +1047,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", - "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", + "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", "cpu": [ "arm" ], @@ -1044,9 +1060,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", - "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", + "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", "cpu": [ "arm64" ], @@ -1057,9 +1073,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", - "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", + "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", "cpu": [ "arm64" ], @@ -1070,9 +1086,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", - "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", + "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", "cpu": [ "x64" ], @@ -1083,9 +1099,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", - "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", + "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", "cpu": [ "arm64" ], @@ -1096,9 +1112,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", - "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", + "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", "cpu": [ "x64" ], @@ -1109,9 +1125,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", - "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", + "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", "cpu": [ "arm" ], @@ -1122,9 +1138,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", - "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", + "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", "cpu": [ "arm" ], @@ -1135,9 +1151,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", - "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", + "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", "cpu": [ "arm64" ], @@ -1148,9 +1164,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", - "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", + "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", "cpu": [ "arm64" ], @@ -1161,9 +1177,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", - "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", + "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", "cpu": [ "loong64" ], @@ -1174,9 +1190,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", - "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", + "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", "cpu": [ "ppc64" ], @@ -1187,9 +1203,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", - "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", + "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", "cpu": [ "riscv64" ], @@ -1200,9 +1216,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", - "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", + "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", "cpu": [ "riscv64" ], @@ -1213,9 +1229,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", - "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", + "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", "cpu": [ "s390x" ], @@ -1226,9 +1242,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", - "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", + "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", "cpu": [ "x64" ], @@ -1239,9 +1255,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", - "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", + "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", "cpu": [ "x64" ], @@ -1252,9 +1268,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", - "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", + "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", "cpu": [ "arm64" ], @@ -1265,9 +1281,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", - "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", + "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", "cpu": [ "ia32" ], @@ -1278,9 +1294,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", - "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", + "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", "cpu": [ "x64" ], @@ -1297,9 +1313,9 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.9.tgz", - "integrity": "sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.11.tgz", + "integrity": "sha512-P3GM+0lqjFctcp5HhR9mOcvLSX3SptI9L1aux0Fuvgt8oH4f92rCUrkodAa0U2ktmdjcyIiG37xg2mb/dSCYSA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1315,16 +1331,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.12.9", - "@swc/core-darwin-x64": "1.12.9", - "@swc/core-linux-arm-gnueabihf": "1.12.9", - "@swc/core-linux-arm64-gnu": "1.12.9", - "@swc/core-linux-arm64-musl": "1.12.9", - "@swc/core-linux-x64-gnu": "1.12.9", - "@swc/core-linux-x64-musl": "1.12.9", - "@swc/core-win32-arm64-msvc": "1.12.9", - "@swc/core-win32-ia32-msvc": "1.12.9", - "@swc/core-win32-x64-msvc": "1.12.9" + "@swc/core-darwin-arm64": "1.12.11", + "@swc/core-darwin-x64": "1.12.11", + "@swc/core-linux-arm-gnueabihf": "1.12.11", + "@swc/core-linux-arm64-gnu": "1.12.11", + "@swc/core-linux-arm64-musl": "1.12.11", + "@swc/core-linux-x64-gnu": "1.12.11", + "@swc/core-linux-x64-musl": "1.12.11", + "@swc/core-win32-arm64-msvc": "1.12.11", + "@swc/core-win32-ia32-msvc": "1.12.11", + "@swc/core-win32-x64-msvc": "1.12.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1336,9 +1352,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz", - "integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.11.tgz", + "integrity": "sha512-J19Jj9Y5x/N0loExH7W0OI9OwwoVyxutDdkyq1o/kgXyBqmmzV7Y/Q9QekI2Fm/qc5mNeAdP7aj4boY4AY/JPw==", "cpu": [ "arm64" ], @@ -1353,9 +1369,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.9.tgz", - "integrity": "sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.11.tgz", + "integrity": "sha512-PTuUQrfStQ6cjW+uprGO2lpQHy84/l0v+GqRqq8s/jdK55rFRjMfCeyf6FAR0l6saO5oNOQl+zWR1aNpj8pMQw==", "cpu": [ "x64" ], @@ -1370,9 +1386,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.9.tgz", - "integrity": "sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.11.tgz", + "integrity": "sha512-poxBq152HsupOtnZilenvHmxZ9a8SRj4LtfxUnkMDNOGrZR9oxbQNwEzNKfi3RXEcXz+P8c0Rai1ubBazXv8oQ==", "cpu": [ "arm" ], @@ -1387,9 +1403,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz", - "integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.11.tgz", + "integrity": "sha512-y1HNamR/D0Hc8xIE910ysyLe269UYiGaQPoLjQS0phzWFfWdMj9bHM++oydVXZ4RSWycO7KyJ3uvw4NilvyMKQ==", "cpu": [ "arm64" ], @@ -1404,9 +1420,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.9.tgz", - "integrity": "sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.11.tgz", + "integrity": "sha512-LlBxPh/32pyQsu2emMEOFRm7poEFLsw12Y1mPY7FWZiZeptomKSOSHRzKDz9EolMiV4qhK1caP1lvW4vminYgQ==", "cpu": [ "arm64" ], @@ -1421,9 +1437,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz", - "integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.11.tgz", + "integrity": "sha512-bOjiZB8O/1AzHkzjge1jqX62HGRIpOHqFUrGPfAln/NC6NR+Z2A78u3ixV7k5KesWZFhCV0YVGJL+qToL27myA==", "cpu": [ "x64" ], @@ -1438,9 +1454,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz", - "integrity": "sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.11.tgz", + "integrity": "sha512-4dzAtbT/m3/UjF045+33gLiHd8aSXJDoqof7gTtu4q0ZyAf7XJ3HHspz+/AvOJLVo4FHHdFcdXhmo/zi1nFn8A==", "cpu": [ "x64" ], @@ -1455,9 +1471,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.9.tgz", - "integrity": "sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.11.tgz", + "integrity": "sha512-h8HiwBZErKvCAmjW92JvQp0iOqm6bncU4ac5jxBGkRApabpUenNJcj3h2g5O6GL5K6T9/WhnXE5gyq/s1fhPQg==", "cpu": [ "arm64" ], @@ -1472,9 +1488,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.9.tgz", - "integrity": "sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.11.tgz", + "integrity": "sha512-1pwr325mXRNUhxTtXmx1IokV5SiRL+6iDvnt3FRXj+X5UvXXKtg2zeyftk+03u8v8v8WUr5I32hIypVJPTNxNg==", "cpu": [ "ia32" ], @@ -1489,9 +1505,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.9.tgz", - "integrity": "sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==", + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.11.tgz", + "integrity": "sha512-5gggWo690Gvs7XiPxAmb5tHwzB9RTVXUV7AWoGb6bmyUd1OXYaebQF0HAOtade5jIoNhfQMQJ7QReRgt/d2jAA==", "cpu": [ "x64" ], @@ -1992,17 +2008,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", + "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/type-utils": "8.36.0", + "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2016,7 +2032,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.36.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2032,16 +2048,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", + "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4" }, "engines": { @@ -2057,14 +2073,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", + "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.36.0", + "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "engines": { @@ -2079,14 +2095,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", + "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2097,9 +2113,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", + "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", "dev": true, "license": "MIT", "engines": { @@ -2114,14 +2130,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", + "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/utils": "8.36.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2138,9 +2154,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "dev": true, "license": "MIT", "engines": { @@ -2152,16 +2168,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", + "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.36.0", + "@typescript-eslint/tsconfig-utils": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2207,16 +2223,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", + "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2231,13 +2247,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", + "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.36.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2734,9 +2750,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true, "funding": [ { @@ -3154,9 +3170,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.179", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", - "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", + "version": "1.5.182", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", + "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==", "dev": true, "license": "ISC" }, @@ -3345,9 +3361,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3357,31 +3373,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "node_modules/escalade": { @@ -3974,13 +3991,13 @@ } }, "node_modules/framer-motion": { - "version": "12.23.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz", - "integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==", + "version": "12.23.3", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.3.tgz", + "integrity": "sha512-llmLkf44zuIZOPSrE4bl4J0UTg6bav+rlKEfMRKgvDMXqBrUtMg6cspoQeRVK3nqRLxTmAJhfGXk39UDdZD7Kw==", "license": "MIT", "dependencies": { - "motion-dom": "^12.22.0", - "motion-utils": "^12.19.0", + "motion-dom": "^12.23.2", + "motion-utils": "^12.23.2", "tslib": "^2.4.0" }, "peerDependencies": { @@ -5239,18 +5256,18 @@ } }, "node_modules/motion-dom": { - "version": "12.22.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz", - "integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==", + "version": "12.23.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.2.tgz", + "integrity": "sha512-73j6xDHX/NvVh5L5oS1ouAVnshsvmApOq4F3VZo5MkYSD/YVsVLal4Qp9wvVgJM9uU2bLZyc0Sn8B9c/MMKk4g==", "license": "MIT", "dependencies": { - "motion-utils": "^12.19.0" + "motion-utils": "^12.23.2" } }, "node_modules/motion-utils": { - "version": "12.19.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", - "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", + "version": "12.23.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.2.tgz", + "integrity": "sha512-cIEXlBlXAOUyiAtR0S+QPQUM9L3Diz23Bo+zM420NvSd/oPQJwg6U+rT+WRTpp0rizMsBGQOsAwhWIfglUcZfA==", "license": "MIT" }, "node_modules/ms": { @@ -5619,9 +5636,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", - "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", "dev": true, "license": "MIT", "engines": { @@ -5629,6 +5646,8 @@ }, "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", @@ -5650,6 +5669,12 @@ "@ianvs/prettier-plugin-sort-imports": { "optional": true }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, "@prettier/plugin-pug": { "optional": true }, @@ -5837,9 +5862,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.89", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.89.tgz", - "integrity": "sha512-F3a29Uvp7Zf92YM84DidqBTGQjk78Yzqm7xZBjsW7rGXis5Yhakw+7CWeBQjHAbwNMdYAMoBq34AjGYemlXGSQ==", + "version": "3.8.93", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.93.tgz", + "integrity": "sha512-uLt3LeUeA0KAjTWKo5JMpLxxhPslXD7o8KOMCRSlfiQaTpqO5JqqJSSxyiQNKnbd3QYoOXsRyw3Uz8EuvSffRA==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -6013,9 +6038,9 @@ } }, "node_modules/rollup": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", - "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", + "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6028,26 +6053,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.1", - "@rollup/rollup-android-arm64": "4.44.1", - "@rollup/rollup-darwin-arm64": "4.44.1", - "@rollup/rollup-darwin-x64": "4.44.1", - "@rollup/rollup-freebsd-arm64": "4.44.1", - "@rollup/rollup-freebsd-x64": "4.44.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", - "@rollup/rollup-linux-arm-musleabihf": "4.44.1", - "@rollup/rollup-linux-arm64-gnu": "4.44.1", - "@rollup/rollup-linux-arm64-musl": "4.44.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-musl": "4.44.1", - "@rollup/rollup-linux-s390x-gnu": "4.44.1", - "@rollup/rollup-linux-x64-gnu": "4.44.1", - "@rollup/rollup-linux-x64-musl": "4.44.1", - "@rollup/rollup-win32-arm64-msvc": "4.44.1", - "@rollup/rollup-win32-ia32-msvc": "4.44.1", - "@rollup/rollup-win32-x64-msvc": "4.44.1", + "@rollup/rollup-android-arm-eabi": "4.44.2", + "@rollup/rollup-android-arm64": "4.44.2", + "@rollup/rollup-darwin-arm64": "4.44.2", + "@rollup/rollup-darwin-x64": "4.44.2", + "@rollup/rollup-freebsd-arm64": "4.44.2", + "@rollup/rollup-freebsd-x64": "4.44.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", + "@rollup/rollup-linux-arm-musleabihf": "4.44.2", + "@rollup/rollup-linux-arm64-gnu": "4.44.2", + "@rollup/rollup-linux-arm64-musl": "4.44.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-musl": "4.44.2", + "@rollup/rollup-linux-s390x-gnu": "4.44.2", + "@rollup/rollup-linux-x64-gnu": "4.44.2", + "@rollup/rollup-linux-x64-musl": "4.44.2", + "@rollup/rollup-win32-arm64-msvc": "4.44.2", + "@rollup/rollup-win32-ia32-msvc": "4.44.2", + "@rollup/rollup-win32-x64-msvc": "4.44.2", "fsevents": "~2.3.2" } }, diff --git a/ui/package.json b/ui/package.json index 8723767..6b80b9e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,7 +33,7 @@ "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.4", - "framer-motion": "^12.23.0", + "framer-motion": "^12.23.3", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", "react": "^19.1.0", @@ -42,7 +42,7 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.89", + "react-simple-keyboard": "^3.8.93", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -63,8 +63,8 @@ "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/eslint-plugin": "^8.36.0", + "@typescript-eslint/parser": "^8.36.0", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", "eslint": "^9.30.1", @@ -76,7 +76,7 @@ "globals": "^16.3.0", "postcss": "^8.5.6", "prettier": "^3.6.2", - "prettier-plugin-tailwindcss": "^0.6.13", + "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.11", "typescript": "^5.8.3", "vite": "^6.3.5", From b4525b8760a76a2efad70a45adc49596da0aaf0e Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 11 Jul 2025 16:41:05 -0500 Subject: [PATCH 162/165] chore/ Fix go lint error (#683) --- internal/usbgadget/hid_keyboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index a087145..6ad3b6a 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -17,7 +17,7 @@ var keyboardConfig = gadgetConfigItem{ "protocol": "1", "subclass": "1", "report_length": "8", - "no_out_endpoint": "0", + "no_out_endpoint": "0", }, reportDesc: keyboardReportDesc, } From cff3ddad293f2c14e736048c23506361d6f2de9c Mon Sep 17 00:00:00 2001 From: Aveline <352441+ym@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:10:49 +0200 Subject: [PATCH 163/165] chore: add issue templates (#686) * chore: add issue templates * chore: add remote device info --- .github/ISSUE_TEMPLATE/bug.yml | 76 ++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 10 ++++ .github/ISSUE_TEMPLATE/feature.yml | 46 ++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..fd3be8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,76 @@ +--- +name: Bug report +description: 🐛 Let us know about an unexpected error, a crash, or an unexpected behavior. +type: 'Bug' +labels: + - 'type: bug' +body: + - type: checkboxes + attributes: + label: Disclaimer + description: | + For support questions, please use the [discussions][] or [Discord][] instead. Before + opening a bug report, ensure you have read the [documentation][], + [Troubleshooting][] and [Device FAQs][]. Only use bug reports for actual + bugs. + + [documentation]: https://jetkvm.com/docs + [Troubleshooting]: https://jetkvm.com/docs/getting-started/troubleshooting + [Device FAQs]: https://jetkvm.com/docs/getting-started/faq + [discussions]: https://github.com/jetkvm/kvm/discussions + [Discord]: https://jetkvm.com/discord + options: + - label: I have read and understood the disclaimer. + required: true + - type: input + attributes: + label: Application version + description: | + Provide the application version (can be found in General settings) + validations: + required: true + - type: input + attributes: + label: System version + description: | + Provide the system version (can be found in General settings) + validations: + required: true + - type: dropdown + attributes: + label: Device model + description: Provide the device model + options: + - JetKVM + - JetKVM (POE) + validations: + required: false + - type: dropdown + attributes: + label: Extension model + description: Provide the extension model (if the bug is related to the extension) + options: + - ATX Power Control + - DC Power Control + - Serial Console + validations: + required: false + - type: input + attributes: + label: Remote device Hardware + description: If the bug is related to a remote device, please provide its hardware information e.g. Raspberry Pi 5 + validations: + required: false + - type: input + attributes: + label: Remote device OS + description: If the bug is related to a remote device, please provide its OS information as detailed as possible e.g. Debian 12. + validations: + required: false + - type: textarea + attributes: + label: Bug description + description: | + Provide a description of the problem: steps to reproduce it, what you are expecting and what you got. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6b45cd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: true + +contact_links: + - name: Hardware Issues + url: https://jetkvm.com/contact + about: If your hardware is not powering on or is not working, please contact us. + + - name: Discord + url: https://jetkvm.com/discord + about: Engage with the JetKVM team and other community members. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..9f105cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,46 @@ +name: Feature +type: 'Feature' +description: 🚀 Request a new feature. +labels: +- 'type: feature' +body: + - type: textarea + attributes: + label: A note for the community + value: | + > [!NOTE] + > Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. + validations: + required: true + - type: checkboxes + attributes: + label: Disclaimer + description: | + Before requesting a feature, check it does not already exist in the [documentation](https://jetkvm.com/docs) or our [roadmap](https://jetkvm.com/roadmap). + You are quite welcome opening a feature request before spending time to implement it yourself. + options: + - label: I have read and understood the disclaimer. + required: true + - label: I plan to implement the feature myself. + - type: dropdown + attributes: + label: Subsystem + description: Provide the subsystem of the feature you request, you can choose multiple if you think it fits in multiple areas. + options: + - Hardware + - Device Compatibility + - Keyboard + - Mouse + - Power + - UI: Screen + - UI: Application + - UI: Cloud + validations: + required: false + - type: textarea + attributes: + label: Feature description + description: | + Provide a description of the feature you request. + validations: + required: true \ No newline at end of file From 55fbd6c359f308b764bf1374a031370dd525aa66 Mon Sep 17 00:00:00 2001 From: Silke pilon Date: Tue, 15 Jul 2025 23:40:42 +0200 Subject: [PATCH 164/165] docs: add comprehensive DEVELOPMENT.md for JetKVM (#692) * docs: add comprehensive DEVELOPMENT.md for JetKVM Add a detailed development guide covering setup, project structure, and workflows for both full device and frontend-only development. Include prerequisites, build commands, deployment scripts, environment variables, and testing instructions to streamline onboarding and contributions. This improves developer experience and standardizes development practices across the project. * docs: clean up DEVELOPMENT.md by removing outdated sections Remove the Custom Build Tags and Release Process sections to simplify the documentation and avoid confusion with deprecated build and release instructions. Focus the document on current performance profiling steps. * docs: rewrite DEVELOPMENT.md for clearer setup and usage Revise the JetKVM development guide to improve clarity and usability. Simplify the introduction and reorganize prerequisites and setup steps to help new developers get started quickly. Add explicit instructions for cloning, tool verification, deployment, and testing. Streamline common tasks sections with clear commands for UI and backend development, testing, and log viewing. Update project layout overview for easier navigation. These changes reduce onboarding friction and enhance the developer experience. * docs: remove duplicate "Get Started" header in DEVELOPMENT.md Clean up the DEVELOPMENT.md file by deleting the repeated "Get Started" header * docs: add recommended development environment section Add guidance recommending Linux or macOS for development and suggest using WSL on Windows to ensure compatibility with shell scripts and build tools. This improves the developer experience and reduces setup issues across different operating systems. * docs: add links to prerequisites in DEVELOPMENT.md Update DEVELOPMENT.md to URLs for Go, Node.js, Git, and SSH access prerequisites. This improves clarity and helps developers quickly find installation resources. --- DEVELOPMENT.md | 355 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +- 2 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..d95db77 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,355 @@ +
    + JetKVM logo + +### Development Guide + +[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs) + +[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/jetkvm.svg?style=social&label=Follow%20%40JetKVM)](https://twitter.com/jetkvm) + +[![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](https://goreportcard.com/report/github.com/jetkvm/kvm) + +
    + +# JetKVM Development Guide + +Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase. + +## Get Started + +### Prerequisites +- **A JetKVM device** (for full development) +- **[Go 1.24.4+](https://go.dev/doc/install)** and **[Node.js 22.15.0](https://nodejs.org/en/download/)** +- **[Git](https://git-scm.com/downloads)** for version control +- **[SSH access](https://jetkvm.com/docs/advanced-usage/developing#developer-mode)** to your JetKVM device + +### Development Environment + +**Recommended:** Development is best done on **Linux** or **macOS**. + +If you're using Windows, we strongly recommend using **WSL (Windows Subsystem for Linux)** for the best development experience: +- [Install WSL on Windows](https://docs.microsoft.com/en-us/windows/wsl/install) +- [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) + +This ensures compatibility with shell scripts and build tools used in the project. + +### Project Setup + +1. **Clone the repository:** + ```bash + git clone https://github.com/jetkvm/kvm.git + cd kvm + ``` + +2. **Check your tools:** + ```bash + go version && node --version + ``` + +3. **Find your JetKVM IP address** (check your router or device screen) + +4. **Deploy and test:** + ```bash + ./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP + ``` + +5. **Open in browser:** `http://192.168.1.100` + +That's it! You're now running your own development version of JetKVM. + +--- + +## Common Tasks + +### Modify the UI + +```bash +cd ui +npm install +./dev_device.sh 192.168.1.100 # Replace with your device IP +``` + +Now edit files in `ui/src/` and see changes live in your browser! + +### Modify the backend + +```bash +# Edit Go files (config.go, web.go, etc.) +./dev_deploy.sh -r 192.168.1.100 --skip-ui-build +``` + +### Run tests + +```bash +./dev_deploy.sh -r 192.168.1.100 --run-go-tests +``` + +### View logs + +```bash +ssh root@192.168.1.100 +tail -f /var/log/jetkvm.log +``` + +--- + +## Project Layout + +``` +/kvm/ +├── main.go # App entry point +├── config.go # Settings & configuration +├── web.go # API endpoints +├── ui/ # React frontend +│ ├── src/routes/ # Pages (login, settings, etc.) +│ └── src/components/ # UI components +└── internal/ # Internal Go packages +``` + +**Key files for beginners:** + +- `web.go` - Add new API endpoints here +- `config.go` - Add new settings here +- `ui/src/routes/` - Add new pages here +- `ui/src/components/` - Add new UI components here + +--- + +## Development Modes + +### Full Development (Recommended) + +*Best for: Complete feature development* + +```bash +# Deploy everything to your JetKVM device +./dev_deploy.sh -r +``` + +### Frontend Only + +*Best for: UI changes without device* + +```bash +cd ui +npm install +./dev_device.sh +``` + +### Quick Backend Changes + +*Best for: API or backend logic changes* + +```bash +# Skip frontend build for faster deployment +./dev_deploy.sh -r --skip-ui-build +``` + +--- + +## Debugging Made Easy + +### Check if everything is working + +```bash +# Test connection to device +ping 192.168.1.100 + +# Check if JetKVM is running +ssh root@192.168.1.100 ps aux | grep jetkvm +``` + +### View live logs + +```bash +ssh root@192.168.1.100 +tail -f /var/log/jetkvm.log +``` + +### Reset everything (if stuck) + +```bash +ssh root@192.168.1.100 +rm /userdata/kvm_config.json +systemctl restart jetkvm +``` + +--- + +## Testing Your Changes + +### Manual Testing + +1. Deploy your changes: `./dev_deploy.sh -r ` +2. Open browser: `http://` +3. Test your feature +4. Check logs: `ssh root@ tail -f /var/log/jetkvm.log` + +### Automated Testing + +```bash +# Run all tests +./dev_deploy.sh -r --run-go-tests + +# Frontend linting +cd ui && npm run lint +``` + +### API Testing + +```bash +# Test login endpoint +curl -X POST http:///auth/password-local \ + -H "Content-Type: application/json" \ + -d '{"password": "test123"}' +``` + +--- + +## Common Issues & Solutions + +### "Build failed" or "Permission denied" + +```bash +# Fix permissions +ssh root@ chmod +x /userdata/jetkvm/bin/jetkvm_app_debug + +# Clean and rebuild +go clean -modcache +go mod tidy +make build_dev +``` + +### "Can't connect to device" + +```bash +# Check network +ping + +# Check SSH +ssh root@ echo "Connection OK" +``` + +### "Frontend not updating" + +```bash +# Clear cache and rebuild +cd ui +npm cache clean --force +rm -rf node_modules +npm install +``` + +--- + +## Next Steps + +### Adding a New Feature + +1. **Backend:** Add API endpoint in `web.go` +2. **Config:** Add settings in `config.go` +3. **Frontend:** Add UI in `ui/src/routes/` +4. **Test:** Deploy and test with `./dev_deploy.sh` + +### Code Style + +- **Go:** Follow standard Go conventions +- **TypeScript:** Use TypeScript for type safety +- **React:** Keep components small and reusable + +### Environment Variables + +```bash +# Enable debug logging +export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc" + +# Frontend development +export JETKVM_PROXY_URL="ws://" +``` + +--- + +## Need Help? + +1. **Check logs first:** `ssh root@ tail -f /var/log/jetkvm.log` +2. **Search issues:** [GitHub Issues](https://github.com/jetkvm/kvm/issues) +3. **Ask on Discord:** [JetKVM Discord](https://jetkvm.com/discord) +4. **Read docs:** [JetKVM Documentation](https://jetkvm.com/docs) + +--- + +## Contributing + +### Ready to contribute? + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +### Before submitting: + +- [ ] Code works on device +- [ ] Tests pass +- [ ] Code follows style guidelines +- [ ] Documentation updated (if needed) + +--- + +## Advanced Topics + +### Performance Profiling + +```bash +# Enable profiling +go build -o bin/jetkvm_app -ldflags="-X main.enableProfiling=true" cmd/main.go + +# Access profiling +curl http://:6060/debug/pprof/ +``` +### Advanced Environment Variables + +```bash +# Enable trace logging (useful for debugging) +export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc" + +# For frontend development +export JETKVM_PROXY_URL="ws://" + +# Enable SSL in development +export USE_SSL=true +``` + +### Configuration Management + +The application uses a JSON configuration file stored at `/userdata/kvm_config.json`. + +#### Adding New Configuration Options + +1. **Update the Config struct in `config.go`:** + + ```go + type Config struct { + // ... existing fields + NewFeatureEnabled bool `json:"new_feature_enabled"` + } + ``` + +2. **Update the default configuration:** + + ```go + var defaultConfig = &Config{ + // ... existing defaults + NewFeatureEnabled: false, + } + ``` + +3. **Add migration logic if needed for existing installations** + + +--- + +**Happy coding!** + +For more information, visit the [JetKVM Documentation](https://jetkvm.com/docs) or join our [Discord Server](https://jetkvm.com/discord). diff --git a/README.md b/README.md index 0f0700f..541578c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,9 @@ JetKVM is written in Go & TypeScript. with some bits and pieces written in C. An The project contains two main parts, the backend software that runs on the KVM device and the frontend software that is served by the KVM device, and also the cloud. -For most of local device development, all you need is to use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information. +For comprehensive development information, including setup, testing, debugging, and contribution guidelines, see **[DEVELOPMENT.md](DEVELOPMENT.md)**. + +For quick device development, use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information. ## Backend From 33ac9fe0b638df413cd72f29ee3895446625377e Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 12 Aug 2025 04:24:05 -0500 Subject: [PATCH 165/165] chore(ui)/package upgrades (#724) | Package | From | To | | -------------------------------- | ----------- | ------------ | | @headlessui/react | 2.2.4 | 2.2.7 | | framer-motion | 12.23.3 | 12.23.12 | | react | 19.1.0 | 19.1.1 | | react-dom | 19.1.0 | 19.1.1 | | react-simple-keyboard | 3.8.93 | 3.8.106 | |@eslint/js | 9.30.1 | 9.32.0 | | @types/react | 19.1.8 | 19.1.9 | | @types/react-dom | 19.1.8 | 19.1.9 | |eslint | 9.30.1 | 9.32.0 | |eslint-config-prettier | 10.1.5 | 10.1.8 | | typescript | 5.8.3 | 5.9.2 | --- ui/package-lock.json | 877 +++++++++++++++++++++---------------------- ui/package.json | 28 +- 2 files changed, 447 insertions(+), 458 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index f0531d3..72a4849 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,14 +1,14 @@ { "name": "kvm-ui", - "version": "0.0.0", + "version": "2025.08.07.001", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvm-ui", - "version": "0.0.0", + "version": "2025.08.07.001", "dependencies": { - "@headlessui/react": "^2.2.4", + "@headlessui/react": "^2.2.7", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", "@vitejs/plugin-basic-ssl": "^2.1.0", @@ -22,16 +22,16 @@ "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.4", - "framer-motion": "^12.23.3", + "framer-motion": "^12.23.12", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", - "react": "^19.1.0", + "react": "^19.1.1", "react-animate-height": "^3.2.3", - "react-dom": "^19.1.0", + "react-dom": "^19.1.1", "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.93", + "react-simple-keyboard": "^3.8.106", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -43,21 +43,21 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.32.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.11", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.36.0", - "@typescript-eslint/parser": "^8.36.0", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.30.1", - "eslint-config-prettier": "^10.1.5", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", @@ -67,7 +67,7 @@ "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.11", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" }, @@ -103,18 +103,18 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -128,9 +128,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -144,9 +144,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -208,9 +208,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -224,9 +224,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -288,9 +288,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -336,9 +336,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -368,9 +368,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -416,9 +416,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -448,9 +448,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", "cpu": [ "arm64" ], @@ -464,9 +464,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -480,9 +480,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -496,9 +496,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -512,9 +512,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -596,9 +596,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -643,9 +643,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -664,9 +664,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.15.1", @@ -676,34 +676,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, @@ -723,12 +711,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@floating-ui/dom": "^1.7.3" }, "peerDependencies": { "react": ">=16.8.0", @@ -742,9 +730,9 @@ "license": "MIT" }, "node_modules/@headlessui/react": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", - "integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz", + "integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", @@ -934,14 +922,14 @@ } }, "node_modules/@react-aria/focus": { - "version": "3.20.5", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz", - "integrity": "sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.0.tgz", + "integrity": "sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==", "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.25.3", - "@react-aria/utils": "^3.29.1", - "@react-types/shared": "^3.30.0", + "@react-aria/interactions": "^3.25.4", + "@react-aria/utils": "^3.30.0", + "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, @@ -951,15 +939,15 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.25.3", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz", - "integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==", + "version": "3.25.4", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.4.tgz", + "integrity": "sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.9", - "@react-aria/utils": "^3.29.1", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.30.0", "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.30.0", + "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -968,9 +956,9 @@ } }, "node_modules/@react-aria/ssr": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", - "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -983,15 +971,15 @@ } }, "node_modules/@react-aria/utils": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz", - "integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.0.tgz", + "integrity": "sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.9", + "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.10.7", - "@react-types/shared": "^3.30.0", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, @@ -1010,9 +998,9 @@ } }, "node_modules/@react-stately/utils": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz", - "integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==", + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -1022,9 +1010,9 @@ } }, "node_modules/@react-types/shared": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz", - "integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==", + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.31.0.tgz", + "integrity": "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==", "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" @@ -1040,16 +1028,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", - "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], @@ -1060,9 +1048,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", - "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], @@ -1073,9 +1061,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", - "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], @@ -1086,9 +1074,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", - "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], @@ -1099,9 +1087,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", - "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", "cpu": [ "arm64" ], @@ -1112,9 +1100,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", - "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", "cpu": [ "x64" ], @@ -1125,9 +1113,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", - "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], @@ -1138,9 +1126,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", - "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", "cpu": [ "arm" ], @@ -1151,9 +1139,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", - "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], @@ -1164,9 +1152,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", - "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], @@ -1177,9 +1165,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", - "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ "loong64" ], @@ -1189,10 +1177,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", - "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", "cpu": [ "ppc64" ], @@ -1203,9 +1191,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", - "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], @@ -1216,9 +1204,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", - "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", "cpu": [ "riscv64" ], @@ -1229,9 +1217,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", - "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], @@ -1242,9 +1230,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", - "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], @@ -1255,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", - "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], @@ -1268,9 +1256,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", - "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], @@ -1281,9 +1269,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", - "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], @@ -1294,9 +1282,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", - "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], @@ -1313,9 +1301,9 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.11.tgz", - "integrity": "sha512-P3GM+0lqjFctcp5HhR9mOcvLSX3SptI9L1aux0Fuvgt8oH4f92rCUrkodAa0U2ktmdjcyIiG37xg2mb/dSCYSA==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz", + "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1331,16 +1319,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.12.11", - "@swc/core-darwin-x64": "1.12.11", - "@swc/core-linux-arm-gnueabihf": "1.12.11", - "@swc/core-linux-arm64-gnu": "1.12.11", - "@swc/core-linux-arm64-musl": "1.12.11", - "@swc/core-linux-x64-gnu": "1.12.11", - "@swc/core-linux-x64-musl": "1.12.11", - "@swc/core-win32-arm64-msvc": "1.12.11", - "@swc/core-win32-ia32-msvc": "1.12.11", - "@swc/core-win32-x64-msvc": "1.12.11" + "@swc/core-darwin-arm64": "1.13.3", + "@swc/core-darwin-x64": "1.13.3", + "@swc/core-linux-arm-gnueabihf": "1.13.3", + "@swc/core-linux-arm64-gnu": "1.13.3", + "@swc/core-linux-arm64-musl": "1.13.3", + "@swc/core-linux-x64-gnu": "1.13.3", + "@swc/core-linux-x64-musl": "1.13.3", + "@swc/core-win32-arm64-msvc": "1.13.3", + "@swc/core-win32-ia32-msvc": "1.13.3", + "@swc/core-win32-x64-msvc": "1.13.3" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1352,9 +1340,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.11.tgz", - "integrity": "sha512-J19Jj9Y5x/N0loExH7W0OI9OwwoVyxutDdkyq1o/kgXyBqmmzV7Y/Q9QekI2Fm/qc5mNeAdP7aj4boY4AY/JPw==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz", + "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==", "cpu": [ "arm64" ], @@ -1369,9 +1357,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.11.tgz", - "integrity": "sha512-PTuUQrfStQ6cjW+uprGO2lpQHy84/l0v+GqRqq8s/jdK55rFRjMfCeyf6FAR0l6saO5oNOQl+zWR1aNpj8pMQw==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz", + "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==", "cpu": [ "x64" ], @@ -1386,9 +1374,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.11.tgz", - "integrity": "sha512-poxBq152HsupOtnZilenvHmxZ9a8SRj4LtfxUnkMDNOGrZR9oxbQNwEzNKfi3RXEcXz+P8c0Rai1ubBazXv8oQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz", + "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==", "cpu": [ "arm" ], @@ -1403,9 +1391,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.11.tgz", - "integrity": "sha512-y1HNamR/D0Hc8xIE910ysyLe269UYiGaQPoLjQS0phzWFfWdMj9bHM++oydVXZ4RSWycO7KyJ3uvw4NilvyMKQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz", + "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==", "cpu": [ "arm64" ], @@ -1420,9 +1408,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.11.tgz", - "integrity": "sha512-LlBxPh/32pyQsu2emMEOFRm7poEFLsw12Y1mPY7FWZiZeptomKSOSHRzKDz9EolMiV4qhK1caP1lvW4vminYgQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz", + "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==", "cpu": [ "arm64" ], @@ -1437,9 +1425,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.11.tgz", - "integrity": "sha512-bOjiZB8O/1AzHkzjge1jqX62HGRIpOHqFUrGPfAln/NC6NR+Z2A78u3ixV7k5KesWZFhCV0YVGJL+qToL27myA==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", + "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", "cpu": [ "x64" ], @@ -1454,9 +1442,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.11.tgz", - "integrity": "sha512-4dzAtbT/m3/UjF045+33gLiHd8aSXJDoqof7gTtu4q0ZyAf7XJ3HHspz+/AvOJLVo4FHHdFcdXhmo/zi1nFn8A==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz", + "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==", "cpu": [ "x64" ], @@ -1471,9 +1459,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.11.tgz", - "integrity": "sha512-h8HiwBZErKvCAmjW92JvQp0iOqm6bncU4ac5jxBGkRApabpUenNJcj3h2g5O6GL5K6T9/WhnXE5gyq/s1fhPQg==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz", + "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==", "cpu": [ "arm64" ], @@ -1488,9 +1476,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.11.tgz", - "integrity": "sha512-1pwr325mXRNUhxTtXmx1IokV5SiRL+6iDvnt3FRXj+X5UvXXKtg2zeyftk+03u8v8v8WUr5I32hIypVJPTNxNg==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz", + "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==", "cpu": [ "ia32" ], @@ -1505,9 +1493,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.12.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.11.tgz", - "integrity": "sha512-5gggWo690Gvs7XiPxAmb5tHwzB9RTVXUV7AWoGb6bmyUd1OXYaebQF0HAOtade5jIoNhfQMQJ7QReRgt/d2jAA==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz", + "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==", "cpu": [ "x64" ], @@ -1538,9 +1526,9 @@ } }, "node_modules/@swc/types": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", - "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.24.tgz", + "integrity": "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1976,18 +1964,18 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -2008,17 +1996,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", - "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/type-utils": "8.36.0", - "@typescript-eslint/utils": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2032,9 +2020,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.36.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -2048,16 +2036,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", - "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -2069,18 +2057,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", - "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.36.0", - "@typescript-eslint/types": "^8.36.0", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -2091,18 +2079,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", - "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2113,9 +2101,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", - "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -2126,18 +2114,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", - "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.36.0", - "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2150,13 +2139,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", - "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -2168,16 +2157,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", - "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.36.0", - "@typescript-eslint/tsconfig-utils": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2193,7 +2182,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2223,16 +2212,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", - "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2243,17 +2232,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", - "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2290,17 +2279,17 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", - "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.11", - "@swc/core": "^1.11.31" + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" + "vite": "^4 || ^5 || ^6 || ^7" } }, "node_modules/@xterm/addon-clipboard": { @@ -2750,9 +2739,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "dev": true, "funding": [ { @@ -3170,16 +3159,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.182", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", - "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==", + "version": "1.5.198", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.198.tgz", + "integrity": "sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -3361,9 +3350,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3373,32 +3362,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -3424,19 +3413,19 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3484,9 +3473,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -3991,13 +3980,13 @@ } }, "node_modules/framer-motion": { - "version": "12.23.3", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.3.tgz", - "integrity": "sha512-llmLkf44zuIZOPSrE4bl4J0UTg6bav+rlKEfMRKgvDMXqBrUtMg6cspoQeRVK3nqRLxTmAJhfGXk39UDdZD7Kw==", + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", "license": "MIT", "dependencies": { - "motion-dom": "^12.23.2", - "motion-utils": "^12.23.2", + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { @@ -4748,9 +4737,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "devOptional": true, "license": "MIT", "bin": { @@ -5256,18 +5245,18 @@ } }, "node_modules/motion-dom": { - "version": "12.23.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.2.tgz", - "integrity": "sha512-73j6xDHX/NvVh5L5oS1ouAVnshsvmApOq4F3VZo5MkYSD/YVsVLal4Qp9wvVgJM9uU2bLZyc0Sn8B9c/MMKk4g==", + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", "license": "MIT", "dependencies": { - "motion-utils": "^12.23.2" + "motion-utils": "^12.23.6" } }, "node_modules/motion-utils": { - "version": "12.23.2", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.2.tgz", - "integrity": "sha512-cIEXlBlXAOUyiAtR0S+QPQUM9L3Diz23Bo+zM420NvSd/oPQJwg6U+rT+WRTpp0rizMsBGQOsAwhWIfglUcZfA==", + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", "license": "MIT" }, "node_modules/ms": { @@ -5764,9 +5753,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5786,15 +5775,15 @@ } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-hot-toast": { @@ -5862,9 +5851,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.93", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.93.tgz", - "integrity": "sha512-uLt3LeUeA0KAjTWKo5JMpLxxhPslXD7o8KOMCRSlfiQaTpqO5JqqJSSxyiQNKnbd3QYoOXsRyw3Uz8EuvSffRA==", + "version": "3.8.106", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.106.tgz", + "integrity": "sha512-ItCHCdhVCzn9huhenuyuHQMOGsl3UMLu5xAO1bkjj4AAgVoktFC1DQ4HWkOS6BGPvUJejFM3Q5hVM8Bl2oX9pA==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -6038,9 +6027,9 @@ } }, "node_modules/rollup": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", - "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6053,26 +6042,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.2", - "@rollup/rollup-android-arm64": "4.44.2", - "@rollup/rollup-darwin-arm64": "4.44.2", - "@rollup/rollup-darwin-x64": "4.44.2", - "@rollup/rollup-freebsd-arm64": "4.44.2", - "@rollup/rollup-freebsd-x64": "4.44.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", - "@rollup/rollup-linux-arm-musleabihf": "4.44.2", - "@rollup/rollup-linux-arm64-gnu": "4.44.2", - "@rollup/rollup-linux-arm64-musl": "4.44.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-musl": "4.44.2", - "@rollup/rollup-linux-s390x-gnu": "4.44.2", - "@rollup/rollup-linux-x64-gnu": "4.44.2", - "@rollup/rollup-linux-x64-musl": "4.44.2", - "@rollup/rollup-win32-arm64-msvc": "4.44.2", - "@rollup/rollup-win32-ia32-msvc": "4.44.2", - "@rollup/rollup-win32-x64-msvc": "4.44.2", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, @@ -6559,9 +6548,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -6722,9 +6711,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -6964,9 +6953,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" diff --git a/ui/package.json b/ui/package.json index 6b80b9e..9f0c298 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "kvm-ui", "private": true, - "version": "0.0.0", + "version": "2025.08.07.001", "type": "module", "engines": { "node": "22.15.0" @@ -19,7 +19,7 @@ "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.2.4", + "@headlessui/react": "^2.2.7", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", "@vitejs/plugin-basic-ssl": "^2.1.0", @@ -33,16 +33,16 @@ "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", "focus-trap-react": "^11.0.4", - "framer-motion": "^12.23.3", + "framer-motion": "^12.23.12", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", - "react": "^19.1.0", + "react": "^19.1.1", "react-animate-height": "^3.2.3", - "react-dom": "^19.1.0", + "react-dom": "^19.1.1", "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.8.93", + "react-simple-keyboard": "^3.8.106", "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -54,21 +54,21 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.32.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.11", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.36.0", - "@typescript-eslint/parser": "^8.36.0", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.30.1", - "eslint-config-prettier": "^10.1.5", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", @@ -78,7 +78,7 @@ "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.11", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" }