From d6e00c4534c8ec80dea86359834671276c0074aa Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 18 Apr 2026 18:50:08 +0200 Subject: [PATCH 01/10] UART_FIFO_LEN is deprecated --- src/uart/emsuart_esp32.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/uart/emsuart_esp32.cpp b/src/uart/emsuart_esp32.cpp index b14fef8f7..4ce8b405e 100644 --- a/src/uart/emsuart_esp32.cpp +++ b/src/uart/emsuart_esp32.cpp @@ -39,7 +39,7 @@ uint32_t inverse_mask = 0; // receive task, wait for break and call incoming_telegram void EMSuart::uart_event_task(void * pvParameters) { uart_event_t event; - uint8_t telegram[UART_FIFO_LEN + 1]; // same size as in driver_install + uint8_t telegram[UART_HW_FIFO_LEN(EMSUART_NUM) + 1]; // same size as in driver_install uint8_t length = 0; while (1) { @@ -92,7 +92,12 @@ void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t uart_param_config(EMSUART_NUM, &uart_config); uart_set_pin(EMSUART_NUM, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); uart_set_line_inverse(EMSUART_NUM, inverse_mask); - uart_driver_install(EMSUART_NUM, UART_FIFO_LEN + 1, 0, UART_FIFO_LEN + 3, &uart_queue, 0); // buffer must be > fifo, queue can hold data+break+overflow message + uart_driver_install(EMSUART_NUM, + UART_HW_FIFO_LEN(EMSUART_NUM) + 1, + 0, + UART_HW_FIFO_LEN(EMSUART_NUM) + 3, + &uart_queue, + 0); // buffer must be > fifo, queue can hold data+break+overflow message uart_set_rx_full_threshold(EMSUART_NUM, 1); uart_set_rx_timeout(EMSUART_NUM, 0); // disable From 86a20fc97a9396d310c472cdb6f67fb296547235 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 18 Apr 2026 18:54:33 +0200 Subject: [PATCH 02/10] sync with dev-16 --- CHANGELOG_LATEST.md | 2 + Makefile | 4 +- cspell.json | 3 +- interface/package.json | 2 +- interface/pnpm-lock.yaml | 111 +++++++++-------- interface/progmem-generator.js | 26 ++-- interface/vite.config.ts | 54 +++++++- lib/semver/LICENSE | 22 ---- lib/semver/README.md | 7 -- lib/semver/Semver200_comparator.cpp | 121 ------------------ lib/semver/Semver200_parser.cpp | 187 ---------------------------- lib/semver/semver200.h | 53 -------- lib/semver/version.h | 149 ---------------------- lib/semver/version.inl | 132 -------------------- platformio.ini | 8 +- src/ESP32React/ArduinoJsonJWT.cpp | 16 ++- src/ESP32React/ESP32React.cpp | 113 ++++++++++------- src/ESP32React/ESP32React.h | 1 + src/core/EMSESP_Version.h | 124 ++++++++++++++++++ src/core/emsfactory.h | 1 + src/core/system.cpp | 55 ++++---- src/web/WebStatusService.cpp | 14 +-- src/web/WebStatusService.h | 2 +- 23 files changed, 380 insertions(+), 827 deletions(-) delete mode 100644 lib/semver/LICENSE delete mode 100644 lib/semver/README.md delete mode 100644 lib/semver/Semver200_comparator.cpp delete mode 100644 lib/semver/Semver200_parser.cpp delete mode 100644 lib/semver/semver200.h delete mode 100644 lib/semver/version.h delete mode 100644 lib/semver/version.inl create mode 100644 src/core/EMSESP_Version.h diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index ab7e9aad6..3f9d14590 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -14,6 +14,7 @@ For more details go to [emsesp.org](https://emsesp.org/). - heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933) - e-mail notification using ReadyMail Client - 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991) +- full system backup and restore ## Fixed @@ -34,3 +35,4 @@ For more details go to [emsesp.org](https://emsesp.org/). - secure mqtt uses ESP_SSLClient - fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017) - move http client from stack to heap +- heap optimizations [#3021](https://github.com/emsesp/EMS-ESP32/discussions/3021) diff --git a/Makefile b/Makefile index 0728ef0f6..28c81c2f8 100644 --- a/Makefile +++ b/Makefile @@ -47,8 +47,8 @@ MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2))) #---------------------------------------------------------------------- TARGET := emsesp BUILD := build -SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton -INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src +SOURCES := src/core src/devices src/web src/test lib_standalone lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton +INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src LIBRARIES := CPPCHECK = cppcheck diff --git a/cspell.json b/cspell.json index 075f7629f..4d4f9520b 100644 --- a/cspell.json +++ b/cspell.json @@ -38,6 +38,7 @@ "vite.config.ts", "lib/esp32-psram/**", "test/test_api/test_api.h", - "lib_standalone/**" + "lib_standalone/**", + "**/*.js" ] } \ No newline at end of file diff --git a/interface/package.json b/interface/package.json index f2a1dea0b..a0327fb0e 100644 --- a/interface/package.json +++ b/interface/package.json @@ -57,7 +57,7 @@ "@types/react-dom": "^19.2.3", "axe-core": "^4.11.3", "concurrently": "^9.2.1", - "eslint": "^10.2.0", + "eslint": "^10.2.1", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.3", "rollup-plugin-visualizer": "^7.0.1", diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 9b6c0b7c5..174a07821 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: version: 7.29.0 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.2.0) + version: 10.0.1(eslint@10.2.1) '@preact/preset-vite': specifier: ^2.10.5 version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)) @@ -103,11 +103,11 @@ importers: specifier: ^9.2.1 version: 9.2.1 eslint: - specifier: ^10.2.0 - version: 10.2.0 + specifier: ^10.2.1 + version: 10.2.1 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.2.0) + version: 10.1.8(eslint@10.2.1) prettier: specifier: ^3.8.3 version: 3.8.3 @@ -119,7 +119,7 @@ importers: version: 5.46.1 typescript-eslint: specifier: ^8.58.2 - version: 8.58.2(eslint@10.2.0)(typescript@6.0.3) + version: 8.58.2(eslint@10.2.1)(typescript@6.0.3) vite: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1) @@ -498,12 +498,16 @@ packages: resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -1181,8 +1185,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.19: - resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1710,8 +1714,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.2.0: - resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2019,8 +2023,8 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} he@1.2.0: @@ -3693,9 +3697,9 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)': dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3716,9 +3720,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.2.0)': + '@eslint/js@10.0.1(eslint@10.2.1)': optionalDependencies: - eslint: 10.2.0 + eslint: 10.2.1 '@eslint/object-schema@3.0.5': {} @@ -3727,13 +3731,18 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': + '@humanfs/core@0.19.2': dependencies: - '@humanfs/core': 0.19.1 + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 '@humanwhocodes/retry': 0.4.3 + '@humanfs/types@0.15.0': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -4166,15 +4175,15 @@ snapshots: dependencies: '@types/node': 25.6.0 - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 10.2.0 + eslint: 10.2.1 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -4182,14 +4191,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/types': 8.58.2 '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4212,13 +4221,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.58.2(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.58.2 '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -4241,13 +4250,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.58.2(eslint@10.2.1)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/types': 8.58.2 '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - eslint: 10.2.0 + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4325,7 +4334,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.19: {} + baseline-browser-mapping@2.10.20: {} bin-build@3.0.0: dependencies: @@ -4386,7 +4395,7 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.19 + baseline-browser-mapping: 2.10.20 caniuse-lite: 1.0.30001788 electron-to-chromium: 1.5.340 node-releases: 2.0.37 @@ -4898,9 +4907,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.2.0): + eslint-config-prettier@10.1.8(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-scope@9.1.2: dependencies: @@ -4913,15 +4922,15 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.2.0: + eslint@10.2.1: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.5.5 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.1 - '@humanfs/node': 0.16.7 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -5169,7 +5178,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.3 math-intrinsics: 1.1.0 get-proto@1.0.1: @@ -5302,7 +5311,7 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -5412,7 +5421,7 @@ snapshots: is-core-module@2.16.1: dependencies: - hasown: 2.0.2 + hasown: 2.0.3 is-cwebp-readable@3.0.0: dependencies: @@ -6390,13 +6399,13 @@ snapshots: dependencies: typescript: 6.0.3 - typescript-eslint@8.58.2(eslint@10.2.0)(typescript@6.0.3): + typescript-eslint@8.58.2(eslint@10.2.1)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - eslint: 10.2.0 + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color diff --git a/interface/progmem-generator.js b/interface/progmem-generator.js index 30fc00094..be5cead21 100644 --- a/interface/progmem-generator.js +++ b/interface/progmem-generator.js @@ -24,20 +24,26 @@ let bundleStats = { other: { count: 0, uncompressed: 0, compressed: 0 } }; -const generateWWWClass = - () => `typedef std::function RouteRegistrationHandler; -// Bundle Statistics: +// AsyncWebHandler that performs the lookup. +const generateWWWClass = () => `// Bundle Statistics: // - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB // - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB // - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}% // - Generated on: ${new Date().toISOString()} -class WWWData { -${INDENT}public: -${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) { -${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, ${f.hash});`).join('\n')} -${INDENT.repeat(2)}} +struct WWWAsset { +${INDENT}const char * uri; +${INDENT}const char * contentType; +${INDENT}const uint8_t * content; +${INDENT}size_t len; +${INDENT}const char * etag; // already includes enclosing double quotes }; + +static const WWWAsset WWW_ASSETS[] = { +${fileInfo.map((f) => `${INDENT}{"${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "\\"${f.rawHash}\\""},`).join('\n')} +}; + +static constexpr size_t WWW_ASSETS_COUNT = sizeof(WWW_ASSETS) / sizeof(WWW_ASSETS[0]); `; const getFilesSync = (dir, files = []) => { @@ -72,6 +78,7 @@ const writeFile = (relativeFilePath, buffer) => { const zipBuffer = zlib.gzipSync(buffer, { level: 9 }); // const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex'); const hash = etag(zipBuffer); // use smaller md5 instead of sha256 + const rawHash = hash.replace(/^"|"$/g, ''); zipBuffer.forEach((b) => { if (!(size % bytesPerLine)) { @@ -94,7 +101,8 @@ const writeFile = (relativeFilePath, buffer) => { mimeType, variable, size, - hash + hash, + rawHash }); totalSize += size; diff --git a/interface/vite.config.ts b/interface/vite.config.ts index 0c16f6a7b..5c288677c 100644 --- a/interface/vite.config.ts +++ b/interface/vite.config.ts @@ -166,6 +166,51 @@ const createManualChunks = (detailed = false) => { }; }; +// Rolldown-native codeSplitting groups for the production build. +// See: https://rolldown.rs/reference/outputoptions.codesplitting +const createRolldownCodeSplitting = () => ({ + groups: [ + { name: '@preact', test: /[\\/]node_modules[\\/].*preact/, priority: 100 }, + { + name: '@react-router', + test: /[\\/]node_modules[\\/].*react-router/, + priority: 95 + }, + { + name: '@mui-material', + test: /[\\/]node_modules[\\/]@mui[\\/]material/, + priority: 95 + }, + { + name: '@mui-icons', + test: /[\\/]node_modules[\\/]@mui[\\/]icons-material/, + priority: 95 + }, + { name: '@alova', test: /[\\/]node_modules[\\/].*alova/, priority: 90 }, + { name: '@i18n', test: /[\\/]node_modules[\\/].*typesafe-i18n/, priority: 90 }, + { + name: '@toastify', + test: /[\\/]node_modules[\\/].*react-toastify/, + priority: 90 + }, + { + name: '@table-library', + test: /[\\/]node_modules[\\/]@table-library/, + priority: 90 + }, + { name: 'vendor', test: /[\\/]node_modules[\\/]/, priority: 10 }, + // Collapse the lazy-loaded route stubs + shared app/components/utils + // code into one chunk. This cuts firmware-side route count roughly in + // half and speeds up route-matching on every incoming HTTP request. + { + name: 'app', + test: /[\\/](app|components|utils)[\\/]/, + priority: 5 + }, + { name: 'api', test: /[\\/]api[\\/]/, priority: 5 } + ] +}); + // Common build base configuration const createBaseBuildConfig = () => ({ target: ES_TARGET, @@ -330,10 +375,17 @@ export default defineConfig( chunkFileNames: 'assets/[name]-[hash].js', entryFileNames: 'assets/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash].[ext]', + // Kept as a no-op for documentation/fallback. With Vite 8 manualChunks: createManualChunks(true), sourcemap: false } - } + }, + rolldownOptions: { + output: { + codeSplitting: createRolldownCodeSplitting() + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any } }; } diff --git a/lib/semver/LICENSE b/lib/semver/LICENSE deleted file mode 100644 index 53234651b..000000000 --- a/lib/semver/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Marko Živanović - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/lib/semver/README.md b/lib/semver/README.md deleted file mode 100644 index 82c86d7d3..000000000 --- a/lib/semver/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# About - -This project is MIT-licensed, C++14 implementation of [semantic versioning](http://semver.org) parser and comparator with support for modifying parsed version strings. Semantic versioning 2.0.0 specification is supported out-of-the-box and the code should be flexible-enough to support future revisions or other similar versioning schemes. - -Copyright (c) 2015 Marko Zivanovic - -Based on https://github.com/zmarko/semver diff --git a/lib/semver/Semver200_comparator.cpp b/lib/semver/Semver200_comparator.cpp deleted file mode 100644 index 1430bd14b..000000000 --- a/lib/semver/Semver200_comparator.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#include -#include -#include -#include "semver200.h" - -using namespace std; - -namespace version { - -namespace { - -// Compare normal version identifiers. -int compare_normal(const Version_data & l, const Version_data & r) { - if (l.major > r.major) - return 1; - if (l.major < r.major) - return -1; - if (l.minor > r.minor) - return 1; - if (l.minor < r.minor) - return -1; - if (l.patch > r.patch) - return 1; - if (l.patch < r.patch) - return -1; - return 0; -} - -// Compare alphanumeric prerelease identifiers. -inline int cmp_alnum_prerel_ids(const string & l, const string & r) { - auto cmp = l.compare(r); - if (cmp == 0) { - return cmp; - } else { - return cmp > 0 ? 1 : -1; - } -} - -// Compare numeric prerelease identifiers. -inline int cmp_num_prerel_ids(const string & l, const string & r) { - long long li = stoll(l); - long long ri = stoll(r); - if (li == ri) - return 0; - return li > ri ? 1 : -1; -} - -using Prerel_type_pair = pair; -using Prerel_id_comparator = function; -const map comparators = {{{Id_type::alnum, Id_type::alnum}, cmp_alnum_prerel_ids}, - {{Id_type::alnum, Id_type::num}, [](const string &, const string &) { return 1; }}, - {{Id_type::num, Id_type::alnum}, [](const string &, const string &) { return -1; }}, - {{Id_type::num, Id_type::num}, cmp_num_prerel_ids}}; - -// Compare prerelease identifiers based on their types. -inline int compare_prerel_identifiers(const Prerelease_identifier & l, const Prerelease_identifier & r) { - auto cmp = comparators.at({l.second, r.second}); - return cmp(l.first, r.first); -} - -inline int cmp_rel_prerel(const Prerelease_identifiers & l, const Prerelease_identifiers & r) { - if (l.empty() && !r.empty()) - return 1; - if (r.empty() && !l.empty()) - return -1; - return 0; -} -} // namespace - -int Semver200_comparator::compare(const Version_data & l, const Version_data & r) const { - // Compare normal version components. - int cmp = compare_normal(l, r); - if (cmp != 0) - return cmp; - - // Compare if one version is release and the other prerelease - release is always higher. - cmp = cmp_rel_prerel(l.prerelease_ids, r.prerelease_ids); - if (cmp != 0) - return cmp; - - // Compare prerelease by looking at each identifier: numeric ones are compared as numbers, - // alphanum as ASCII strings. - auto shorter = min(l.prerelease_ids.size(), r.prerelease_ids.size()); - for (size_t i = 0; i < shorter; i++) { - cmp = compare_prerel_identifiers(l.prerelease_ids[i], r.prerelease_ids[i]); - if (cmp != 0) - return cmp; - } - - // Prerelease identifiers are the same, to the length of the shorter version string; - // if they are the same length, then versions are equal, otherwise, longer one wins. - if (l.prerelease_ids.size() == r.prerelease_ids.size()) - return 0; - return l.prerelease_ids.size() > r.prerelease_ids.size() ? 1 : -1; -} - -} // namespace version diff --git a/lib/semver/Semver200_parser.cpp b/lib/semver/Semver200_parser.cpp deleted file mode 100644 index e5956be0e..000000000 --- a/lib/semver/Semver200_parser.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#include -#include -#include "semver200.h" - -using namespace std; - -namespace version { - -namespace { -enum class Parser_state { major, minor, patch, prerelease, build }; - -using Validator = function; -using State_transition_hook = function; -/// State transition is described by a character that triggers it, a state to transition to and -/// optional hook to be invoked on transition. -using Transition = tuple; -using Transitions = vector; -using State = tuple; -using State_machine = std::map; - -// Ranges of characters allowed in prerelease and build identifiers. -const vector> allowed_prerel_id_chars = {{'0', '9'}, {'A', 'Z'}, {'a', 'z'}, {'-', '-'}}; - -inline Transition mkx(const char c, Parser_state p, State_transition_hook pth) { - return make_tuple(c, p, pth); -} - -inline void Parse_error(const std::string & s) { - // EMSESP::logger().err("parse error: %s", s.c_str()); -} - -/// Advance parser state machine by a single step. -/** - Perform single step of parser state machine: if character matches one from transition tables - - trigger transition to next state; otherwise, validate if current token is in legal state - (throw Parse_error if not) and then add character to current token; State transition includes - preparing various vars for next state and invoking state transition hook (if specified) which is - where whole tokens are validated. - */ -inline void process_char(const char c, Parser_state & cstate, Parser_state & pstate, const Transitions & transitions, string & target, Validator validate) { - for (const auto & transition : transitions) { - if (c == get<0>(transition)) { - if (get<2>(transition)) - get<2>(transition)(target); - pstate = cstate; - cstate = get<1>(transition); - return; - } - } - validate(target, c); - target.push_back(c); -} - -/// Validate normal (major, minor, patch) version components. -inline void normal_version_validator(const string & tgt, const char c) { - if (c < '0' || c > '9') - Parse_error("invalid character encountered: " + string(1, c)); - if (tgt.compare(0, 1, "0") == 0) - Parse_error("leading 0 not allowed"); -} - -/// Validate that prerelease and build version identifiers are comprised of allowed chars only. -inline void prerelease_version_validator(const string &, const char c) { - bool res = false; - for (const auto & r : allowed_prerel_id_chars) { - res |= (c >= r.first && c <= r.second); - } - if (!res) - Parse_error("invalid character encountered: " + string(1, c)); -} - -inline bool is_identifier_numeric(const string & id) { - return id.find_first_not_of("0123456789") == string::npos; -} - -inline bool check_for_leading_0(const string & str) { - return str.length() > 1 && str[0] == '0'; -} - -/// Validate every individual prerelease identifier, determine it's type and add it to collection. -void prerelease_hook_impl(string & id, Prerelease_identifiers & prerelease) { - if (id.empty()) - Parse_error("version identifier cannot be empty"); - Id_type t = Id_type::alnum; - if (is_identifier_numeric(id)) { - t = Id_type::num; - if (check_for_leading_0(id)) { - Parse_error("numeric identifiers cannot have leading 0"); - } - } - prerelease.push_back(Prerelease_identifier(id, t)); - id.clear(); -} - -/// Validate every individual build identifier and add it to collection. -void build_hook_impl(string & id, Parser_state & pstate, Build_identifiers & build, std::string & prerelease_id, Prerelease_identifiers & prerelease) { - // process last token left from parsing prerelease data - if (pstate == Parser_state::prerelease) - prerelease_hook_impl(prerelease_id, prerelease); - if (id.empty()) - Parse_error("version identifier cannot be empty"); - build.push_back(id); - id.clear(); -} - -} // namespace - -/// Parse semver 2.0.0-compatible string to Version_data structure. -/** - Version text parser is implemented as a state machine. In each step one successive character from version - string is consumed and is either added to current token or triggers state transition. Hooks can be - injected into state transitions for validation/customization purposes. - */ -Version_data Semver200_parser::parse(const string & s) const { - string major; - string minor; - string patch; - string prerelease_id; - string build_id; - Prerelease_identifiers prerelease; - Build_identifiers build; - Parser_state cstate{Parser_state::major}; - Parser_state pstate; - - auto prerelease_hook = [&](string & id) { prerelease_hook_impl(id, prerelease); }; - - auto build_hook = [&](string & id) { build_hook_impl(id, pstate, build, prerelease_id, prerelease); }; - - // State transition tables - auto major_trans = {mkx('.', Parser_state::minor, {})}; - auto minor_trans = {mkx('.', Parser_state::patch, {})}; - auto patch_trans = {mkx('-', Parser_state::prerelease, {}), mkx('+', Parser_state::build, {})}; - auto prerelease_trans = {// When identifier separator (.) is found, stay in the same state but invoke hook - // in order to process each individual identifier separately. - mkx('.', Parser_state::prerelease, prerelease_hook), - mkx('+', Parser_state::build, {})}; - auto build_trans = {// Same stay-in-the-same-state-but-invoke-hook trick from above. - mkx('.', Parser_state::build, build_hook)}; - - State_machine state_machine = {{Parser_state::major, State{major_trans, major, normal_version_validator}}, - {Parser_state::minor, State{minor_trans, minor, normal_version_validator}}, - {Parser_state::patch, State{patch_trans, patch, normal_version_validator}}, - {Parser_state::prerelease, State{prerelease_trans, prerelease_id, prerelease_version_validator}}, - {Parser_state::build, State{build_trans, build_id, prerelease_version_validator}}}; - - // Main loop. - for (const auto & c : s) { - auto state = state_machine.at(cstate); - process_char(c, cstate, pstate, get<0>(state), get<1>(state), get<2>(state)); - } - - // Trigger appropriate hooks in order to process last token, because no state transition was - // triggered for it. - if (cstate == Parser_state::prerelease) { - prerelease_hook(prerelease_id); - } else if (cstate == Parser_state::build) { - build_hook(build_id); - } - - return Version_data{stoi(major), stoi(minor), stoi(patch), prerelease, build}; -} - -} // namespace version diff --git a/lib/semver/semver200.h b/lib/semver/semver200.h deleted file mode 100644 index 5c19f07ff..000000000 --- a/lib/semver/semver200.h +++ /dev/null @@ -1,53 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#pragma once - -#include "version.h" - -namespace version { - -/// Parse string into Version_data structure according to semantic versioning 2.0.0 rules. -struct Semver200_parser { - Version_data parse(const std::string &) const; -}; - -/// Compare Version_data to another using semantic versioning 2.0.0 rules. -struct Semver200_comparator { - int compare(const Version_data &, const Version_data &) const; -}; - -/// Concrete version class that binds all semver 2.0.0 functionality together. -class Semver200_version : public Basic_version { - public: - Semver200_version() - : Basic_version{Semver200_parser(), Semver200_comparator()} { - } - - Semver200_version(const std::string & v) - : Basic_version{v, Semver200_parser(), Semver200_comparator()} { - } -}; - -} // namespace version \ No newline at end of file diff --git a/lib/semver/version.h b/lib/semver/version.h deleted file mode 100644 index 517d6f435..000000000 --- a/lib/semver/version.h +++ /dev/null @@ -1,149 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#pragma once - -#include -#include - -namespace version { - -/// Type of prerelease identifier: alphanumeric or numeric. -/** - Type of identifier affects comparison: alphanumeric identifiers are compared as ASCII strings, while - numeric identifiers are compared as numbers. - */ -enum class Id_type { - alnum, ///< Identifier is alphanumerical - num ///< Identifier is numeric -}; - -/// Container for prerelease identifier value and it's type. -/** - Prerelease version string consist of an optional series of dot-separated identifiers. - These identifiers can be either numerical or alphanumerical. - This structure describes one such identifier. - */ -using Prerelease_identifier = std::pair; - -/// Container for all prerelease identifiers for a given version string. -using Prerelease_identifiers = std::vector; - -/// Build identifier is arbitrary string with no special meaning with regards to version precedence. -using Build_identifier = std::string; - -/// Container for all build identifiers of a given version string. -using Build_identifiers = std::vector; - -/// Description of version broken into parts, as per semantic versioning specification. -struct Version_data { - Version_data(const int M, const int m, const int p, const Prerelease_identifiers & pr, const Build_identifiers & b) - : major{M} - , minor{m} - , patch{p} - , prerelease_ids{pr} - , build_ids{b} { - } - - int major; ///< Major version, change only on incompatible API modifications. - int minor; ///< Minor version, change on backwards-compatible API modifications. - int patch; ///< Patch version, change only on bugfixes. - - /// Optional series of prerelease identifiers. - Prerelease_identifiers prerelease_ids; - - /// Optional series of build identifiers. - Build_identifiers build_ids; -}; - -// Forward declaration required for operators' template declarations. -template -class Basic_version; - -/// Test if left-hand version operand is of lower precedence than the right-hand version. -template -bool operator<(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand if of equal precedence as the right-hand version. -template -bool operator==(const Basic_version &, const Basic_version &); - -/// Test if left-hand version and right-hand version are of different precedence. -template -bool operator!=(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand is of higher precedence than the right-hand version. -template -bool operator>(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand is of higher or equal precedence as the right-hand version. -template -bool operator>=(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand is of lower or equal precedence as the right-hand version. -template -bool operator<=(const Basic_version &, const Basic_version &); - -/// Base class for various version parsing, precedence ordering and data manipulation schemes. -/** - Basic_version class describes general version object without prescribing parsing, - validation, comparison and modification rules. These rules are implemented by supplied Parser, Comparator - and Modifier objects. - */ -template -class Basic_version { - public: - /// Construct Basic_version object using Parser object to parse default ("0.0.0") version string, Comparator for comparison and Modifier for modification. - Basic_version(Parser, Comparator); - - /// Construct Basic_version object using Parser to parse supplied version string, Comparator for comparison and Modifier for modification. - Basic_version(const std::string &, Parser, Comparator); - - /// Construct Basic_version object using supplied Version_data, Parser, Comparator and Modifier objects. - Basic_version(const Version_data &, Parser, Comparator); - - /// Construct Basic_version by copying data from another one. - Basic_version(const Basic_version &); - - /// Copy version data from another Basic_version to this one. - Basic_version & operator=(const Basic_version &); - - int major() const; ///< Get major version. - int minor() const; ///< Get minor version. - int patch() const; ///< Get patch version. - const std::string prerelease() const; ///< Get prerelease version string. - const std::string build() const; ///< Get build version string. - - friend bool operator< <>(const Basic_version &, const Basic_version &); - friend bool operator==<>(const Basic_version &, const Basic_version &); - - private: - Parser parser_; - Comparator comparator_; - Version_data ver_; -}; - -} // namespace version - -#include "version.inl" diff --git a/lib/semver/version.inl b/lib/semver/version.inl deleted file mode 100644 index 8de18af61..000000000 --- a/lib/semver/version.inl +++ /dev/null @@ -1,132 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#pragma once - -#include "version.h" - -namespace version { - -namespace { - -/// Utility function to splice all vector elements to output stream, using designated separator -/// between elements and function object for getting values from vector elements. -template -std::string & splice(std::string & ss, const std::vector & vec, const std::string & sep, F read) { - if (!vec.empty()) { - for (auto it = vec.cbegin(); it < vec.cend() - 1; ++it) { - ss += read(*it) + sep; - } - ss += read(*vec.crbegin()); - } - return ss; -} - -} // namespace - -template -Basic_version::Basic_version(Parser p, Comparator c) - : parser_(p) - , comparator_(c) - , ver_(parser_.parse("0.0.0")) { -} - -template -Basic_version::Basic_version(const std::string & v, Parser p, Comparator c) - : parser_(p) - , comparator_(c) - , ver_(parser_.parse(v)) { -} - -template -Basic_version::Basic_version(const Version_data & v, Parser p, Comparator c) - : parser_(p) - , comparator_(c) - , ver_(v) { -} - -template -Basic_version::Basic_version(const Basic_version &) = default; - -template -Basic_version & Basic_version::operator=(const Basic_version &) = default; - -template -int Basic_version::major() const { - return ver_.major; -} - -template -int Basic_version::minor() const { - return ver_.minor; -} - -template -int Basic_version::patch() const { - return ver_.patch; -} - -template -const std::string Basic_version::prerelease() const { - std::string ss; - return splice(ss, ver_.prerelease_ids, ".", [](const Prerelease_identifier & id) { return id.first; }); -} - -template -const std::string Basic_version::build() const { - std::string ss; - return splice(ss, ver_.build_ids, ".", [](const std::string & id) { return id; }); -} - -template -bool operator<(const Basic_version & l, const Basic_version & r) { - return l.comparator_.compare(l.ver_, r.ver_) == -1; -} - -template -bool operator==(const Basic_version & l, const Basic_version & r) { - return l.comparator_.compare(l.ver_, r.ver_) == 0; -} - -template -inline bool operator!=(const Basic_version & l, const Basic_version & r) { - return !(l == r); -} - -template -inline bool operator>(const Basic_version & l, const Basic_version & r) { - return r < l; -} - -template -inline bool operator>=(const Basic_version & l, const Basic_version & r) { - return !(l < r); -} - -template -inline bool operator<=(const Basic_version & l, const Basic_version & r) { - return !(l > r); -} - -} // namespace version diff --git a/platformio.ini b/platformio.ini index 1b2348bca..917051208 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,8 +21,8 @@ extra_configs = pio_local.ini [common] -core_build_flags = -std=c++17 -std=gnu++17 -O3 -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers -core_unbuild_flags = -std=gnu++11 +core_build_flags = -std=gnu++20 -O3 -flto=auto -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers +core_unbuild_flags = -std=gnu++11 -std=gnu++14 -std=gnu++17 -fno-lto my_build_flags = @@ -169,7 +169,7 @@ build_src_flags = -DEMSESP_STANDALONE -DEMSESP_TEST -DARDUINOJSON_ENABLE_ARDUINO_STRING=1 -DNO_TLS_SUPPORT - -std=gnu++17 -Og -ggdb + -std=gnu++20 -Og -ggdb -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare @@ -210,7 +210,7 @@ build_src_flags = -DARDUINOJSON_ENABLE_ARDUINO_STRING=1 -DNO_TLS_SUPPORT -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" - -std=gnu++17 -Og -ggdb + -std=gnu++20 -Og -ggdb -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare diff --git a/src/ESP32React/ArduinoJsonJWT.cpp b/src/ESP32React/ArduinoJsonJWT.cpp index bbc2b11ac..7e7676af5 100644 --- a/src/ESP32React/ArduinoJsonJWT.cpp +++ b/src/ESP32React/ArduinoJsonJWT.cpp @@ -70,12 +70,16 @@ void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument & jsonDocument) { */ String ArduinoJsonJWT::sign(String & payload) { std::array hmacResult{}; - mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), - reinterpret_cast(_secret.c_str()), - _secret.length(), - reinterpret_cast(payload.c_str()), - payload.length(), - hmacResult.data()); + { + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + mbedtls_md_hmac_starts(&ctx, reinterpret_cast(_secret.c_str()), _secret.length()); + mbedtls_md_hmac_update(&ctx, reinterpret_cast(payload.c_str()), payload.length()); + mbedtls_md_hmac_finish(&ctx, hmacResult.data()); + mbedtls_md_free(&ctx); + } return encode(reinterpret_cast(hmacResult.data()), hmacResult.size()); } diff --git a/src/ESP32React/ESP32React.cpp b/src/ESP32React/ESP32React.cpp index 9b533dfba..024dcd1ff 100644 --- a/src/ESP32React/ESP32React.cpp +++ b/src/ESP32React/ESP32React.cpp @@ -2,10 +2,75 @@ #include "WWWData.h" // include auto-generated static web resources +#include + static constexpr const char CACHE_CONTROL[] = "public,max-age=60"; +// Single static-content handler serving all assets embedded in WWWData.h. +class StaticContentHandler : public AsyncWebHandler { + public: + bool canHandle(AsyncWebServerRequest * request) const override { + const auto method = request->method(); + return method == HTTP_GET || method == HTTP_HEAD || method == HTTP_OPTIONS; + } + + void handleRequest(AsyncWebServerRequest * request) override { + // OPTIONS is handled generically - the server-level CORS headers are + // attached via DefaultHeaders in ESP32React::begin(). + if (request->method() == HTTP_OPTIONS) { + request->send(200); + return; + } + + const char * url = request->url().c_str(); + const WWWAsset * found = lookup(url); + const WWWAsset * asset = found ? found : index_asset(); + + if (asset == nullptr) { + request->send(404); + return; + } + + // If the client already has this exact ETag, respond 304 Not Modified without sending the body. + const String & inm = request->header(asyncsrv::T_INM); + if (inm.length() != 0 && strcmp(inm.c_str(), asset->etag) == 0) { + request->send(304); + return; + } + + AsyncWebServerResponse * response = request->beginResponse(200, asset->contentType, asset->content, asset->len); + response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false); + response->addHeader(asyncsrv::T_ETag, asset->etag, false); + response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false); + request->send(response); + } + + private: + // Exact-match lookup in the asset table + static const WWWAsset * lookup(const char * url) { + for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) { + if (strcmp(WWW_ASSETS[i].uri, url) == 0) { + return &WWW_ASSETS[i]; + } + } + return nullptr; + } + + // Returns the /index.html asset, used as the SPA fallback for any GET + // that didn't match an embedded asset (React Router handles routing on + // the client side). + static const WWWAsset * index_asset() { + static const WWWAsset * cached = nullptr; + if (cached == nullptr) { + cached = lookup("/index.html"); + } + return cached; + } +}; + ESP32React::ESP32React(AsyncWebServer * server, FS * fs) - : _securitySettingsService(server, fs) + : _server(server) + , _securitySettingsService(server, fs) , _networkSettingsService(server, fs, &_securitySettingsService) , _wifiScanner(server, &_securitySettingsService) , _networkStatus(server, &_securitySettingsService) @@ -17,50 +82,6 @@ ESP32React::ESP32React(AsyncWebServer * server, FS * fs) , _mqttSettingsService(server, fs, &_securitySettingsService) , _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService) , _authenticationService(server, &_securitySettingsService) { - // - // Serve static web resources - // - - ArRequestHandlerFunction indexHtmlHandler = nullptr; - - WWWData::registerRoutes([server, &indexHtmlHandler](const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash) { - String etag = "\"" + hash + "\""; // RFC9110: ETag must be enclosed in double quotes - - ArRequestHandlerFunction requestHandler = [contentType, content, len, etag](AsyncWebServerRequest * request) { - if (request->header(asyncsrv::T_INM) == etag) { - request->send(304); - return; - } - - AsyncWebServerResponse * response = request->beginResponse(200, contentType, content, len); - response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false); - response->addHeader(asyncsrv::T_ETag, etag, false); - response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false); - request->send(response); - }; - - server->on(uri, HTTP_GET, requestHandler); - - // Capture index.html handler to set onNotFound once after all routes are registered - if (strcmp(uri, "/index.html") == 0) { - indexHtmlHandler = requestHandler; - } - }); - - // Set onNotFound handler once after all routes are registered - // Serving non matching get requests with "/index.html" - // OPTIONS get a straight up 200 response - if (indexHtmlHandler != nullptr) { - server->onNotFound([indexHtmlHandler](AsyncWebServerRequest * request) { - if (request->method() == HTTP_GET) { - indexHtmlHandler(request); - } else if (request->method() == HTTP_OPTIONS) { - request->send(200); - } else { - request->send(404); // not found - } - }); - } } void ESP32React::begin() { @@ -78,11 +99,11 @@ void ESP32React::begin() { _ntpSettingsService.begin(); _mqttSettingsService.begin(); _securitySettingsService.begin(); + _server->addHandler(new StaticContentHandler()); } void ESP32React::loop() { _networkSettingsService.loop(); _apSettingsService.loop(); _mqttSettingsService.loop(); - _ntpSettingsService.loop(); } \ No newline at end of file diff --git a/src/ESP32React/ESP32React.h b/src/ESP32React/ESP32React.h index f5af50a8a..cbcea487e 100644 --- a/src/ESP32React/ESP32React.h +++ b/src/ESP32React/ESP32React.h @@ -68,6 +68,7 @@ class ESP32React { } private: + AsyncWebServer * _server; SecuritySettingsService _securitySettingsService; NetworkSettingsService _networkSettingsService; WiFiScanner _wifiScanner; diff --git a/src/core/EMSESP_Version.h b/src/core/EMSESP_Version.h new file mode 100644 index 000000000..c9be68f61 --- /dev/null +++ b/src/core/EMSESP_Version.h @@ -0,0 +1,124 @@ +/* + * 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_Version_H +#define EMSESP_Version_H + +#include +#include +#include + +// Drop-in lightweight replacement for the subset of the semver library actually used by EMS-ESP. +// The previous semver library (lib/semver) builds a std::map + std::function-based state machine on +// every parse, which fragments the internal heap on the ESP32. This replacement does no heap +// allocation beyond the std::string member for the prerelease tag, and matches the API surface +// we consume: construction from string, major()/minor()/patch()/prerelease(), and operator>/(const EMSESP_Version & a, const EMSESP_Version & b) { + return b < a; + } + + friend bool operator==(const EMSESP_Version & a, const EMSESP_Version & b) { + return a.major_ == b.major_ && a.minor_ == b.minor_ && a.patch_ == b.patch_; + } + + friend bool operator!=(const EMSESP_Version & a, const EMSESP_Version & b) { + return !(a == b); + } + + friend bool operator>=(const EMSESP_Version & a, const EMSESP_Version & b) { + return !(a < b); + } + + friend bool operator<=(const EMSESP_Version & a, const EMSESP_Version & b) { + return !(b < a); + } + + private: + int major_ = 0; + int minor_ = 0; + int patch_ = 0; + std::string prerelease_; + + void parse(const char * s) { + major_ = minor_ = patch_ = 0; + prerelease_.clear(); + if (s == nullptr || *s == '\0') { + return; + } + // parse numeric major.minor.patch; accept partial ("3", "3.9", "3.9.0") + sscanf(s, "%d.%d.%d", &major_, &minor_, &patch_); + // capture prerelease tag after '-' if present (stop at '+' which is build metadata) + const char * dash = strchr(s, '-'); + if (dash != nullptr) { + const char * plus = strchr(dash, '+'); + if (plus != nullptr) { + prerelease_.assign(dash + 1, plus - dash - 1); + } else { + prerelease_.assign(dash + 1); + } + } + } +}; + +} // namespace version + +#endif diff --git a/src/core/emsfactory.h b/src/core/emsfactory.h index 771aad4af..3be963be0 100644 --- a/src/core/emsfactory.h +++ b/src/core/emsfactory.h @@ -19,6 +19,7 @@ #ifndef EMSESP_EMSFACTORY_H_ #define EMSESP_EMSFACTORY_H_ +#include #include // for unique_ptr #include diff --git a/src/core/system.cpp b/src/core/system.cpp index 82fba4144..d1d39487c 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -26,13 +26,12 @@ #include #include "esp_efuse.h" #include -#include #endif #include #include -#include +#include "EMSESP_Version.h" #if defined(EMSESP_TEST) #include "../test/test.h" @@ -457,15 +456,16 @@ void System::get_partition_info() { strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); p_info.install_date = d > 1500000000L ? time_string : ""; - esp_image_metadata_t meta = {}; - esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; - if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { - p_info.size = meta.image_len / 1024; // actual firmware size in KB - } else { - p_info.size = 0; + if (!p_info.version.empty()) { + esp_image_metadata_t meta = {}; + esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; + if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { + p_info.size = meta.image_len / 1024; // actual firmware size in KB + } else { + p_info.size = 0; + } + partition_info_[part->label] = p_info; } - - partition_info_[part->label] = p_info; } it = esp_partition_next(it); // loop to next partition @@ -1613,8 +1613,8 @@ bool System::check_upgrade() { settingsVersion = "3.5.0"; // this was the last stable version without version info } - version::Semver200_version settings_version(settingsVersion); - version::Semver200_version this_version(EMSESP_APP_VERSION); + version::EMSESP_Version settings_version(settingsVersion); + version::EMSESP_Version this_version(EMSESP_APP_VERSION); std::string settings_version_type = settings_version.prerelease().empty() ? "" : ("-" + settings_version.prerelease()); std::string this_version_type = this_version.prerelease().empty() ? "" : ("-" + this_version.prerelease()); @@ -1765,18 +1765,17 @@ void System::exportSettings(const std::string & type, const char * filename, Jso File settingsFile = LittleFS.open(filename); if (settingsFile) { - JsonDocument jsonDocument; - DeserializationError error = deserializeJson(jsonDocument, settingsFile); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject node = output[section].to(); - for (JsonPair kvp : jsonDocument.as()) { - node[kvp.key()] = kvp.value(); + { + JsonDocument jsonDocument; + DeserializationError error = deserializeJson(jsonDocument, settingsFile); + settingsFile.close(); // close early, we no longer need the file + if (error || !jsonDocument.is()) { + LOG_ERROR("Failed to deserialize settings file %s", filename); + return; } - } else { - LOG_ERROR("Failed to deserialize settings file %s", filename); + output[section].set(jsonDocument.as()); } LOG_DEBUG("Exported %s settings from file %s", section, filename); - settingsFile.close(); } else { LOG_ERROR("No settings file for %s found", filename); } @@ -1828,13 +1827,15 @@ void System::exportSystemBackup(JsonObject output) { if (file) { JsonDocument jsonDocument; DeserializationError error = deserializeJson(jsonDocument, file); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject node = nodes.add(); - node["type"] = "customSupport"; - node["data"] = jsonDocument.as(); + file.close(); // close early, we no longer need the file + if (!error && jsonDocument.is()) { + JsonObject support_node = nodes.add(); + support_node["type"] = "customSupport"; + support_node["data"].set(jsonDocument.as()); + LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE); + } else { + LOG_ERROR("Failed to deserialize custom support file"); } - file.close(); - LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE); } // Backup NVS values diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index 23f4d3486..f360ff99e 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -253,7 +253,7 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) { // it's a filename with a .bin or .md extension, try and extract the version from it // e.g. EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin -> major=3 minor=8 patch=2 - version::Semver200_version latest_version; + version::EMSESP_Version latest_version; if ((version.find(".bin") != std::string::npos) || (version.find(".md") != std::string::npos)) { std::string filename = version; auto pos = filename.find("EMS-ESP-"); @@ -274,18 +274,18 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) { std::string major_version = filename.substr(pos, underscore1 - pos); std::string minor_version = filename.substr(underscore1 + 1, underscore2 - underscore1 - 1); std::string patch_version = filename.substr(underscore2 + 1, dash - underscore2 - 1); - latest_version = version::Semver200_version(major_version + "." + minor_version + "." + patch_version); + latest_version = version::EMSESP_Version(major_version + "." + minor_version + "." + patch_version); } else { // if it's .json file exit if (version.find(".json") != std::string::npos) { return 0; } else { // treat it like a version string like "3.9.0" - latest_version = version::Semver200_version(version); + latest_version = version::EMSESP_Version(version); } } - version::Semver200_version current_version(current_version_s); // get current version + version::EMSESP_Version current_version(current_version_s); // get current version if (latest_version > current_version && current_version.minor() < latest_version.minor()) { return 0; // if it's just a minor version upgrade return 0 @@ -306,9 +306,9 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) { // versions holds the latest development version and stable version in one string, comma separated bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) { if (!version.empty()) { - version::Semver200_version current_version(current_version_s); - version::Semver200_version latest_dev_version(version.substr(0, version.find(','))); - version::Semver200_version latest_stable_version(version.substr(version.find(',') + 1)); + version::EMSESP_Version current_version(current_version_s); + version::EMSESP_Version latest_dev_version(version.substr(0, version.find(','))); + version::EMSESP_Version latest_stable_version(version.substr(version.find(',') + 1)); bool dev_upgradeable = latest_dev_version > current_version; bool stable_upgradeable = latest_stable_version > current_version; diff --git a/src/web/WebStatusService.h b/src/web/WebStatusService.h index 8d2c69bfd..6d3f59f1a 100644 --- a/src/web/WebStatusService.h +++ b/src/web/WebStatusService.h @@ -4,7 +4,7 @@ #define EMSESP_SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus" #define EMSESP_ACTION_SERVICE_PATH "/rest/action" -#include // for version checking +#include "../core/EMSESP_Version.h" #include "../emsesp_version.h" namespace emsesp { From 1d33a2631819422c32087fe3605bb27a449374bf Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 18 Apr 2026 19:42:45 +0200 Subject: [PATCH 03/10] fix dns name being set to "tasmota" --- src/ESP32React/NetworkSettingsService.cpp | 10 ++++++---- src/core/system.cpp | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ESP32React/NetworkSettingsService.cpp b/src/ESP32React/NetworkSettingsService.cpp index a69a549c6..29b12b19a 100644 --- a/src/ESP32React/NetworkSettingsService.cpp +++ b/src/ESP32React/NetworkSettingsService.cpp @@ -80,11 +80,11 @@ void NetworkSettingsService::loop() { case CONNECT_IDLE: if (ETH.started() && _state.ssid.length() == 0) { emsesp::EMSESP::logger().info("ETH started"); + ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str()); ETH.enableIPv6(true); if (_state.staticIPConfig) { ETH.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); } - ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str()); connect = CONNECT_WAIT_ETH; } if (WiFi.isConnected()) { @@ -173,11 +173,13 @@ void NetworkSettingsService::manageSTA() { // Connect or reconnect as required if ((WiFi.getMode() & WIFI_STA) == 0) { + WiFi.setHostname(_state.hostname.c_str()); // updates shared default_hostname buffer + WiFi.enableSTA(true); // creates the STA netif + WiFi.STA.setHostname(_state.hostname.c_str()); // pushes to esp_netif_set_hostname WiFi.enableIPv6(true); if (_state.staticIPConfig) { WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); // configure for static IP } - WiFi.setHostname(_state.hostname.c_str()); // set hostname // www.esp32.com/viewtopic.php?t=12055 if (_state.bandwidth20) { @@ -419,11 +421,11 @@ void NetworkSettingsService::WiFiEvent(arduino_event_id_t event, arduino_event_i break; case ARDUINO_EVENT_ETH_START: - // configure for static IP + // apply hostname FIRST so DHCP DISCOVER carries the correct name + ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str()); if (_state.staticIPConfig) { ETH.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); } - ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str()); break; case ARDUINO_EVENT_ETH_GOT_IP: diff --git a/src/core/system.cpp b/src/core/system.cpp index d1d39487c..b2acd2c50 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1061,6 +1061,10 @@ void System::network_init() { digitalWrite(eth_power_, HIGH); } eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); + if (eth_present_) { + // Push hostname to the ETH netif immediately after it's created + ETH.setHostname(hostname_.c_str()); + } #endif } From 539e6ed08011e5d1e5b747a2a0152b55c48be12c Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 10:17:10 +0200 Subject: [PATCH 04/10] remove lazy loading --- interface/src/AppRouting.tsx | 68 ++++++------- interface/src/AuthenticatedRouting.tsx | 131 ++++++++++++------------- interface/vite.config.ts | 97 ++---------------- 3 files changed, 98 insertions(+), 198 deletions(-) diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx index 91c8f0945..aee79140d 100644 --- a/interface/src/AppRouting.tsx +++ b/interface/src/AppRouting.tsx @@ -1,19 +1,13 @@ -import { type FC, Suspense, lazy, memo, useContext, useEffect, useRef } from 'react'; +import { type FC, memo, useContext, useEffect, useRef } from 'react'; import { Navigate, Route, Routes } from 'react-router'; import { toast } from 'react-toastify'; -import { - LoadingSpinner, - RequireAuthenticated, - RequireUnauthenticated -} from 'components'; +import AuthenticatedRouting from 'AuthenticatedRouting'; +import SignIn from 'SignIn'; +import { RequireAuthenticated, RequireUnauthenticated } from 'components'; import { Authentication, AuthenticationContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; -// Lazy load route components for better code splitting -const SignIn = lazy(() => import('SignIn')); -const AuthenticatedRouting = lazy(() => import('AuthenticatedRouting')); - interface SecurityRedirectProps { readonly message: string; readonly signOut?: boolean; @@ -45,34 +39,32 @@ const AppRouting: FC = memo(() => { return ( - }> - - } - /> - } - /> - - - - } - /> - - - - } - /> - - + + } + /> + } + /> + + + + } + /> + + + + } + /> + ); }); diff --git a/interface/src/AuthenticatedRouting.tsx b/interface/src/AuthenticatedRouting.tsx index 1be5b0f49..8ace232aa 100644 --- a/interface/src/AuthenticatedRouting.tsx +++ b/interface/src/AuthenticatedRouting.tsx @@ -1,86 +1,77 @@ -import { Suspense, lazy, memo, useContext } from 'react'; +import { memo, useContext } from 'react'; import { Navigate, Route, Routes } from 'react-router'; -import { Layout, LoadingSpinner } from 'components'; +import CustomEntities from 'app/main/CustomEntities'; +import Customizations from 'app/main/Customizations'; +import Dashboard from 'app/main/Dashboard'; +import Devices from 'app/main/Devices'; +import Help from 'app/main/Help'; +import Modules from 'app/main/Modules'; +import Scheduler from 'app/main/Scheduler'; +import Sensors from 'app/main/Sensors'; +import UserProfile from 'app/main/UserProfile'; +import APSettings from 'app/settings/APSettings'; +import ApplicationSettings from 'app/settings/ApplicationSettings'; +import DownloadUpload from 'app/settings/DownloadUpload'; +import MqttSettings from 'app/settings/MqttSettings'; +import NTPSettings from 'app/settings/NTPSettings'; +import Settings from 'app/settings/Settings'; +import Network from 'app/settings/network/Network'; +import Security from 'app/settings/security/Security'; +import APStatus from 'app/status/APStatus'; +import Activity from 'app/status/Activity'; +import HardwareStatus from 'app/status/HardwareStatus'; +import MqttStatus from 'app/status/MqttStatus'; +import NTPStatus from 'app/status/NTPStatus'; +import NetworkStatus from 'app/status/NetworkStatus'; +import Status from 'app/status/Status'; +import SystemLog from 'app/status/SystemLog'; +import Version from 'app/status/Version'; +import { Layout } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; -// Lazy load all route components for better code splitting -const Dashboard = lazy(() => import('app/main/Dashboard')); -const Devices = lazy(() => import('app/main/Devices')); -const Sensors = lazy(() => import('app/main/Sensors')); -const Help = lazy(() => import('app/main/Help')); -const Customizations = lazy(() => import('app/main/Customizations')); -const Scheduler = lazy(() => import('app/main/Scheduler')); -const CustomEntities = lazy(() => import('app/main/CustomEntities')); -const Modules = lazy(() => import('app/main/Modules')); -const UserProfile = lazy(() => import('app/main/UserProfile')); - -const Status = lazy(() => import('app/status/Status')); -const HardwareStatus = lazy(() => import('app/status/HardwareStatus')); -const Activity = lazy(() => import('app/status/Activity')); -const SystemLog = lazy(() => import('app/status/SystemLog')); -const MqttStatus = lazy(() => import('app/status/MqttStatus')); -const NTPStatus = lazy(() => import('app/status/NTPStatus')); -const APStatus = lazy(() => import('app/status/APStatus')); -const NetworkStatus = lazy(() => import('app/status/NetworkStatus')); -const Version = lazy(() => import('app/status/Version')); - -const Settings = lazy(() => import('app/settings/Settings')); -const ApplicationSettings = lazy(() => import('app/settings/ApplicationSettings')); -const MqttSettings = lazy(() => import('app/settings/MqttSettings')); -const NTPSettings = lazy(() => import('app/settings/NTPSettings')); -const APSettings = lazy(() => import('app/settings/APSettings')); -const DownloadUpload = lazy(() => import('app/settings/DownloadUpload')); -const Network = lazy(() => import('app/settings/network/Network')); -const Security = lazy(() => import('app/settings/security/Security')); - const AuthenticatedRouting = memo(() => { const { me } = useContext(AuthenticatedContext); return ( - }> - - } /> - } /> - } /> - } /> - } /> + + } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {me.admin && ( - <> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> + {me.admin && ( + <> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - } /> - } /> + } /> + } /> - } /> - } /> - } /> - - )} + } /> + } /> + } /> + + )} - } /> - - + } /> + ); }); diff --git a/interface/vite.config.ts b/interface/vite.config.ts index 5c288677c..ff34966b7 100644 --- a/interface/vite.config.ts +++ b/interface/vite.config.ts @@ -15,7 +15,7 @@ const REPEAT_CHAR = '='; const REPEAT_COUNT = 50; const DEFAULT_OUT_DIR = 'dist'; const ES_TARGET = 'es2020'; -const CHUNK_SIZE_WARNING_LIMIT = 512; +const CHUNK_SIZE_WARNING_LIMIT = 1024; const ASSETS_INLINE_LIMIT = 4096; // Common resolve aliases @@ -130,87 +130,11 @@ const createBasePlugins = ( return plugins; }; -// Manual chunk splitting strategy -const createManualChunks = (detailed = false) => { - return (id: string): string | undefined => { - if (id.includes('node_modules')) { - if (id.includes('preact')) return '@preact'; - if (detailed) { - if (id.includes('react-router')) return '@react-router'; - if (id.includes('@mui/material')) return '@mui-material'; - if (id.includes('@mui/icons-material')) return '@mui-icons'; - if (id.includes('alova')) return '@alova'; - if (id.includes('typesafe-i18n')) return '@i18n'; - if (id.includes('react-toastify')) return '@toastify'; - if (id.includes('@table-library')) return '@table-library'; - if (id.includes('uuid')) return '@uuid'; - if (id.includes('axios') || id.includes('fetch')) return '@http'; - if (id.includes('lodash') || id.includes('ramda')) return '@utils'; - } - return 'vendor'; - } - if (detailed) { - // Group circularly dependent modules together to avoid circular chunk warnings - // components, app, and utils are tightly coupled, so combine them - if ( - id.includes('components/') || - id.includes('app/') || - id.includes('utils/') - ) { - return 'app'; - } - // Keep api separate as it's typically more independent - if (id.includes('api/')) return 'api'; - } - return undefined; - }; +const manualChunks = (id: string): string | undefined => { + if (id.includes('node_modules')) return 'vendor'; + return undefined; }; -// Rolldown-native codeSplitting groups for the production build. -// See: https://rolldown.rs/reference/outputoptions.codesplitting -const createRolldownCodeSplitting = () => ({ - groups: [ - { name: '@preact', test: /[\\/]node_modules[\\/].*preact/, priority: 100 }, - { - name: '@react-router', - test: /[\\/]node_modules[\\/].*react-router/, - priority: 95 - }, - { - name: '@mui-material', - test: /[\\/]node_modules[\\/]@mui[\\/]material/, - priority: 95 - }, - { - name: '@mui-icons', - test: /[\\/]node_modules[\\/]@mui[\\/]icons-material/, - priority: 95 - }, - { name: '@alova', test: /[\\/]node_modules[\\/].*alova/, priority: 90 }, - { name: '@i18n', test: /[\\/]node_modules[\\/].*typesafe-i18n/, priority: 90 }, - { - name: '@toastify', - test: /[\\/]node_modules[\\/].*react-toastify/, - priority: 90 - }, - { - name: '@table-library', - test: /[\\/]node_modules[\\/]@table-library/, - priority: 90 - }, - { name: 'vendor', test: /[\\/]node_modules[\\/]/, priority: 10 }, - // Collapse the lazy-loaded route stubs + shared app/components/utils - // code into one chunk. This cuts firmware-side route count roughly in - // half and speeds up route-matching on every incoming HTTP request. - { - name: 'app', - test: /[\\/](app|components|utils)[\\/]/, - priority: 5 - }, - { name: 'api', test: /[\\/]api[\\/]/, priority: 5 } - ] -}); - // Common build base configuration const createBaseBuildConfig = () => ({ target: ES_TARGET, @@ -335,7 +259,7 @@ export default defineConfig( moduleSideEffects: false }, output: { - manualChunks: createManualChunks(false) + manualChunks } } } @@ -375,17 +299,10 @@ export default defineConfig( chunkFileNames: 'assets/[name]-[hash].js', entryFileNames: 'assets/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash].[ext]', - // Kept as a no-op for documentation/fallback. With Vite 8 - manualChunks: createManualChunks(true), + manualChunks, sourcemap: false } - }, - rolldownOptions: { - output: { - codeSplitting: createRolldownCodeSplitting() - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any + } } }; } From bbb086ea413c581c9f843528df5894b783fab837 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 13:14:54 +0200 Subject: [PATCH 05/10] add back NTP --- src/ESP32React/ESP32React.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ESP32React/ESP32React.cpp b/src/ESP32React/ESP32React.cpp index 024dcd1ff..89f8fd485 100644 --- a/src/ESP32React/ESP32React.cpp +++ b/src/ESP32React/ESP32React.cpp @@ -6,7 +6,7 @@ static constexpr const char CACHE_CONTROL[] = "public,max-age=60"; -// Single static-content handler serving all assets embedded in WWWData.h. +// Single static-content handler serving all assets embedded in WWWData.h class StaticContentHandler : public AsyncWebHandler { public: bool canHandle(AsyncWebServerRequest * request) const override { @@ -15,8 +15,7 @@ class StaticContentHandler : public AsyncWebHandler { } void handleRequest(AsyncWebServerRequest * request) override { - // OPTIONS is handled generically - the server-level CORS headers are - // attached via DefaultHeaders in ESP32React::begin(). + // OPTIONS is handled generically - the server-level CORS headers are attached via DefaultHeaders in ESP32React::begin(). if (request->method() == HTTP_OPTIONS) { request->send(200); return; @@ -57,8 +56,7 @@ class StaticContentHandler : public AsyncWebHandler { } // Returns the /index.html asset, used as the SPA fallback for any GET - // that didn't match an embedded asset (React Router handles routing on - // the client side). + // that didn't match an embedded asset (React Router handles routing on the client side). static const WWWAsset * index_asset() { static const WWWAsset * cached = nullptr; if (cached == nullptr) { @@ -106,4 +104,5 @@ void ESP32React::loop() { _networkSettingsService.loop(); _apSettingsService.loop(); _mqttSettingsService.loop(); + _ntpSettingsService.loop(); } \ No newline at end of file From 51cea8e757234d45971609c91e1452323f07ab19 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 13:15:11 +0200 Subject: [PATCH 06/10] no need to call ntp on begin as its in the loop --- src/ESP32React/NTPSettingsService.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ESP32React/NTPSettingsService.cpp b/src/ESP32React/NTPSettingsService.cpp index 2cf67353f..4faebb1b9 100644 --- a/src/ESP32React/NTPSettingsService.cpp +++ b/src/ESP32React/NTPSettingsService.cpp @@ -16,7 +16,6 @@ NTPSettingsService::NTPSettingsService(AsyncWebServer * server, FS * fs, Securit void NTPSettingsService::begin() { _fsPersistence.readFromFS(); - configureNTP(); } void NTPSettingsService::loop() { @@ -53,7 +52,7 @@ void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVari time_t time = mktime(&tm); struct timeval now = {.tv_sec = time, .tv_usec = {}}; #if CONFIG_IDF_TARGET_ESP32C3 - // settimeofday and adjtime() does not work, unknown how to set time + // settimeofday and adjtime() does not work, unknown how to set time emsesp::EMSESP::logger().warning("manual clock setting not possible"); #else settimeofday(&now, nullptr); From b0db054e1131a7ba6d94e690f96aae6fa599115c Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 14:17:31 +0200 Subject: [PATCH 07/10] fix firmware install date (was using UTC as TZ not initialised) --- src/core/system.cpp | 27 ++++++++++++++++----------- src/core/system.h | 2 +- src/web/WebStatusService.cpp | 21 +++++++++++++++------ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index b2acd2c50..b39ce5e15 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -407,11 +407,11 @@ void System::get_partition_info() { partition_info_.clear(); // clear existing data #ifdef EMSESP_STANDALONE - // dummy data for standalone mode - version, size, install_date - partition_info_["app0"] = {EMSESP_APP_VERSION, 0, ""}; - partition_info_["app1"] = {"", 0, ""}; - partition_info_["factory"] = {"", 0, ""}; - partition_info_["boot"] = {"", 0, ""}; + // dummy data for standalone mode - version, size, install_date in UTC epoch + partition_info_["app0"] = {EMSESP_APP_VERSION, 0, 0}; + partition_info_["app1"] = {"", 0, 0}; + partition_info_["factory"] = {"", 0, 0}; + partition_info_["boot"] = {"", 0, 0}; #else auto current_partition = (const char *)esp_ota_get_running_partition()->label; @@ -451,10 +451,8 @@ void System::get_partition_info() { p_info.version = EMSESP::nvs_.getString(part->label, "").c_str(); char c[20]; snprintf(c, sizeof(c), "d_%s", (const char *)part->label); - time_t d = EMSESP::nvs_.getULong(c, 0); - char time_string[25]; - strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); - p_info.install_date = d > 1500000000L ? time_string : ""; + time_t d = EMSESP::nvs_.getULong(c, 0); + p_info.install_date = d > 1500000000L ? d : 0; // store UTC epoch; formatted to local time at render if (!p_info.version.empty()) { esp_image_metadata_t meta = {}; @@ -474,7 +472,7 @@ void System::get_partition_info() { #endif } -// set NTP install time/date for the current partition +// set install time/date for the current partition, in UTC // assumes NTP is connected and working void System::set_partition_install_date() { #ifndef EMSESP_STANDALONE @@ -1337,11 +1335,18 @@ void System::show_system(uuid::console::Shell & shell) { if (partition.second.version.empty()) { continue; // no version, empty string } + std::string installed; + if (partition.second.install_date > 0) { + char time_string[25]; + time_t d = partition.second.install_date; + strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); + installed = std::string(", installed on ") + time_string; + } shell.printfln(" %s: v%s (%d KB%s) %s", partition.first.c_str(), partition.second.version.c_str(), partition.second.size, - partition.second.install_date.empty() ? "" : (std::string(", installed on ") + partition.second.install_date).c_str(), + installed.c_str(), (strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : ""); } diff --git a/src/core/system.h b/src/core/system.h index e869faf4b..52f6b0e5c 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -75,7 +75,7 @@ enum FUSE_VALUE : uint8_t { ALL = 0, MFG = 1, MODEL = 2, BOARD = 3, REV = 4, BAT struct PartitionInfo { std::string version; size_t size; - std::string install_date; // optional, only available if NTP is connected + time_t install_date; // UTC epoch seconds; 0 if unknown. Format with localtime() at render time so it honors the current TZ. }; class System { diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index f360ff99e..aa66292b1 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -146,18 +146,27 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { } // get the partition info for each partition, including the running one - // the partition data is done once in System::start() and stored in partition_info_ + // the partition data is gathered once in System::start() and stored in partition_info_ + // install_date is stored as a UTC epoch and formatted to local time here so it honors + // the current TZ (which may not have been set yet when System::start() ran). JsonArray partitions = root["partitions"].to(); for (const auto & partition : EMSESP::system_.partition_info_) { // Skip partition if it has no version, or it's size is 0 if (partition.second.version.empty() || partition.second.size == 0) { continue; } - JsonObject part = partitions.add(); - part["partition"] = partition.first; - part["version"] = partition.second.version; - part["size"] = partition.second.size; - part["install_date"] = partition.second.install_date; + JsonObject part = partitions.add(); + part["partition"] = partition.first; + part["version"] = partition.second.version; + part["size"] = partition.second.size; + if (partition.second.install_date > 0) { + char time_string[25]; + time_t d = partition.second.install_date; + strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); + part["install_date"] = time_string; + } else { + part["install_date"] = ""; + } } root["developer_mode"] = EMSESP::system_.developer_mode(); From acd23925b5915036201f9da9a1de5f5fed73ebf4 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 15:10:20 +0200 Subject: [PATCH 08/10] download text changes --- interface/src/i18n/cz/index.ts | 6 +++--- interface/src/i18n/de/index.ts | 4 ++-- interface/src/i18n/en/index.ts | 4 ++-- interface/src/i18n/fr/index.ts | 4 ++-- interface/src/i18n/it/index.ts | 4 ++-- interface/src/i18n/nl/index.ts | 4 ++-- interface/src/i18n/no/index.ts | 4 ++-- interface/src/i18n/pl/index.ts | 4 ++-- interface/src/i18n/sk/index.ts | 4 ++-- interface/src/i18n/sv/index.ts | 4 ++-- interface/src/i18n/tr/index.ts | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/interface/src/i18n/cz/index.ts b/interface/src/i18n/cz/index.ts index 158655f96..fbce09041 100644 --- a/interface/src/i18n/cz/index.ts +++ b/interface/src/i18n/cz/index.ts @@ -350,19 +350,19 @@ const cz: Translation = { NO_DATA_1: 'Nebyly nalezeny žádné oblíbené entity. Použijte modul', NO_DATA_2: 'pro jejich výběr.', NO_DATA_3: 'Pro zobrazení všech dostupných entit navštivte stránku', - NO_GPIO:'Nebylo nalezeno žádné volné GPIO', + NO_GPIO: 'Nebylo nalezeno žádné volné GPIO', THIS_VERSION: 'Tato verze', PLATFORM: 'Platforma', RELEASE_TYPE: 'Typ sestavení', INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení', SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi', FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru', - NO_DATA: 'Žádné údaje', + NO_DATA: 'žádné údaje', USER_PROFILE: 'Uživatelský profil', STORED_VERSIONS: 'Uložené verze', ONLINE_HELP: 'online nápověda', UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy', - UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že jste vytvořili zálohu své konfigurace a nastavení před pokračováním a nahrajte ji po instalaci nové verze.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.', UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.', WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?' diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index e8d17c01b..4a9710e1d 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -350,7 +350,7 @@ const de: Translation = { NO_DATA_1: 'Keine favorisierten EMS-Entitäten gefunden! Verwenden Sie das Modul', NO_DATA_2: ', um sie zu markieren.', NO_DATA_3: 'Um alle verfügbaren Entitäten anzuzeigen, gehen Sie zu', - NO_GPIO:'Keine freien GPIO gefunden', + NO_GPIO: 'Keine freien GPIO gefunden', THIS_VERSION: 'Diese Version', PLATFORM: 'Plattform', RELEASE_TYPE: 'Release Typ', @@ -362,7 +362,7 @@ const de: Translation = { STORED_VERSIONS: 'Gespeicherte Versionen', ONLINE_HELP: 'Online-Hilfe', UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren', - UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie eine Sicherung Ihrer Konfiguration und Einstellungen vor dem Fortfahren erstellt haben und diese nach der Installation der neuen Version hochladen.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.', UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.', WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und -einstellungen erstellen. Alle Passwörter werden im Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?' diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 0c116d399..610ed0f00 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -350,7 +350,7 @@ const en: Translation = { NO_DATA_1: 'No favorite EMS entities found yet. Use the', NO_DATA_2: 'module to mark them.', NO_DATA_3: 'To see all available entities go to', - NO_GPIO:'No available GPIO found', + NO_GPIO: 'No available GPIO found', THIS_VERSION: 'This Version', PLATFORM: 'Platform', RELEASE_TYPE: 'Release Type', @@ -362,7 +362,7 @@ const en: Translation = { STORED_VERSIONS: 'Stored Versions', ONLINE_HELP: 'online help', UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages', - UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you have made a backup of your configuration and settings before continuing, and upload this after the new version is installed.', + UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.', UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.', WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?' diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index 528c83bd9..f1945d2ab 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -350,7 +350,7 @@ const fr: Translation = { NO_DATA_1: 'Aucune entité EMS favorite trouvée. Utilisez le', NO_DATA_2: 'module pour les marquer.', NO_DATA_3: 'Pour voir toutes les entités disponibles, aller à', - NO_GPIO:"Aucun GPIO disponible n'a été détecté", + NO_GPIO: "Aucun GPIO disponible n'a été détecté", THIS_VERSION: 'Cette version', PLATFORM: 'Plateforme', RELEASE_TYPE: 'Type de version', @@ -362,7 +362,7 @@ const fr: Translation = { STORED_VERSIONS: 'Versions stockées', ONLINE_HELP: 'aide en ligne', UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants', - UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous d\'avoir créé une sauvegarde de vos configurations et paramètres avant de continuer et de la charger après l\'installation de la nouvelle version.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.', UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.', WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?' diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index 04fe977d2..df8b4de61 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -350,7 +350,7 @@ const it: Translation = { NO_DATA_1: 'Nessuna entità EMS preferita trovata. Usa il', NO_DATA_2: 'modulo per marcarle.', NO_DATA_3: 'Per vedere tutte le entità disponibili vai a', - NO_GPIO:'Non è stato trovato alcun GPIO disponibile', + NO_GPIO: 'Non è stato trovato alcun GPIO disponibile', THIS_VERSION: 'Questa versione', PLATFORM: 'Piattaforma', RELEASE_TYPE: 'Tipo di rilascio', @@ -362,7 +362,7 @@ const it: Translation = { STORED_VERSIONS: 'Versioni memorizzate', ONLINE_HELP: 'aiuto online', UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti', - UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di aver creato un backup delle tue configurazioni e impostazioni prima di continuare e di caricarlo dopo l\'installazione della nuova versione.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.', UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.', WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?' diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 70176a084..888c033d8 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -350,7 +350,7 @@ const nl: Translation = { NO_DATA_1: 'Er zijn nog geen favoriete EMS-entiteiten gevonden. Gebruik de', NO_DATA_2: 'module om ze te markeren.', NO_DATA_3: 'Om alle beschikbare entiteiten te zien, ga naar', - NO_GPIO:'Er is geen beschikbare GPIO gevonden', + NO_GPIO: 'Er is geen beschikbare GPIO gevonden', THIS_VERSION: 'Deze Versie', PLATFORM: 'Platform', RELEASE_TYPE: 'Release Typ', @@ -362,7 +362,7 @@ const nl: Translation = { STORED_VERSIONS: 'Opgeslagen versies', ONLINE_HELP: 'online help', UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten', - UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u een back-up van uw configuratie en instellingen hebt gemaakt voordat u doorgaat en upload deze na de installatie van de nieuwe versie.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.', UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.', WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?' diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index f37f87d99..693ef29e8 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -350,7 +350,7 @@ const no: Translation = { NO_DATA_1: 'Ingen favoritte EMS enheter funnet enda. Bruk', NO_DATA_2: 'modul for å markere dem.', NO_DATA_3: 'For å se alle tilgjengelige enheter, gå til', - NO_GPIO:'Det ble ikke funnet noen tilgjengelige GPIO-porter', + NO_GPIO: 'Det ble ikke funnet noen tilgjengelige GPIO-porter', THIS_VERSION: 'Denne versjonen', PLATFORM: 'Plattform', RELEASE_TYPE: 'Utgivelses type', @@ -362,7 +362,7 @@ const no: Translation = { STORED_VERSIONS: 'Lagret versjoner', ONLINE_HELP: 'online hjelp', UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger', - UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du har laget en sikkerhetskopi av din konfigurasjon og innstillinger før du fortsetter, og last denne opp etter at den nye versjonen er installert.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.', UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.', WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?' diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index 5a279ba93..c6dd004de 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -350,7 +350,7 @@ const pl: BaseTranslation = { NO_DATA_1: 'Brak ulubionych encji EMS. Użyj', NO_DATA_2: 'moduł do ich oznaczenia.', NO_DATA_3: 'Aby zobaczyć wszystkie dostępne encje przejdź do', - NO_GPIO:'Nie znaleziono dostępnych pinów GPIO', + NO_GPIO: 'Nie znaleziono dostępnych pinów GPIO', THIS_VERSION: 'Ta wersja', PLATFORM: 'Platforma', RELEASE_TYPE: 'Typ wydania', @@ -362,7 +362,7 @@ const pl: BaseTranslation = { STORED_VERSIONS: 'Zapisane wersje', ONLINE_HELP: 'pomoc online', UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości', - UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że masz utworzoną kopię swoich ustawień i konfiguracji przed kontynuowaniem i przesuń ją po zainstalowaniu nowej wersji.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.', UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.', WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?' diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index be742048f..5e993e998 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -350,7 +350,7 @@ const sk: Translation = { NO_DATA_1: 'Nenašli sa žiadne obľúbené entity EMS. Použite', NO_DATA_2: 'modul na ich označenie.', NO_DATA_3: 'Ak chcete zobraziť všetky dostupné entity, prejdite na', - NO_GPIO:'Nebol nájdený žiadny dostupný GPIO', + NO_GPIO: 'Nebol nájdený žiadny dostupný GPIO', THIS_VERSION: 'Táto verzia', PLATFORM: 'Platforma', RELEASE_TYPE: 'Typ vydania', @@ -362,7 +362,7 @@ const sk: Translation = { STORED_VERSIONS: 'Uložené verzie', ONLINE_HELP: 'online pomoc', UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy', - UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že ste vytvorili zálohu svojich konfigurácií a nastavení pred pokračovaním a nahrajte ju po instalácii novej verzie.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.', UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.', WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?' diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index c980a54fd..032eeb026 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -350,7 +350,7 @@ const sv: Translation = { NO_DATA_1: 'Inga favorit EMS enheter hittade än. Använd', NO_DATA_2: 'modul för att markera dem.', NO_DATA_3: 'För att se alla tillgängliga enheter, gå till', - NO_GPIO:'Inga tillgängliga GPIO-portar hittades', + NO_GPIO: 'Inga tillgängliga GPIO-portar hittades', THIS_VERSION: 'Denna version', PLATFORM: 'Plattform', RELEASE_TYPE: 'Utgivelsestyp', @@ -362,7 +362,7 @@ const sv: Translation = { STORED_VERSIONS: 'Lagrad versioner', ONLINE_HELP: 'online hjälp', UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden', - UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du har gjort en säkerhetskopia av din konfiguration och inställningar innan du fortsätter och ladda upp denna efter att den nya versionen är installerad.', + UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.', UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.', WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?' diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index 154115d6a..b6caa3238 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -350,7 +350,7 @@ const tr: Translation = { NO_DATA_1: 'Henüz bir favori EMS varlığı bulunamadı. Kullanın', NO_DATA_2: 'modülünü kullanın.', NO_DATA_3: 'Tüm kullanılabilir varlıkları görmek için git', - NO_GPIO:'Kullanılabilir GPIO bulunamadı', + NO_GPIO: 'Kullanılabilir GPIO bulunamadı', THIS_VERSION: 'Bu Sürüm', PLATFORM: 'Platforma', RELEASE_TYPE: 'Sürüm Tipi', From e2750b85727a3217aef722f5722fd7bb1c1ec23a Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 15:10:41 +0200 Subject: [PATCH 09/10] don't show system backup as its the same page! --- interface/src/components/upload/DragNdrop.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/src/components/upload/DragNdrop.tsx b/interface/src/components/upload/DragNdrop.tsx index 4bc865fc3..0305b9492 100644 --- a/interface/src/components/upload/DragNdrop.tsx +++ b/interface/src/components/upload/DragNdrop.tsx @@ -211,14 +211,15 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => { {LL.UPGRADE_IMPORTANT_MESSAGES()} - {upgradeImportantMessageType === 1 && - LL.UPGRADE_IMPORTANT_MESSAGES_1()} {upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()} + {upgradeImportantMessageType === 1 && + LL.UPGRADE_IMPORTANT_MESSAGES_1()} {LL.ONLINE_HELP()} From dde6a8c5db48ba99ade9adc3000f73dd6ba156df Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 19 Apr 2026 15:10:51 +0200 Subject: [PATCH 10/10] close dialog after download --- interface/src/app/settings/DownloadUpload.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/app/settings/DownloadUpload.tsx b/interface/src/app/settings/DownloadUpload.tsx index 6905c92b9..02a373e12 100644 --- a/interface/src/app/settings/DownloadUpload.tsx +++ b/interface/src/app/settings/DownloadUpload.tsx @@ -112,6 +112,7 @@ const DownloadUpload = () => { const handleDownload = useCallback( (type: string) => () => { void sendExportData(type); + setConfirmBackup(false); }, [sendExportData] );