From 3d0399b3a8e7fd0d773295ee460326a74327c21b Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 11:18:12 +0200 Subject: [PATCH 01/14] package update --- interface/package.json | 8 +- interface/pnpm-lock.yaml | 311 ++++++++++++++++++++------------------- mock-api/pnpm-lock.yaml | 10 +- 3 files changed, 167 insertions(+), 162 deletions(-) diff --git a/interface/package.json b/interface/package.json index c9ee5fffb..6303df2f5 100644 --- a/interface/package.json +++ b/interface/package.json @@ -26,8 +26,8 @@ "@alova/adapter-xhr": "2.3.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^9.3.1", - "@mui/material": "^9.3.1", + "@mui/icons-material": "^9.4.0", + "@mui/material": "^9.4.0", "@table-library/react-table-library": "4.1.15", "alova": "^3.5.5", "jwt-decode": "^4.0.0", @@ -42,7 +42,7 @@ "@eslint/js": "^10.0.1", "@preact/preset-vite": "^2.10.6", "@trivago/prettier-plugin-sort-imports": "^6.0.2", - "@types/node": "^26.2.0", + "@types/node": "^26.4.0", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.5", "concurrently": "^10.0.5", @@ -52,7 +52,7 @@ "mime-types": "^3.0.2", "prettier": "^3.9.6", "rollup-plugin-visualizer": "^7.1.1", - "terser": "^5.50.0", + "terser": "^5.51.2", "typescript": "^6.0.3", "typescript-eslint": "^8.68.0", "vite": "^8.2.2", diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 5c4fcccab..96510223a 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: ^11.14.1 version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2) '@mui/icons-material': - specifier: ^9.3.1 - version: 9.3.1(@mui/material@9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) + specifier: ^9.4.0 + version: 9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8) '@mui/material': - specifier: ^9.3.1 - version: 9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^9.4.0 + version: 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@table-library/react-table-library': specifier: 4.1.15 version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -56,13 +56,13 @@ importers: version: 10.0.1(eslint@10.9.1(supports-color@10.2.2)) '@preact/preset-vite': specifier: ^2.10.6 - version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0)) + version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2)) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2) '@types/node': - specifier: ^26.2.0 - version: 26.2.0 + specifier: ^26.4.0 + version: 26.4.0 '@types/react': specifier: ^19.2.18 version: 19.2.18 @@ -89,10 +89,10 @@ importers: version: 3.9.6 rollup-plugin-visualizer: specifier: ^7.1.1 - version: 7.1.1(rolldown@1.2.5) + version: 7.1.1(rolldown@1.2.6) terser: - specifier: ^5.50.0 - version: 5.50.0 + specifier: ^5.51.2 + version: 5.51.2 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -101,10 +101,10 @@ importers: version: 8.68.0(eslint@10.9.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) vite: specifier: ^8.2.2 - version: 8.2.2(@types/node@26.2.0)(terser@5.50.0) + version: 8.2.2(@types/node@26.4.0)(terser@5.51.2) vite-plugin-imagemin: specifier: ^0.6.1 - version: 0.6.1(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0)) + version: 0.6.1(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2)) packages: @@ -348,30 +348,33 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mui/core-downloads-tracker@9.3.1': - resolution: {integrity: sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==} + '@mui/core-downloads-tracker@9.4.0': + resolution: {integrity: sha512-d+gIYM1raJP87yrVQcVg+Q3vycVMArMkgu/TjJLi2vTvO9FHKWNRR1xs9nnN4eLpM9nGxxm02UMvc9BrmcUKvQ==} - '@mui/icons-material@9.3.1': - resolution: {integrity: sha512-rZj5ccG7vkpV38o/l4ys+chfE9GFypmvZr9dSNBoYVCNBD9yC6KfKq1TYpEMshWbjic7GKQ8MA1LQlcpGgcq9Q==} + '@mui/icons-material@9.4.0': + resolution: {integrity: sha512-5PVgBYtLOXTk6u0YUAjoV9GoTUQDbeZ8g+pus2h0GphLT/rpmftRnB5D/Kw6QCAovB7EyBX7UxKdZqPBBAHUKw==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^9.3.1 + '@mui/material': ^9.4.0 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/material@9.3.1': - resolution: {integrity: sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==} + '@mui/material@9.4.0': + resolution: {integrity: sha512-6KKUIvkQ2r/s48s9VZV9JEZ1W2dRa7RndVsF+Ipx4CYxDOQeozeRsS5r9NiheFf8A5hpbqJJIGoX7hjg5X4XUw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material-pigment-css': ^9.3.0 + '@mui/material-pigment-css': ^9.4.0 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -385,8 +388,8 @@ packages: '@types/react': optional: true - '@mui/private-theming@9.3.0': - resolution: {integrity: sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==} + '@mui/private-theming@9.4.0': + resolution: {integrity: sha512-qQpX/2XPGDz5F2OIWzo4kkorURlDeXGKytRQudXCdLS5TuwKsMKaZyx77LZEVSB73c7o3UrLWwQ/goRuHBdtjw==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -395,8 +398,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@9.3.0': - resolution: {integrity: sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==} + '@mui/styled-engine@9.4.0': + resolution: {integrity: sha512-SlqVEYqnyEgXMFW4JuIDMelyhKzoWBJ85v2mNCQd0XtU0uk7U9E1zaJZ06XqodhA8oJrb+4Q+3E/JlwBe4jWkw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -408,8 +411,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@9.3.0': - resolution: {integrity: sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==} + '@mui/system@9.4.0': + resolution: {integrity: sha512-AZz9z13oGS1J0BYeAFHTbV7PegCtPiE/uMEIi7TJ1kTx3Vkt+jUpLyXF/2uV8B6Y79iTh98kVeOH9fX3x3zXXg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -424,16 +427,16 @@ packages: '@types/react': optional: true - '@mui/types@9.3.0': - resolution: {integrity: sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==} + '@mui/types@9.4.0': + resolution: {integrity: sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/utils@9.3.0': - resolution: {integrity: sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==} + '@mui/utils@9.4.0': + resolution: {integrity: sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -454,8 +457,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.146.0': - resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -469,8 +472,8 @@ packages: '@prefresh/babel-plugin@0.5.3': resolution: {integrity: sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==} - '@prefresh/core@1.5.10': - resolution: {integrity: sha512-7yPTFbG56sutaFu8krp3B4a200KOFUvrtlllKWRuLjsYXo9UUucHOZRcer+gtgMkFTpv6ob8TGcTwA32bSwa1w==} + '@prefresh/core@1.5.11': + resolution: {integrity: sha512-Ml00PP8jeHaADxfQqxUs4+M5JS+T5J26UnvPVfsrmlpGw2I3e9XtM0DMeuzkmvg+Tn7VdHSMpLBrDPG0iHoDqQ==} peerDependencies: preact: ^10.0.0 || ^11.0.0-0 @@ -483,98 +486,98 @@ packages: preact: ^10.4.0 || ^11.0.0-0 vite: '>=2.0.0' - '@rolldown/binding-android-arm-eabi@1.2.5': - resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.5': - resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.5': - resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.5': - resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.5': - resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.5': - resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.5': - resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.5': - resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.5': - resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.5': - resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.5': - resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.5': - resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.5': - resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.5': - resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.5': - resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -668,8 +671,8 @@ packages: resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - '@types/node@26.2.0': - resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@types/node@26.4.0': + resolution: {integrity: sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1433,8 +1436,8 @@ packages: resolution: {integrity: sha512-a6Qh1RMCNbSrU1+sAyAAZH3rTe+OaWJbNZIq0S+ifZciUUOQtlVxBJwoTUE2bYhysmG/RYyI5WJFIKdBahJdrQ==} hasBin: true - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastq@1.20.2: + resolution: {integrity: sha512-UpGiiODyCGprM8EPP6JodP6jC9Rws6TCuiDOD+nn0CJhR8guI3g/ozo4ugL0vJ+Yz1UtJuuRPqvQuybVOF1VQA==} fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -2505,8 +2508,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rolldown@1.2.5: - resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2750,8 +2753,8 @@ packages: resolution: {integrity: sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==} engines: {node: '>=4'} - terser@5.50.0: - resolution: {integrity: sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==} + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} engines: {node: '>=10'} hasBin: true @@ -3286,28 +3289,30 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mui/core-downloads-tracker@9.3.1': {} + '@mui/core-downloads-tracker@9.4.0': {} - '@mui/icons-material@9.3.1(@mui/material@9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)': + '@mui/icons-material@9.4.0(@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react@19.2.18)(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/material': 9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@mui/material': 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 - '@mui/material@9.3.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@mui/material@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/core-downloads-tracker': 9.3.1 - '@mui/system': 9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8) - '@mui/types': 9.3.0(@types/react@19.2.18) - '@mui/utils': 9.3.0(@types/react@19.2.18)(react@19.2.8) + '@mui/core-downloads-tracker': 9.4.0 + '@mui/system': 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8) + '@mui/types': 9.4.0(@types/react@19.2.18) + '@mui/utils': 9.4.0(@types/react@19.2.18)(react@19.2.8) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.12(@types/react@19.2.18) clsx: 2.1.1 @@ -3322,16 +3327,16 @@ snapshots: '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2) '@types/react': 19.2.18 - '@mui/private-theming@9.3.0(@types/react@19.2.18)(react@19.2.8)': + '@mui/private-theming@9.4.0(@types/react@19.2.18)(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/utils': 9.3.0(@types/react@19.2.18)(react@19.2.8) + '@mui/utils': 9.4.0(@types/react@19.2.18)(react@19.2.8) prop-types: 15.8.1 react: 19.2.8 optionalDependencies: '@types/react': 19.2.18 - '@mui/styled-engine@9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(react@19.2.8)': + '@mui/styled-engine@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@emotion/cache': 11.14.0 @@ -3344,13 +3349,13 @@ snapshots: '@emotion/react': 11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2) - '@mui/system@9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)': + '@mui/system@9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/private-theming': 9.3.0(@types/react@19.2.18)(react@19.2.8) - '@mui/styled-engine': 9.3.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(react@19.2.8) - '@mui/types': 9.3.0(@types/react@19.2.18) - '@mui/utils': 9.3.0(@types/react@19.2.18)(react@19.2.8) + '@mui/private-theming': 9.4.0(@types/react@19.2.18)(react@19.2.8) + '@mui/styled-engine': 9.4.0(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(react@19.2.8) + '@mui/types': 9.4.0(@types/react@19.2.18) + '@mui/utils': 9.4.0(@types/react@19.2.18)(react@19.2.8) clsx: 2.1.1 csstype: 3.2.3 prop-types: 15.8.1 @@ -3360,16 +3365,16 @@ snapshots: '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.8)(supports-color@10.2.2) '@types/react': 19.2.18 - '@mui/types@9.3.0(@types/react@19.2.18)': + '@mui/types@9.4.0(@types/react@19.2.18)': dependencies: '@babel/runtime': 7.29.7 optionalDependencies: '@types/react': 19.2.18 - '@mui/utils@9.3.0(@types/react@19.2.18)(react@19.2.8)': + '@mui/utils@9.4.0(@types/react@19.2.18)(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 - '@mui/types': 9.3.0(@types/react@19.2.18) + '@mui/types': 9.4.0(@types/react@19.2.18) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 @@ -3388,25 +3393,25 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.20.2 - '@oxc-project/types@0.146.0': {} + '@oxc-project/types@0.147.0': {} '@popperjs/core@2.11.8': {} - '@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0))': + '@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2))': dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) - '@prefresh/vite': 2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0)) + '@prefresh/vite': 2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2)) '@rollup/pluginutils': 5.4.0 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.7(supports-color@10.2.2)) debug: 4.4.3(supports-color@10.2.2) magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.2.2(@types/node@26.2.0)(terser@5.50.0) - vite-prerender-plugin: 0.5.13(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0)) + vite: 8.2.2(@types/node@26.4.0)(terser@5.51.2) + vite-prerender-plugin: 0.5.13(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact @@ -3415,67 +3420,67 @@ snapshots: '@prefresh/babel-plugin@0.5.3': {} - '@prefresh/core@1.5.10(preact@10.29.8)': + '@prefresh/core@1.5.11(preact@10.29.8)': dependencies: preact: 10.29.8 '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0))': + '@prefresh/vite@2.4.12(preact@10.29.8)(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2))': dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@prefresh/babel-plugin': 0.5.3 - '@prefresh/core': 1.5.10(preact@10.29.8) + '@prefresh/core': 1.5.11(preact@10.29.8) '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 preact: 10.29.8 - vite: 8.2.2(@types/node@26.2.0)(terser@5.50.0) + vite: 8.2.2(@types/node@26.4.0)(terser@5.51.2) transitivePeerDependencies: - supports-color - '@rolldown/binding-android-arm-eabi@1.2.5': + '@rolldown/binding-android-arm-eabi@1.2.6': optional: true - '@rolldown/binding-android-arm64@1.2.5': + '@rolldown/binding-android-arm64@1.2.6': optional: true - '@rolldown/binding-darwin-arm64@1.2.5': + '@rolldown/binding-darwin-arm64@1.2.6': optional: true - '@rolldown/binding-darwin-x64@1.2.5': + '@rolldown/binding-darwin-x64@1.2.6': optional: true - '@rolldown/binding-freebsd-x64@1.2.5': + '@rolldown/binding-freebsd-x64@1.2.6': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.5': + '@rolldown/binding-linux-arm64-gnu@1.2.6': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.5': + '@rolldown/binding-linux-arm64-musl@1.2.6': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.6': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.5': + '@rolldown/binding-linux-s390x-gnu@1.2.6': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.5': + '@rolldown/binding-linux-x64-gnu@1.2.6': optional: true - '@rolldown/binding-linux-x64-musl@1.2.5': + '@rolldown/binding-linux-x64-musl@1.2.6': optional: true - '@rolldown/binding-openharmony-arm64@1.2.5': + '@rolldown/binding-openharmony-arm64@1.2.6': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.5': + '@rolldown/binding-win32-arm64-msvc@1.2.6': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.5': + '@rolldown/binding-win32-x64-msvc@1.2.6': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -3523,7 +3528,7 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 - '@types/node': 26.2.0 + '@types/node': 26.4.0 '@types/imagemin-gifsicle@7.0.4': dependencies: @@ -3552,23 +3557,23 @@ snapshots: '@types/imagemin@7.0.1': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.4.0 '@types/imagemin@9.0.1': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.4.0 '@types/json-schema@7.0.15': {} '@types/keyv@3.1.4': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.4.0 '@types/minimatch@6.0.0': dependencies: minimatch: 10.2.6 - '@types/node@26.2.0': + '@types/node@26.4.0': dependencies: undici-types: 8.3.0 @@ -3590,11 +3595,11 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.4.0 '@types/svgo@2.6.4': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.4.0 '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: @@ -4436,7 +4441,7 @@ snapshots: dependencies: strnum: 1.1.2 - fastq@1.20.1: + fastq@1.20.2: dependencies: reusify: 1.1.0 @@ -5021,7 +5026,7 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 make-dir@1.3.0: dependencies: @@ -5432,35 +5437,35 @@ snapshots: dependencies: glob: 7.2.3 - rolldown@1.2.5: + rolldown@1.2.6: dependencies: - '@oxc-project/types': 0.146.0 + '@oxc-project/types': 0.147.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.5 - '@rolldown/binding-android-arm64': 1.2.5 - '@rolldown/binding-darwin-arm64': 1.2.5 - '@rolldown/binding-darwin-x64': 1.2.5 - '@rolldown/binding-freebsd-x64': 1.2.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.5 - '@rolldown/binding-linux-arm64-gnu': 1.2.5 - '@rolldown/binding-linux-arm64-musl': 1.2.5 - '@rolldown/binding-linux-ppc64-gnu': 1.2.5 - '@rolldown/binding-linux-s390x-gnu': 1.2.5 - '@rolldown/binding-linux-x64-gnu': 1.2.5 - '@rolldown/binding-linux-x64-musl': 1.2.5 - '@rolldown/binding-openharmony-arm64': 1.2.5 - '@rolldown/binding-win32-arm64-msvc': 1.2.5 - '@rolldown/binding-win32-x64-msvc': 1.2.5 + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 - rollup-plugin-visualizer@7.1.1(rolldown@1.2.5): + rollup-plugin-visualizer@7.1.1(rolldown@1.2.6): dependencies: open: 11.0.1 picomatch: 4.0.7 source-map: 0.8.0 yargs: 18.1.0 optionalDependencies: - rolldown: 1.2.5 + rolldown: 1.2.6 run-applescript@7.1.0: {} @@ -5664,7 +5669,7 @@ snapshots: temp-dir: 1.0.0 uuid: 3.4.0 - terser@5.50.0: + terser@5.51.2: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.18.0 @@ -5775,7 +5780,7 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0)): + vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2)): dependencies: '@types/imagemin': 7.0.1 '@types/imagemin-gifsicle': 7.0.4 @@ -5800,11 +5805,11 @@ snapshots: imagemin-webp: 6.1.0 jpegtran-bin: 6.0.1 pathe: 0.2.0 - vite: 8.2.2(@types/node@26.2.0)(terser@5.50.0) + vite: 8.2.2(@types/node@26.4.0)(terser@5.51.2) transitivePeerDependencies: - supports-color - vite-prerender-plugin@0.5.13(vite@8.2.2(@types/node@26.2.0)(terser@5.50.0)): + vite-prerender-plugin@0.5.13(vite@8.2.2(@types/node@26.4.0)(terser@5.51.2)): dependencies: kolorist: 1.8.0 magic-string: 0.30.21 @@ -5812,19 +5817,19 @@ snapshots: simple-code-frame: 1.3.0 source-map: 0.7.6 stack-trace: 1.0.0 - vite: 8.2.2(@types/node@26.2.0)(terser@5.50.0) + vite: 8.2.2(@types/node@26.4.0)(terser@5.51.2) - vite@8.2.2(@types/node@26.2.0)(terser@5.50.0): + vite@8.2.2(@types/node@26.4.0)(terser@5.51.2): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 postcss: 8.5.26 - rolldown: 1.2.5 + rolldown: 1.2.6 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.2.0 + '@types/node': 26.4.0 fsevents: 2.3.3 - terser: 5.50.0 + terser: 5.51.2 which-typed-array@1.1.22: dependencies: diff --git a/mock-api/pnpm-lock.yaml b/mock-api/pnpm-lock.yaml index 3dc6869ff..fffb7c7ae 100644 --- a/mock-api/pnpm-lock.yaml +++ b/mock-api/pnpm-lock.yaml @@ -71,8 +71,8 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -227,17 +227,17 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@msgpack/msgpack@3.1.3': {} From 319dfe69446c1c4b9ce0c59deb7e4232ca73810a Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 11:18:19 +0200 Subject: [PATCH 02/14] update words --- project-words.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project-words.txt b/project-words.txt index 70eb94d0a..242eb64b1 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1344,4 +1344,5 @@ IILE Sumr eraseap roomtempoffset -dhcpc \ No newline at end of file +dhcpc +wifioff \ No newline at end of file From 416fecfaede71987f89383b7b11a4f5ae9b5a327 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 11:18:29 +0200 Subject: [PATCH 03/14] 3.9.0-dev.6 --- src/emsesp_version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 19dbf2a47..e7d93e24b 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.9.0-dev.5" +#define EMSESP_APP_VERSION "3.9.0-dev.6" From 98a6199a6934167db9c2e7883861fc8abdfdb7e2 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 11:18:53 +0200 Subject: [PATCH 04/14] fixes for email service --- .../src/app/settings/ApplicationSettings.tsx | 10 ++++++- mock-api/restServer.ts | 11 ++++++- src/core/system.cpp | 29 +++++++++++-------- src/web/WebSettingsService.cpp | 5 ++-- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/interface/src/app/settings/ApplicationSettings.tsx b/interface/src/app/settings/ApplicationSettings.tsx index 15cd5770b..53ef9841f 100644 --- a/interface/src/app/settings/ApplicationSettings.tsx +++ b/interface/src/app/settings/ApplicationSettings.tsx @@ -430,7 +430,15 @@ const ApplicationSettings = () => { label={LL.SECURITY(0)} value={data.email_security} variant="outlined" - onChange={updateFormValue} + onChange={(event) => + updateFormValue({ + target: { + name: 'email_security', + type: 'number', + value: event.target.value + } + } as React.ChangeEvent) + } margin="normal" select > diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index 7f9de376a..919620cb9 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -65,7 +65,16 @@ let settings = { modbus_max_clients: 10, modbus_timeout: 10000, developer_mode: true, - disable_reset: false + disable_reset: false, + email_enabled: false, + email_security: 2, + email_server: 'smtp.gmail.com', + email_port: 587, + email_login: 'test@emsesp.org', + email_pass: 'password', + email_sender: 'test@emsesp.org', + email_recp: 'test@emsesp.org', + email_subject: 'ems-esp notification' }; // EMS-ESP System Settings diff --git a/src/core/system.cpp b/src/core/system.cpp index a4e595dd3..2c900e5da 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -147,7 +147,7 @@ bool System::command_sendmail(const char * value, const int8_t) { port, security == EMAIL_SECURITY::SSL ? " (SSL)" : security == EMAIL_SECURITY::STARTTLS ? " (STARTTLS)" - : "", + : " (plain)", value); bool success = false; @@ -158,7 +158,11 @@ bool System::command_sendmail(const char * value, const int8_t) { auto * r_client = new ReadyClient(*ssl_client); auto * smtp = new SMTPClient(*r_client); - ssl_client->setClient(basic_client); + // enableSSL is baked in at setClient() time (same as HTTP fetch). Default true + // would send a TLS Client Hello on connect — which a plain port-25 server + // logs as "SMTP syntax error" / NUL characters. + const bool implicit_ssl = (security == EMAIL_SECURITY::SSL); + ssl_client->setClient(basic_client, implicit_ssl); ssl_client->setInsecure(); ssl_client->setBufferSizes(16384, 1024); basic_client->setTimeout(5000); // socket-level read timeout @@ -168,7 +172,6 @@ bool System::command_sendmail(const char * value, const int8_t) { : security == EMAIL_SECURITY::SSL ? readymail_protocol_ssl : readymail_protocol_tls); - // smtp->connect(server, port, sendmailCallback); smtp->connect(server, port); if (!smtp->isConnected()) { @@ -180,15 +183,17 @@ bool System::command_sendmail(const char * value, const int8_t) { return false; } - // LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str()); - smtp->authenticate(login, pass, readymail_auth_password); - if (!smtp->isAuthenticated()) { - LOG_ERROR("send email authentication error"); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; - return false; + // internal/open relays often have no AUTH; empty login means skip it + if (!login.isEmpty()) { + smtp->authenticate(login, pass, readymail_auth_password); + if (!smtp->isAuthenticated()) { + LOG_ERROR("send email authentication error"); + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; + return false; + } } JsonDocument doc(PSRAM_DOC); String body = value; diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index f4489a0da..e46d1e093 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -316,8 +316,9 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL; settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT; - settings.email_enabled = root["email_enabled"] | FACTORY_EMAIL_ENABLE; - settings.email_security = root["email_security"] | FACTORY_EMAIL_SECURITY; + settings.email_enabled = root["email_enabled"] | FACTORY_EMAIL_ENABLE; + // Off is 0, so `| FACTORY_EMAIL_SECURITY` would replace it with STARTTLS (2) + settings.email_security = root["email_security"].isNull() ? FACTORY_EMAIL_SECURITY : root["email_security"].as(); settings.email_server = root["email_server"] | FACTORY_EMAIL_SERVER; settings.email_port = root["email_port"] | FACTORY_EMAIL_PORT; settings.email_login = root["email_login"] | FACTORY_EMAIL_LOGIN; From 94926b0ca7cdbc4b945920409b77d360c68c9c3a Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 11:19:01 +0200 Subject: [PATCH 05/14] S3 wifi support --- src/core/network.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/network.cpp b/src/core/network.cpp index cf3d9fd5a..b62baeb64 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -78,20 +78,20 @@ void Network::begin() { phase_ = initialPhase(); - // Initialise WiFi once when the Network service starts + // Initialise WiFi once when the Network service starts. + // persistent(false) keeps credentials in RAM so we don't auto-join leftover NVS config. WiFi.persistent(false); WiFi.setAutoReconnect(false); WiFi.mode(WIFI_STA); + // Keep the radio up. disconnect(wifioff=true) maps to STA.end() -> esp_wifi_stop() if (WiFi.STA.started()) { - WiFi.disconnect(true, true); // wipe old settings; only when STA is up - } else { - WiFi.disconnect(true, false); // disconnect/off without erase, so no warning is shown + WiFi.disconnect(false, true); // drop leftover association, wipe RAM config } WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer - WiFi.enableSTA(true); // creates the STA netif + WiFi.enableSTA(true); // no-op if already STA; recreates netif after a full off WiFi.STA.setHostname(hostname_.c_str()); // pushes to esp_netif_set_hostname WiFi.enableIPv6(true); if (staticIPConfig_) { @@ -220,10 +220,10 @@ void Network::reconnect() { LOG_DEBUG("Reconnecting all networks"); #ifndef EMSESP_STANDALONE - // disconnect WiFi + // Drop STA association but leave the driver running. begin() re-applies hostname + // and IPv6; tearing the radio down here races with WIFI_EVENT_STA_START (12289/259). if (wifi_connected()) { - WiFi.disconnect(true, true); - WiFi.mode(WIFI_STA); // reset mode + WiFi.disconnect(false, true); } // disconnect AP From 4d4d716e5f0712c5cebefbabf5138c4bab6c0575 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 16:26:08 +0200 Subject: [PATCH 06/14] pkg update --- interface/pnpm-lock.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 96510223a..d26ecf751 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -345,9 +345,6 @@ packages: '@jridgewell/source-map@0.3.11': resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.6.0': resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} @@ -3272,7 +3269,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -3287,14 +3284,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@mui/core-downloads-tracker@9.4.0': {} From db5ccb018f385cb5d59c812612449e2fc16f5c68 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 16:26:26 +0200 Subject: [PATCH 07/14] improve http fallouts, move to its own class --- lib_standalone/WiFiClient.h | 2 + src/core/httpClient.cpp | 152 ++++++++++++++++++++++++++++++++++ src/core/httpClient.h | 41 +++++++++ src/core/shuntingYard.cpp | 100 +--------------------- src/core/shuntingYard.h | 6 +- src/web/WebCommandService.cpp | 3 +- src/web/WebStatusService.cpp | 8 +- 7 files changed, 204 insertions(+), 108 deletions(-) create mode 100644 src/core/httpClient.cpp create mode 100644 src/core/httpClient.h diff --git a/lib_standalone/WiFiClient.h b/lib_standalone/WiFiClient.h index 3266f5c87..cb8a36306 100644 --- a/lib_standalone/WiFiClient.h +++ b/lib_standalone/WiFiClient.h @@ -41,6 +41,8 @@ class WiFiClient { } void setTimeout(uint32_t){ + } + void setConnectionTimeout(uint32_t) { } // ESP32 socket option passthrough (e.g. TCP_NODELAY) int setSocketOption(int, int, const void *, size_t) { diff --git a/src/core/httpClient.cpp b/src/core/httpClient.cpp new file mode 100644 index 000000000..8f0cfe22d --- /dev/null +++ b/src/core/httpClient.cpp @@ -0,0 +1,152 @@ +/* + * EMS-ESP - https://github.com/emsesp/EMS-ESP + * Copyright 2020-2026 emsesp.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "emsesp.h" + +#include "httpClient.h" + +#include +#include + +namespace emsesp { + +int HttpClient::request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result) { + int httpResult = 0; + const bool is_post = value.length() || Helpers::toLower(method) == "post"; + const auto lower_url = Helpers::toLower(url.c_str()); + + const bool is_https = lower_url.starts_with("https://"); + if (!is_https && !lower_url.starts_with("http://")) { + return 0; // unsupported scheme + } + + WiFiClient * basic_client = new WiFiClient; + ESP_SSLClient * ssl_client = new ESP_SSLClient; + if (is_https) { + ssl_client->setInsecure(); + // Notes: with root CA we should set here: ssl_client->setCACert(rootCACert); + // 1 KB RX buffer is fine for small JSON-style endpoints used by the scheduler/shunting-yard but it is NOT enough for servers that send full-size TLS records (>1 KB) + ssl_client->setBufferSizes(16384, 1024); + ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds) + } + // WiFiClient is NetworkClient, which declares no setTimeout() of its own - calling it binds to + // Stream::setTimeout() and only affects readBytes(), leaving the socket on the core's 3s default + basic_client->setConnectionTimeout(CONNECT_TIMEOUT_MS); + ssl_client->setTimeout(5); // seconds, drives BearSSL only - unused on the plain HTTP path + ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP + + url.replace(0, is_https ? 8 : 7, ""); + std::string host = url; + auto index = url.find_first_of('/'); + if (index != std::string::npos) { + host = url.substr(0, index); + url.replace(0, index, ""); + } else { + url = "/"; + } + + const uint16_t port = is_https ? 443 : 80; + if (ssl_client->connect(host.c_str(), port)) { + bool content_set = false; + bool agent_set = false; + + // assemble the request in one buffer. Sent as a dozen small writes it is left to Nagle to + // dribble them out an ACK at a time, which on a slow link can cost more than the read budget + std::string req = (is_post ? "POST " : "GET ") + url + " HTTP/1.1\r\nHost: " + host + "\r\n"; + for (JsonPairConst p : headers) { + const auto key = Helpers::toLower(p.key().c_str()); + content_set |= (key == "content-type"); + agent_set |= (key == "user-agent"); + req += std::string(p.key().c_str()) + ": " + p.value().as() + "\r\n"; + } + if (!agent_set) { + req += "User-Agent: EMS-ESP\r\n"; // CDNs are more likely to stall on a request without one + } + if (is_post) { + if (!content_set) { + req += "Content-Type: "; + req += value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain; + req += "\r\n"; + } + req += "Content-Length: " + std::to_string(value.length()) + "\r\n"; + } + req += "Connection: close\r\n\r\n"; // the blank line terminates the headers - without it the server never responds + + ssl_client->print(req.c_str()); + if (is_post && value.length()) { + ssl_client->print(value.c_str()); // sent separately so the body isn't copied into req + } + + // available() drops to zero between TCP segments, so stopping at the first gap truncates + // any response that doesn't arrive in a single packet. Keep reading until the peer closes, + // the stream goes idle, or the overall budget runs out + const uint32_t started = millis(); + uint32_t last_data = started; + while (millis() - started < TOTAL_TIMEOUT_MS) { + const int avail = ssl_client->available(); + if (avail > 0) { + uint8_t buf[128]; + const size_t want = (avail < (int)sizeof(buf)) ? (size_t)avail : sizeof(buf); + const int len = ssl_client->read(buf, want); + if (len > 0) { + result.append(reinterpret_cast(buf), len); + last_data = millis(); + } + continue; + } + if (!ssl_client->connected()) { + break; // closed, with nothing left buffered + } + // the server may take a while to start replying + if (millis() - last_data > (result.empty() ? FIRST_BYTE_TIMEOUT_MS : IDLE_TIMEOUT_MS)) { + break; + } + delay(1); + } + ssl_client->stop(); + + const auto received = result.length(); + + // parse the status line "HTTP/1.x ". stoi() would abort rather than throw on + // a malformed response, since the firmware is built with -fno-exceptions + if (result.starts_with("HTTP/")) { + index = result.find_first_of(' '); + if (index != std::string::npos) { + httpResult = Helpers::atoint(result.c_str() + index + 1); + } + } + + index = result.find("\r\n\r\n"); + if (index != std::string::npos) { + result.replace(0, index + 4, ""); + } + + if (httpResult == 0) { + // the TCP connect worked but nothing usable came back + EMSESP::logger().warning("%s no valid response from %s (%u bytes)", is_https ? "HTTPS" : "HTTP", host.c_str(), (unsigned)received); + } + } else { + EMSESP::logger().warning("%s connection to %s failed", is_https ? "HTTPS" : "HTTP", host.c_str()); + } + delete ssl_client; + delete basic_client; + + return httpResult; +} + +} // namespace emsesp diff --git a/src/core/httpClient.h b/src/core/httpClient.h new file mode 100644 index 000000000..c4bd0649a --- /dev/null +++ b/src/core/httpClient.h @@ -0,0 +1,41 @@ +/* + * EMS-ESP - https://github.com/emsesp/EMS-ESP + * Copyright 2020-2026 emsesp.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EMSESP_HTTPCLIENT_H +#define EMSESP_HTTPCLIENT_H + +#include + +#include + +namespace emsesp { + +class HttpClient { + public: + static int request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result); + + private: + static constexpr uint32_t CONNECT_TIMEOUT_MS = 5000; // TCP connect, the core defaults to 3s + static constexpr uint32_t FIRST_BYTE_TIMEOUT_MS = 8000; // how long the server may take to start replying + static constexpr uint32_t IDLE_TIMEOUT_MS = 500; // gap in the stream that marks the end of a response + static constexpr uint32_t TOTAL_TIMEOUT_MS = 10000; // ceiling for the whole read +}; + +} // namespace emsesp + +#endif diff --git a/src/core/shuntingYard.cpp b/src/core/shuntingYard.cpp index 08957e880..202cf1e09 100644 --- a/src/core/shuntingYard.cpp +++ b/src/core/shuntingYard.cpp @@ -22,8 +22,7 @@ #include "shuntingYard.h" -#include -#include +#include "httpClient.h" namespace emsesp { @@ -336,7 +335,6 @@ bool isnum(const std::string & s) { return false; } - // replace commands like "//" with its value" std::string commands(std::string & expr, bool quotes) { auto expr_new = Helpers::toLower(expr); @@ -691,100 +689,6 @@ std::string calculate(const std::string & expr) { return result; } -// perform an HTTP/HTTPS request; returns the HTTP status code (0 on failure or unsupported scheme) -// the response headers are always stripped, so `result` contains only the body -int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result) { - int httpResult = 0; - const bool is_post = value.length() || Helpers::toLower(method) == "post"; - const auto lower_url = Helpers::toLower(url.c_str()); - - const bool is_https = lower_url.starts_with("https://"); - if (!is_https && !lower_url.starts_with("http://")) { - return 0; // unsupported scheme - } - - WiFiClient * basic_client = new WiFiClient; - ESP_SSLClient * ssl_client = new ESP_SSLClient; - if (is_https) { - ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert); - // NOTE: 1 KB RX buffer is fine for small JSON-style endpoints used by the scheduler/shunting-yard, - // but it is NOT enough for servers that send full-size TLS records (>1 KB), e.g. GitHub release - // assets / large CDN responses. Such servers do not negotiate max_fragment_length, so the body - // can't be decoded and reads return 0. If this path is ever used to fetch large or CDN-hosted - // payloads, bump the RX buffer to 16384 (see uploadFirmwareURL in core/system.cpp for reference). - ssl_client->setBufferSizes(16384, 1024); - ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds) - } - basic_client->setTimeout(5000); // socket-level read timeout - ssl_client->setTimeout(5); // Stream::readBytes timeout used by Update - ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP - - url.replace(0, is_https ? 8 : 7, ""); - std::string host = url; - auto index = url.find_first_of('/'); - if (index != std::string::npos) { - host = url.substr(0, index); - url.replace(0, index, ""); - } else { - url = "/"; - } - - const uint16_t port = is_https ? 443 : 80; - if (ssl_client->connect(host.c_str(), port)) { - bool content_set = false; - ssl_client->print(is_post ? "POST " : "GET "); - ssl_client->print(url.c_str()); - ssl_client->println(" HTTP/1.1"); - ssl_client->print("Host: "); - ssl_client->println(host.c_str()); - for (JsonPairConst p : headers) { - content_set |= (Helpers::toLower(p.key().c_str()) == "content-type"); - ssl_client->print(p.key().c_str()); - ssl_client->print(": "); - ssl_client->println(p.value().as().c_str()); - } - if (is_post) { - if (!content_set) { - ssl_client->print("Content-Type: "); - ssl_client->println(value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain); - } - ssl_client->print("Content-Length: "); - ssl_client->println(value.length()); - ssl_client->println("Connection: close"); - ssl_client->print("\r\n"); - ssl_client->print(value.c_str()); - } else { - ssl_client->println("Connection: close"); - ssl_client->print("\r\n"); // terminate headers - without this the server never responds - } - - auto ms = millis(); - while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) { - delay(1); - } - - while (ssl_client->available()) { - result += (char)ssl_client->read(); - } - ssl_client->stop(); - - index = result.find_first_of(' '); - if (index != std::string::npos) { - httpResult = stoi(result.substr(index + 1, 3)); - } - index = result.find("\r\n\r\n"); - if (index != std::string::npos) { - result.replace(0, index + 4, ""); - } - } else { - EMSESP::logger().warning("%s connection failed", is_https ? "HTTPS" : "HTTP"); - } - delete ssl_client; - delete basic_client; - - return httpResult; -} - // check for multiple instances of ? : std::string compute(const std::string & expr) { std::string expr_new = expr; @@ -829,7 +733,7 @@ std::string compute(const std::string & expr) { std::string method = doc[method_s] | "GET"; std::string result; - int httpResult = http_request(url, method, value, doc[header_s].as(), result); + int httpResult = HttpClient::request(url, method, value, doc[header_s].as(), result); if (httpResult == 200) { std::string key = doc[key_s] | ""; JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body diff --git a/src/core/shuntingYard.h b/src/core/shuntingYard.h index 23e8486ec..7f8f66f77 100644 --- a/src/core/shuntingYard.h +++ b/src/core/shuntingYard.h @@ -84,8 +84,6 @@ std::string calculate(const std::string & expr); // check for multiple instances of ? : std::string compute(const std::string & expr); -int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result); +} // namespace emsesp -#endif - -} // namespace emsesp \ No newline at end of file +#endif \ No newline at end of file diff --git a/src/web/WebCommandService.cpp b/src/web/WebCommandService.cpp index bf71c4e4c..35350599d 100644 --- a/src/web/WebCommandService.cpp +++ b/src/web/WebCommandService.cpp @@ -20,6 +20,7 @@ #include "WebCommandService.h" #include "shuntingYard.h" +#include "httpClient.h" namespace emsesp { @@ -267,7 +268,7 @@ bool WebCommandService::executeCommand(const char * name, const std::string & co auto lower_url = Helpers::toLower(url.c_str()); if (lower_url.starts_with("http://") || lower_url.starts_with("https://")) { std::string result; - int httpResult = http_request(url, method, value, doc["header"].as(), result); + int httpResult = HttpClient::request(url, method, value, doc["header"].as(), result); if (httpResult != 200) { EMSESP::logger().warning("Command '%s': URL command failed with http code %d", name, httpResult); return false; diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index ce4fd1a61..5938fa009 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -20,10 +20,8 @@ #ifndef EMSESP_STANDALONE #include -#include -#include #endif -#include "shuntingYard.h" +#include "httpClient.h" namespace emsesp { @@ -427,9 +425,9 @@ bool WebStatusService::refresh_versions_cache() { #else std::string result; JsonDocument doc; - auto http_code = http_request(VERSIONS_URL, "GET", "", doc.as(), result); + auto http_code = HttpClient::request(VERSIONS_URL, "GET", "", doc.as(), result); if (http_code != 200) { - EMSESP::logger().warning("refresh_versions_cache() HTTP error code %d", http_code); + EMSESP::logger().warning("Unable to retrieve online version information (HTTP error code %d)", http_code); return false; } DeserializationError err = deserializeJson(doc, result); From 4d8d3f0d46f208d82794d3b386e8a0b453bb0609 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Aug 2026 20:06:32 +0200 Subject: [PATCH 08/14] fix email security as number --- interface/src/app/settings/ApplicationSettings.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/interface/src/app/settings/ApplicationSettings.tsx b/interface/src/app/settings/ApplicationSettings.tsx index 53ef9841f..15cd5770b 100644 --- a/interface/src/app/settings/ApplicationSettings.tsx +++ b/interface/src/app/settings/ApplicationSettings.tsx @@ -430,15 +430,7 @@ const ApplicationSettings = () => { label={LL.SECURITY(0)} value={data.email_security} variant="outlined" - onChange={(event) => - updateFormValue({ - target: { - name: 'email_security', - type: 'number', - value: event.target.value - } - } as React.ChangeEvent) - } + onChange={updateFormValue} margin="normal" select > From 0991ed467e827729c642567effbd3e047c24f12c Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Aug 2026 10:17:07 +0200 Subject: [PATCH 09/14] dont use ReadyMail/ESP_SSL if security is off --- src/core/system.cpp | 79 +++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index 2c900e5da..d9bd8e328 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -40,9 +40,6 @@ #ifndef EMSESP_STANDALONE #define ENABLE_SMTP -#define USE_ESP_SSLCLIENT -#define READYCLIENT_SSL_CLIENT ESP_SSLClient -#define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient #include #include #endif @@ -94,6 +91,13 @@ std::vector> System::valid_ std::vector> System::used_gpios_; #ifndef EMSESP_STANDALONE +// TLSHandshakeCallback is a C function pointer, so STARTTLS needs a process-wide client slot. +static ESP_SSLClient * sendmail_ssl_client = nullptr; + +static void sendmail_tls_handshake(bool & success) { + success = sendmail_ssl_client != nullptr && sendmail_ssl_client->connectSSL(); +} + // The DNS slots are shared between IPv4 and IPv6, so with IPv6 enabled the first slot can hold an // IPv6 server. Pick the first IPv4 one so it matches the label we print it under. static IPAddress dns_ipv4(const NetworkInterface & netif) { @@ -153,33 +157,50 @@ bool System::command_sendmail(const char * value, const int8_t) { bool success = false; #ifndef EMSESP_STANDALONE - auto * basic_client = new WiFiClient; - auto * ssl_client = new ESP_SSLClient; - auto * r_client = new ReadyClient(*ssl_client); - auto * smtp = new SMTPClient(*r_client); + // Do not use ReadyClient: after the 220 greeting it calls connectSSL() whenever + // STARTTLS is off, so Security=Off still sends a TLS Client Hello on port 25. + auto * basic_client = new WiFiClient; + ESP_SSLClient * ssl_client = nullptr; + SMTPClient * smtp = nullptr; + + basic_client->setTimeout(5000); - // enableSSL is baked in at setClient() time (same as HTTP fetch). Default true - // would send a TLS Client Hello on connect — which a plain port-25 server - // logs as "SMTP syntax error" / NUL characters. const bool implicit_ssl = (security == EMAIL_SECURITY::SSL); - ssl_client->setClient(basic_client, implicit_ssl); - ssl_client->setInsecure(); - ssl_client->setBufferSizes(16384, 1024); - basic_client->setTimeout(5000); // socket-level read timeout - ssl_client->setTimeout(5); // Stream::readBytes timeout used by Update - r_client->addPort(port, - security == EMAIL_SECURITY::NONE ? readymail_protocol_plain_text - : security == EMAIL_SECURITY::SSL ? readymail_protocol_ssl - : readymail_protocol_tls); + const bool start_tls = (security == EMAIL_SECURITY::STARTTLS); - // smtp->connect(server, port, sendmailCallback); - smtp->connect(server, port); - if (!smtp->isConnected()) { - LOG_ERROR("send email connection error"); + if (security == EMAIL_SECURITY::NONE) { + smtp = new SMTPClient(*basic_client); + } else { + ssl_client = new ESP_SSLClient; + ssl_client->setInsecure(); + ssl_client->setBufferSizes(16384, 1024); + ssl_client->setTimeout(5); + // enableSSL is baked in at setClient() — true only for implicit SSL (port 465) + ssl_client->setClient(basic_client, implicit_ssl); + if (start_tls) { + sendmail_ssl_client = ssl_client; + smtp = new SMTPClient(*ssl_client, sendmail_tls_handshake, true); + } else { + smtp = new SMTPClient(*ssl_client); + } + } + + auto cleanup = [&]() { delete smtp; - delete r_client; + sendmail_ssl_client = nullptr; delete ssl_client; delete basic_client; + }; + + // ssl defaults to true; Off and STARTTLS must start as plain SMTP + if (!smtp->connect(server, port, String(""), static_cast(nullptr), implicit_ssl)) { + LOG_ERROR("send email connection error"); + cleanup(); + return false; + } + if (!smtp->isConnected()) { + LOG_ERROR("send email connection error"); + cleanup(); return false; } @@ -188,10 +209,7 @@ bool System::command_sendmail(const char * value, const int8_t) { smtp->authenticate(login, pass, readymail_auth_password); if (!smtp->isAuthenticated()) { LOG_ERROR("send email authentication error"); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; + cleanup(); return false; } } @@ -236,10 +254,7 @@ bool System::command_sendmail(const char * value, const int8_t) { success = smtp->send(msg); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; + cleanup(); #endif return success; } From 04f99688eb7710687e03ca115e2dceeef2d6b81a Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Aug 2026 14:41:34 +0200 Subject: [PATCH 10/14] pkg update --- interface/package.json | 2 +- interface/pnpm-lock.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/interface/package.json b/interface/package.json index 6303df2f5..8697fa935 100644 --- a/interface/package.json +++ b/interface/package.json @@ -35,7 +35,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "react-icons": "^5.7.0", - "react-router": "^8.3.0", + "react-router": "^8.3.1", "typesafe-i18n": "^5.27.1" }, "devDependencies": { diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index d26ecf751..caaf4c043 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^5.7.0 version: 5.7.0(react@19.2.8) react-router: - specifier: ^8.3.0 - version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^8.3.1 + version: 8.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) typesafe-i18n: specifier: ^5.27.1 version: 5.27.1(typescript@6.0.3) @@ -1433,8 +1433,8 @@ packages: resolution: {integrity: sha512-a6Qh1RMCNbSrU1+sAyAAZH3rTe+OaWJbNZIq0S+ifZciUUOQtlVxBJwoTUE2bYhysmG/RYyI5WJFIKdBahJdrQ==} hasBin: true - fastq@1.20.2: - resolution: {integrity: sha512-UpGiiODyCGprM8EPP6JodP6jC9Rws6TCuiDOD+nn0CJhR8guI3g/ozo4ugL0vJ+Yz1UtJuuRPqvQuybVOF1VQA==} + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -2428,8 +2428,8 @@ packages: react-is@19.2.8: resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} - react-router@8.3.0: - resolution: {integrity: sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==} + react-router@8.3.1: + resolution: {integrity: sha512-TEOpiO2g0TJHEOJeRVv4amUFun9v1npCKszvcquNvzETUtJ8udV86ah5eFoHT7g26bsBvT6EiIhqulR8eDF++A==} engines: {node: '>=22.22.0'} peerDependencies: react: '>=19.2.7' @@ -3388,7 +3388,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.2 + fastq: 1.20.3 '@oxc-project/types@0.147.0': {} @@ -4436,7 +4436,7 @@ snapshots: dependencies: strnum: 1.1.2 - fastq@1.20.2: + fastq@1.20.3: dependencies: reusify: 1.1.0 @@ -5351,7 +5351,7 @@ snapshots: react-is@19.2.8: {} - react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-router@8.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: cookie-es: 3.1.1 react: 19.2.8 From 84d67c0946a9469a2f3fb478a37cba63e05e419a Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Aug 2026 14:42:42 +0200 Subject: [PATCH 11/14] dont run the shunting-yard twice on sendmail values. Sending literal text through `system/sendmail` logged a warning and dropped the computed body --- CHANGELOG_LATEST.md | 2 + .../src/app/settings/ApplicationSettings.tsx | 2 +- src/core/shuntingYard.cpp | 40 ++++++++++++++----- src/core/system.cpp | 38 +++++++++--------- src/web/WebCommandService.cpp | 15 +++++-- src/web/WebCommandService.h | 1 + test/test_api/test_shuntingYard.h | 30 ++++++++++++++ 7 files changed, 94 insertions(+), 34 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 41ca8ec4e..d2deaaf3f 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -16,6 +16,8 @@ This release is based on the latest Espressif/Arduino core version 3. It brings ## Fixed - shunting yard show json +- `system/sendmail` called from a Command or the Scheduler computed its value twice, which stripped the quotes from literal text and then failed on characters like `!` +- shunting yard treated a `?` inside a quoted string as a ternary, so text like `"too hot?"` returned nothing - memory leak when using different timezones for ems-esp and thermostat[#3184](https://github.com/emsesp/EMS-ESP32/issues/3184) - LED stayed off on a healthy system when "Disable LED" was unchecked - Ethernet MAC address changed with the new SDK, breaking DHCP reservations (currently disabled) diff --git a/interface/src/app/settings/ApplicationSettings.tsx b/interface/src/app/settings/ApplicationSettings.tsx index 15cd5770b..d9626d41b 100644 --- a/interface/src/app/settings/ApplicationSettings.tsx +++ b/interface/src/app/settings/ApplicationSettings.tsx @@ -177,7 +177,7 @@ const ApplicationSettings = () => { await sendAPI({ device: 'system', cmd: 'sendmail', - data: 'Email notification test successful!', + data: '"Email notification test successful!"', id: 0 }) .then(() => { diff --git a/src/core/shuntingYard.cpp b/src/core/shuntingYard.cpp index 202cf1e09..a60fc77a6 100644 --- a/src/core/shuntingYard.cpp +++ b/src/core/shuntingYard.cpp @@ -479,14 +479,14 @@ std::string calculate(const std::string & expr) { const auto rhs = stack.back(); stack.pop_back(); if (token.str[0] == '!') { - if (to_logic(rhs) < 0) { - } - if (to_logic(rhs) >= 0) { - stack.push_back(to_logic(rhs) == 0 ? "1" : "0"); + const auto logic = to_logic(rhs); + if (logic >= 0) { + stack.push_back(logic == 0 ? "1" : "0"); } else if (isnum(rhs)) { stack.push_back(std::stod(rhs) == 0 ? "1" : "0"); } else { - EMSESP::logger().warning("missing operator"); + // usually literal text that was left unquoted, so its '!' got read as a logical NOT + EMSESP::logger().warning("'!' needs a boolean or numeric operand, got '%s'. Literal text must be quoted", rhs.c_str()); return ""; } break; @@ -689,6 +689,24 @@ std::string calculate(const std::string & expr) { return result; } +// find the next occurrence of c at or after `from` that is not inside a quoted string, so that +// literal text like "too hot?" is not mistaken for a ternary. Always scans from the start of the +// string because the quote state at `from` depends on everything before it. +static size_t find_unquoted(const std::string & s, char c, size_t from = 0) { + bool in_single = false; + bool in_double = false; + for (size_t i = 0; i < s.length(); i++) { + if (!in_single && s[i] == '"') { + in_double = !in_double; + } else if (!in_double && s[i] == '\'') { + in_single = !in_single; + } else if (!in_single && !in_double && s[i] == c && i >= from) { + return i; + } + } + return std::string::npos; +} + // check for multiple instances of ? : std::string compute(const std::string & expr) { std::string expr_new = expr; @@ -772,14 +790,14 @@ std::string compute(const std::string & expr) { } // positions: q-questionmark, c-colon - auto q = expr_new.find_first_of('?'); + auto q = find_unquoted(expr_new, '?'); while (q != std::string::npos) { // find corresponding colon - auto c1 = expr_new.find_first_of(':', q + 1); - auto q1 = expr_new.find_first_of('?', q + 1); + auto c1 = find_unquoted(expr_new, ':', q + 1); + auto q1 = find_unquoted(expr_new, '?', q + 1); while (q1 < c1 && q1 != std::string::npos && c1 != std::string::npos) { - q1 = expr_new.find_first_of('?', q1 + 1); - c1 = expr_new.find_first_of(':', c1 + 1); + q1 = find_unquoted(expr_new, '?', q1 + 1); + c1 = find_unquoted(expr_new, ':', c1 + 1); } if (c1 == std::string::npos) { return ""; // error: missing colon @@ -810,7 +828,7 @@ std::string compute(const std::string & expr) { } else { return ""; // error } - q = expr_new.find_first_of('?'); // search next instance + q = find_unquoted(expr_new, '?'); // search next instance } return calculate(expr_new); diff --git a/src/core/system.cpp b/src/core/system.cpp index d9bd8e328..8a89ed4b8 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -154,6 +154,26 @@ bool System::command_sendmail(const char * value, const int8_t) { : " (plain)", value); + // Resolve the message before opening the SMTP session: compute() can do entity lookups and a + // blocking {url} fetch, which would otherwise leave the connection idle long enough to time out. + // The value is either a plain body or a JSON envelope overriding the configured subject/to/from. + JsonDocument doc(PSRAM_DOC); + String body = value; + if (body.length()) { + auto error = deserializeJson(doc, (const char *)value); + if (!error && doc.is()) { + subject = doc["subject"] | subject; + recp = doc["to"] | recp; + sender = doc["from"] | sender; + body = doc["body"] | body; + } + } + // keep the original body if the calculator returns nothing, so unquoted literal text still gets sent + std::string computed_body = compute(body.c_str()); + if (!computed_body.empty()) { + body = computed_body.c_str(); + } + bool success = false; #ifndef EMSESP_STANDALONE @@ -213,18 +233,6 @@ bool System::command_sendmail(const char * value, const int8_t) { return false; } } - JsonDocument doc(PSRAM_DOC); - String body = value; - if (body.length()) { - auto error = deserializeJson(doc, (const char *)value); - if (!error && doc.as().size() >= 0) { - subject = doc["subject"] | subject; - recp = doc["to"] | recp; - sender = doc["from"] | sender; - body = doc["body"] | body; - } - } - SMTPMessage & msg = smtp->getMessage(); msg.headers.add(rfc822_subject, subject); msg.headers.add(rfc822_from, sender); @@ -234,12 +242,6 @@ bool System::command_sendmail(const char * value, const int8_t) { // msg.headers.addCustom("Importance", PRIORITY); // msg.headers.addCustom("X-MSMail-Priority", PRIORITY); // msg.headers.addCustom("X-Priority", PRIORITY_NUM); - // run the body through the Shunting Yard calculator (entity substitution, expressions, optional {url} fetch) - // keep the original body if the calculator returns nothing - std::string computed_body = compute(body.c_str()); - if (!computed_body.empty()) { - body = computed_body.c_str(); - } msg.text.body(body); // bodyText.replace("\r\n", "
\r\n"); diff --git a/src/web/WebCommandService.cpp b/src/web/WebCommandService.cpp index 35350599d..646a56764 100644 --- a/src/web/WebCommandService.cpp +++ b/src/web/WebCommandService.cpp @@ -100,6 +100,13 @@ bool WebCommandService::isUrlCommand(const std::string & command) { return lower_url.starts_with("http://") || lower_url.starts_with("https://"); } +// true if the command runs the shunting-yard on its own argument. Those values must be passed +// through raw: a first pass strips the quotes from literal text, so the command's own pass then +// re-parses that text as an expression and chokes on characters like '!' or drops the spaces. +bool WebCommandService::computesOwnValue(const std::string & cmd) { + return cmd == "system/message" || cmd == "system/sendmail"; +} + // true if a value expression contains an embedded {"url":...} JSON snippet, which compute() // will resolve with a blocking HTTP request. Mirrors the scan compute() does in shuntingYard.cpp bool WebCommandService::valueContainsUrl(const std::string & value) { @@ -239,13 +246,13 @@ bool WebCommandService::executeCommand(const char * name, const std::string & co // run the value through the shunting-yard calculator so expressions like "custom/heatcnt + 1" // are resolved (entity references replaced by their values, then computed). Plain values pass // through unchanged. Applies to both URL and internal commands, like the old scheduler code - // which computed the value before executing. system/message runs the shunting-yard on its own - // argument, so pre-computing it here would run it twice - pass it through raw. + // which computed the value before executing. Commands that compute their own argument are + // skipped here so the value is only ever evaluated once. std::string computed_data = data; - if (!data.empty() && cmd != "system/message") { + if (!data.empty() && !computesOwnValue(cmd)) { computed_data = compute(data); if (computed_data.empty()) { - EMSESP::logger().warning("Command '%s': cannot compute value '%s'", name, data.c_str()); + EMSESP::logger().warning("Command '%s': cannot compute value '%s'. Literal text must be quoted", name, data.c_str()); return false; } } diff --git a/src/web/WebCommandService.h b/src/web/WebCommandService.h index 41ef54f67..c4a8cefc0 100644 --- a/src/web/WebCommandService.h +++ b/src/web/WebCommandService.h @@ -85,6 +85,7 @@ class WebCommandService : public StatefulService { static bool isUrlCommand(const std::string & command); // true if the command definition is a HTTP/URL command static bool valueContainsUrl(const std::string & value); // true if a value embeds a {"url":...} compute() will fetch + static bool computesOwnValue(const std::string & cmd); // true if the command runs the shunting-yard itself const CommandItem * find(const char * name); diff --git a/test/test_api/test_shuntingYard.h b/test/test_api/test_shuntingYard.h index cce84bc6f..902368d40 100644 --- a/test/test_api/test_shuntingYard.h +++ b/test/test_api/test_shuntingYard.h @@ -120,6 +120,31 @@ void shuntingYard_test28() { run_shuntingYard_test("", "-x"); } +// quoted text keeps its spaces and punctuation, unquoted text does not +void shuntingYard_test29() { + run_shuntingYard_test("email test!", "\"email test!\""); +} + +// a trailing '!' on unquoted text is read as a logical NOT and has no valid operand +void shuntingYard_test30() { + run_shuntingYard_test("", "email test!"); +} + +// computing an already computed value is lossy - callers must run compute() only once +void shuntingYard_test31() { + run_shuntingYard_test("", emsesp::compute("\"email test!\"")); +} + +// a '?' inside quotes is literal text, not a ternary +void shuntingYard_test32() { + run_shuntingYard_test("Is it hot?", "\"Is it hot?\""); +} + +// mixing quoted text with entity values keeps the punctuation of both parts +void shuntingYard_test33() { + run_shuntingYard_test("temp is 40 C!", "\"temp is \" + boiler/flowtempoffset + \" C!\""); +} + void run_shuntingYard_tests() { RUN_TEST(shuntingYard_test1); RUN_TEST(shuntingYard_test2); @@ -149,4 +174,9 @@ void run_shuntingYard_tests() { RUN_TEST(shuntingYard_test26); RUN_TEST(shuntingYard_test27); RUN_TEST(shuntingYard_test28); + RUN_TEST(shuntingYard_test29); + RUN_TEST(shuntingYard_test30); + RUN_TEST(shuntingYard_test31); + RUN_TEST(shuntingYard_test32); + RUN_TEST(shuntingYard_test33); } From c765b5c9b6702993e08c334396087069baf72d9b Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Aug 2026 14:46:49 +0200 Subject: [PATCH 12/14] update --- CHANGELOG_LATEST.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index d2deaaf3f..f63cac508 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -16,8 +16,7 @@ This release is based on the latest Espressif/Arduino core version 3. It brings ## Fixed - shunting yard show json -- `system/sendmail` called from a Command or the Scheduler computed its value twice, which stripped the quotes from literal text and then failed on characters like `!` -- shunting yard treated a `?` inside a quoted string as a ternary, so text like `"too hot?"` returned nothing +- `system/sendmail` called from a Command or the Scheduler computed its value twice, which stripped the quotes from literal text and then failed on characters like `!`. Also shunting yard treated a `?` inside a quoted string as a ternary, so text like `"too hot?"` returned nothing - memory leak when using different timezones for ems-esp and thermostat[#3184](https://github.com/emsesp/EMS-ESP32/issues/3184) - LED stayed off on a healthy system when "Disable LED" was unchecked - Ethernet MAC address changed with the new SDK, breaking DHCP reservations (currently disabled) From c5e2ba024ca2945fe00aa8e416a4aab6743da88c Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Aug 2026 15:48:40 +0200 Subject: [PATCH 13/14] rename "'Command sent" --- interface/pnpm-lock.yaml | 16 ++++++++-------- interface/src/app/main/CommandsDialog.tsx | 2 +- interface/src/app/main/DevicesDialog.tsx | 2 +- interface/src/i18n/cs/index.ts | 2 +- interface/src/i18n/de/index.ts | 2 +- interface/src/i18n/en/index.ts | 2 +- interface/src/i18n/fr/index.ts | 2 +- interface/src/i18n/it/index.ts | 2 +- interface/src/i18n/nl/index.ts | 2 +- interface/src/i18n/no/index.ts | 2 +- interface/src/i18n/pl/index.ts | 2 +- interface/src/i18n/sk/index.ts | 2 +- interface/src/i18n/sv/index.ts | 2 +- interface/src/i18n/tr/index.ts | 2 +- 14 files changed, 21 insertions(+), 21 deletions(-) diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index caaf4c043..e19a0133d 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -2178,8 +2178,8 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - open@11.0.1: - resolution: {integrity: sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==} + open@11.0.2: + resolution: {integrity: sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==} engines: {node: '>=20'} optionator@0.9.4: @@ -2354,8 +2354,8 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} - powershell-utils@0.2.0: - resolution: {integrity: sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==} + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} engines: {node: '>=20'} preact@10.29.8: @@ -5145,13 +5145,13 @@ snapshots: dependencies: mimic-fn: 2.1.0 - open@11.0.1: + open@11.0.2: dependencies: default-browser: 5.5.1 define-lazy-prop: 3.0.0 is-in-ssh: 1.0.0 is-inside-container: 1.0.0 - powershell-utils: 0.2.0 + powershell-utils: 0.2.1 wsl-utils: 1.0.0 optionator@0.9.4: @@ -5297,7 +5297,7 @@ snapshots: powershell-utils@0.1.0: {} - powershell-utils@0.2.0: {} + powershell-utils@0.2.1: {} preact@10.29.8: {} @@ -5455,7 +5455,7 @@ snapshots: rollup-plugin-visualizer@7.1.1(rolldown@1.2.6): dependencies: - open: 11.0.1 + open: 11.0.2 picomatch: 4.0.7 source-map: 0.8.0 yargs: 18.1.0 diff --git a/interface/src/app/main/CommandsDialog.tsx b/interface/src/app/main/CommandsDialog.tsx index cd4c93faf..070a897df 100644 --- a/interface/src/app/main/CommandsDialog.tsx +++ b/interface/src/app/main/CommandsDialog.tsx @@ -95,7 +95,7 @@ const CommandsDialog = ({ { immediate: false } ) .onSuccess(() => { - toast.success(LL.COMMAND_SENT()); + toast.success(LL.COMMAND_EXECUTED()); }) .onError((error) => { toast.error(String(error.error?.message || 'An error occurred')); diff --git a/interface/src/app/main/DevicesDialog.tsx b/interface/src/app/main/DevicesDialog.tsx index 1feb0ee30..b4bc6cf47 100644 --- a/interface/src/app/main/DevicesDialog.tsx +++ b/interface/src/app/main/DevicesDialog.tsx @@ -70,7 +70,7 @@ const DevicesDialog = ({ { immediate: false } ) .onSuccess(() => { - toast.success(LL.COMMAND_SENT()); + toast.success(LL.COMMAND_EXECUTED()); }) .onError((error) => { toast.error(String(error.error?.message || 'An error occurred')); diff --git a/interface/src/i18n/cs/index.ts b/interface/src/i18n/cs/index.ts index ea7fb2ad7..f44f2ab4e 100644 --- a/interface/src/i18n/cs/index.ts +++ b/interface/src/i18n/cs/index.ts @@ -347,7 +347,7 @@ const cs: Translation = { SYSTEM_NAME: 'Název systému', COMMANDS: 'Příkazy', COMMANDS_HELP_1: 'Definujte vlastní příkazy pro magistrali EMS', - COMMAND_SENT: 'Příkaz odeslán', + COMMAND_EXECUTED: 'Příkaz úspěšně spuštěn', RUN: 'Spustit příkaz', INTERNET_CONNECTION_CHECK: 'Kontrola online pro dostupné verze' }; diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index 38c7ffe84..ea4388a81 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -347,7 +347,7 @@ const de: Translation = { SYSTEM_NAME: 'Systemname', COMMANDS: 'Befehle', COMMANDS_HELP_1: 'Definieren Sie eigene Befehle für die EMS-Magistral', - COMMAND_SENT: 'Befehl gesendet', + COMMAND_EXECUTED: 'Befehl erfolgreich ausgeführt', RUN: 'Befehl ausführen', INTERNET_CONNECTION_CHECK: 'Online-Prüfung auf verfügbare Versionen' }; diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index db133cba7..c1579693e 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -347,7 +347,7 @@ const en: Translation = { SYSTEM_NAME: 'System Name', COMMANDS: 'Commands', COMMANDS_HELP_1: 'Define reusable named commands below. These can be executed from the console, API/MQTT, or referenced by the Scheduler', - COMMAND_SENT: 'Command sent', + COMMAND_EXECUTED: 'Command executed successfully', RUN: 'Run', INTERNET_CONNECTION_CHECK: 'Checking online for available versions' }; diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index fbc02d274..42d32bba3 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -347,7 +347,7 @@ const fr: Translation = { SYSTEM_NAME: 'Nom du système', COMMANDS: 'Commandes', COMMANDS_HELP_1: 'Définir des commandes personnalisées pour la magistral EMS', - COMMAND_SENT: 'Commande envoyée', + COMMAND_EXECUTED: 'Commande exécutée avec succès', RUN: 'Exécuter une commande', INTERNET_CONNECTION_CHECK: 'Vérification en ligne pour les versions disponibles' }; diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index b7977cb6f..674781a89 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -347,7 +347,7 @@ const it: Translation = { SYSTEM_NAME: 'Nome del sistema', COMMANDS: 'Comandi', COMMANDS_HELP_1: 'Definisci comandi personalizzati per la magistrali EMS', - COMMAND_SENT: 'Comando inviato', + COMMAND_EXECUTED: 'Comando eseguito con successo', RUN: 'Esegui comando', INTERNET_CONNECTION_CHECK: 'Controllo online per le versioni disponibili' }; diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 4598d1545..9c33c29f7 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -347,7 +347,7 @@ const nl: Translation = { SYSTEM_NAME: 'Systeemnaam', COMMANDS: 'Commando\'s', COMMANDS_HELP_1: 'Definieer hergebruikbare benoemde commando\'s hieronder. Deze kunnen worden uitgevoerd vanuit de console, API/MQTT, of worden aangeroepen door de Scheduler', - COMMAND_SENT: 'Commando verzonden', + COMMAND_EXECUTED: 'Commando uitgevoerd succesvol', RUN: 'Commando uitvoeren', INTERNET_CONNECTION_CHECK: 'Controleren op online beschikbare versies' }; diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index b54932f8f..df3f028b8 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -347,7 +347,7 @@ const no: Translation = { SYSTEM_NAME: 'Systemnavn', COMMANDS: 'Kommandoer', COMMANDS_HELP_1: 'Definer egne kommandoer for EMS-Magistral', - COMMAND_SENT: 'Kommando sendt', + COMMAND_EXECUTED: 'Kommando kjørt lykkes', RUN: 'Kjør', INTERNET_CONNECTION_CHECK: 'Kontrollerer online for tilgjengelige versjoner' }; diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index 072da8799..6878f2918 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -347,7 +347,7 @@ const pl: BaseTranslation = { SYSTEM_NAME: 'Nazwa systemu', COMMANDS: 'Komendy', COMMANDS_HELP_1: 'Zdefiniuj niestandardowe komendy dla magistrali EMS', - COMMAND_SENT: 'Komenda wysłana', + COMMAND_EXECUTED: 'Komenda wykonana pomyślnie', RUN: 'Wykonaj', INTERNET_CONNECTION_CHECK: 'Sprawdzanie online dla dostępnych wersji' }; diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index 5d8f61623..4933a7954 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -347,7 +347,7 @@ const sk: Translation = { SYSTEM_NAME: 'Názov systému', COMMANDS: 'Príkazy', COMMANDS_HELP_1: 'Definujte vlastné príkazy pre magistrali EMS', - COMMAND_SENT: 'Príkaz odoslaný', + COMMAND_EXECUTED: 'Príkaz vykonaný úspešne', RUN: 'Volať príkaz', INTERNET_CONNECTION_CHECK: 'Kontrola online pre dostupné verzie' }; diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 0f0c35787..563d0f58f 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -347,7 +347,7 @@ const sv: Translation = { SYSTEM_NAME: 'Systemnamn', COMMANDS: 'Kommandon', COMMANDS_HELP_1: 'Definiera egna kommandon för EMS-Magistral', - COMMAND_SENT: 'Kommando skickat', + COMMAND_EXECUTED: 'Kommando kört lyckades', RUN: 'Kör kommando', INTERNET_CONNECTION_CHECK: 'Kontrollerar online för tillgängliga versioner' }; diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index c4164416a..080b409d6 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -347,7 +347,7 @@ const tr: Translation = { SYSTEM_NAME: 'Sistem Adı', COMMANDS: 'Komutlar', COMMANDS_HELP_1: 'Özel komutları EMS hattına tanımlayın', - COMMAND_SENT: 'Komut gönderildi', + COMMAND_EXECUTED: 'Komut başarıyla çalıştırıldı', RUN: 'Komutu çalıştır', INTERNET_CONNECTION_CHECK: 'Online bağlantısı kontrol ediliyor' }; From 518ac0b4b42439f402080c3ac03082f9aedf85a1 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Aug 2026 15:48:58 +0200 Subject: [PATCH 14/14] check TLS, add more error messages --- src/core/system.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index 8a89ed4b8..252c1af83 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -187,6 +187,7 @@ bool System::command_sendmail(const char * value, const int8_t) { const bool implicit_ssl = (security == EMAIL_SECURITY::SSL); const bool start_tls = (security == EMAIL_SECURITY::STARTTLS); + const bool tls_enabled = (security != EMAIL_SECURITY::NONE); if (security == EMAIL_SECURITY::NONE) { smtp = new SMTPClient(*basic_client); @@ -212,14 +213,13 @@ bool System::command_sendmail(const char * value, const int8_t) { delete basic_client; }; - // ssl defaults to true; Off and STARTTLS must start as plain SMTP - if (!smtp->connect(server, port, String(""), static_cast(nullptr), implicit_ssl)) { - LOG_ERROR("send email connection error"); + if (!smtp->connect(server, port, String(""), static_cast(nullptr), tls_enabled)) { + LOG_ERROR("send email connection error: %s", smtp->status().text.c_str()); cleanup(); return false; } if (!smtp->isConnected()) { - LOG_ERROR("send email connection error"); + LOG_ERROR("send email connection error: %s", smtp->status().text.c_str()); cleanup(); return false; } @@ -228,7 +228,7 @@ bool System::command_sendmail(const char * value, const int8_t) { if (!login.isEmpty()) { smtp->authenticate(login, pass, readymail_auth_password); if (!smtp->isAuthenticated()) { - LOG_ERROR("send email authentication error"); + LOG_ERROR("send email authentication error: %s", smtp->status().text.c_str()); cleanup(); return false; } @@ -255,6 +255,9 @@ bool System::command_sendmail(const char * value, const int8_t) { msg.timestamp = time(nullptr); success = smtp->send(msg); + if (!success) { + LOG_ERROR("send email failed: %s", smtp->status().text.c_str()); + } cleanup(); #endif