Skip to content

refactor(ota): split OTA into Manager, Client, and FirmwareCDN#316

Open
hhvrc wants to merge 13 commits intodevelopfrom
refactor/ota-update-manager
Open

refactor(ota): split OTA into Manager, Client, and FirmwareCDN#316
hhvrc wants to merge 13 commits intodevelopfrom
refactor/ota-update-manager

Conversation

@hhvrc
Copy link
Copy Markdown
Contributor

@hhvrc hhvrc commented Nov 12, 2024

Summary

  • Split monolithic OtaUpdateManager.cpp (780 lines) into three focused modules
  • OtaUpdateManager — Watcher task with WiFi event handling, periodic update checks, version comparison, and firmware lifecycle (boot type, validation, rollback)
  • OtaUpdateClient — Single-shot update execution: fetches release metadata from CDN, flashes filesystem and app partitions, reboots into new firmware
  • FirmwareCDN — HTTP client for the firmware CDN: version checks, board listings, binary hash fetching, and release info assembly
  • Updated to current develop APIs (HTTP tcb::span, ContentType constants, StringRemovePrefix)
  • Removed _ prefix from all static functions and variables

Test plan

  • Builds for OpenShock-Core-V2 (ESP32-S3)
  • Builds for Wemos-D1-Mini-ESP32 (ESP32)
  • Verify OTA update check triggers on WiFi connect
  • Verify OTA update downloads and flashes correctly
  • Verify rollback works on failed update

🤖 Generated with Claude Code

@hhvrc hhvrc self-assigned this Nov 12, 2024
@hhvrc hhvrc added this to the 1.5.0 Release milestone Jul 31, 2025
@hhvrc hhvrc changed the title Refactor OTA update manager refactor: Clean up OTA update manager Dec 9, 2025
hhvrc and others added 2 commits March 26, 2026 23:11
Separate OTA update system into three concerns:
- OtaUpdateManager: watcher task, lifecycle, WiFi events, periodic checks
- OtaUpdateClient: single update execution (fetch, flash, reboot)
- FirmwareCDN: HTTP calls to firmware CDN (already existed, now wired in)

Fix OtaUpdateClient to be a clean one-shot task instead of containing
the watcher loop. Use FirmwareCDN::GetFirmwareReleaseInfo instead of
inline HTTP. Fix FnProxy usage, wrong serialization namespaces, stale
API calls, and missing includes. Remove _ prefix from all statics.
Update FirmwareCDN to current HTTP API (tcb::span, ContentType constants).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 26, 2026

🦋 Changeset detected

Latest commit: af0df40

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@hhvrc hhvrc marked this pull request as ready for review March 26, 2026 22:23
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

Cpp-Linter Report ⚠️

Some files did not pass the configured checks!

clang-format (v21.1.8) reports: 8 file(s) not formatted
  • src/GatewayClient.cpp
  • src/captiveportal/CaptivePortalInstance.cpp
  • src/config/OtaUpdateConfig.cpp
  • src/http/FirmwareCDN.cpp
  • src/main.cpp
  • src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp
  • src/ota/OtaUpdateClient.cpp
  • src/ota/OtaUpdateManager.cpp
clang-tidy (v21.1.8) reports: 173 concern(s)
  • include/Common.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/Common.h:3:10: error: [clang-diagnostic-error]

    'cstdint' file not found

        3 | #include <cstdint>
          |          ^~~~~~~~~
  • include/Common.h:6:9: warning: [cppcoreguidelines-macro-usage]

    function-like macro 'DISABLE_DEFAULT' used; consider a 'constexpr' template function

        6 | #define DISABLE_DEFAULT(TypeName) TypeName() = delete
          |         ^
  • include/Common.h:7:9: warning: [cppcoreguidelines-macro-usage]

    function-like macro 'DISABLE_COPY' used; consider a 'constexpr' template function

        7 | #define DISABLE_COPY(TypeName)                   \
          |         ^
  • include/Common.h:9:3: warning: [bugprone-macro-parentheses]

    macro argument should be enclosed in parentheses

        9 |   TypeName& operator=(const TypeName&) = delete
          |   ^       
          |   (       )
  • include/Common.h:10:9: warning: [cppcoreguidelines-macro-usage]

    function-like macro 'DISABLE_MOVE' used; consider a 'constexpr' template function

       10 | #define DISABLE_MOVE(TypeName)              \
          |         ^
  • include/Common.h:11:12: warning: [bugprone-macro-parentheses]

    macro argument should be enclosed in parentheses

       11 |   TypeName(TypeName&&)            = delete; \
          |            ^       
          |            (       )
  • include/Common.h:12:3: warning: [bugprone-macro-parentheses]

    macro argument should be enclosed in parentheses

       12 |   TypeName& operator=(TypeName&&) = delete
          |   ^       
          |   (       )
  • include/Common.h:12:23: warning: [bugprone-macro-parentheses]

    macro argument should be enclosed in parentheses

       12 |   TypeName& operator=(TypeName&&) = delete
          |                       ^       
          |                       (       )
  • include/Common.h:24:9: warning: [cppcoreguidelines-macro-usage]

    function-like macro 'OPENSHOCK_REPO_URL' used; consider a 'constexpr' template function

       24 | #define OPENSHOCK_REPO_URL(path) "https://" OPENSHOCK_REPO_DOMAIN path
          |         ^
  • include/Common.h:26:1: warning: [cppcoreguidelines-macro-to-enum]

    replace macro with enum

       26 | #define OPENSHOCK_GPIO_INVALID -1
          | ^~~~~~~
          |                                =
       27 | 
  • include/Common.h:26:9: warning: [cppcoreguidelines-macro-to-enum]

    macro 'OPENSHOCK_GPIO_INVALID' defines an integral constant; prefer an enum instead

       26 | #define OPENSHOCK_GPIO_INVALID -1
          |         ^
  • include/Common.h:26:32: warning: [bugprone-macro-parentheses]

    macro replacement list should be enclosed in parentheses

       26 | #define OPENSHOCK_GPIO_INVALID -1
          |                                ^ 
          |                                ( )
  • include/config/OtaUpdateConfig.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/config/OtaUpdateConfig.h:30:10: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       30 |     bool FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config) override;
          |     ~~~~ ^
          |     auto                                                                              -> bool
  • include/config/OtaUpdateConfig.h:31:86: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       31 |     [[nodiscard]] flatbuffers::Offset<Serialization::Configuration::OtaUpdateConfig> ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override;
          |                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^
          |                   auto                                                                                                                                                    -> flatbuffers::Offset<Serialization::Configuration::OtaUpdateConfig>
  • include/config/OtaUpdateConfig.h:33:10: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       33 |     bool FromJSON(const cJSON* json) override;
          |     ~~~~ ^
          |     auto                             -> bool
  • include/config/OtaUpdateConfig.h:34:26: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       34 |     [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override;
          |                   ~~~~~~ ^
          |                   auto                                        -> cJSON*
  • include/http/FirmwareCDN.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/http/FirmwareCDN.h:21:33: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       21 |   HTTP::Response<LatestRelease> GetLatestRelease(OtaUpdateChannel channel, const std::string& repoDomain);
          |   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^                                                                        
          |   auto                                                                                                    -> HTTP::Response<LatestRelease>
  • include/http/FirmwareCDN.h:28:39: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       28 |   HTTP::Response<FirmwareReleaseInfo> GetRelease(const OpenShock::SemVer& version, const std::string& repoDomain);
          |   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^                                                                          
          |   auto                                                                                                            -> HTTP::Response<FirmwareReleaseInfo>
  • include/ota/FirmwareBinaryHash.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/ota/FirmwareBinaryHash.h:3:10: error: [clang-diagnostic-error]

    'cstdint' file not found

        3 | #include <cstdint>
          |          ^~~~~~~~~
  • include/ota/FirmwareBinaryHash.h:7:10: warning: [cppcoreguidelines-pro-type-member-init]

    constructor does not initialize these fields: name, hash

        7 |   struct FirmwareBinaryHash {
          |          ^
        8 |     std::string name;
          |                     
          |                     {}
        9 |     uint8_t hash[32];
          |                     
          |                     {}
  • include/ota/FirmwareBinaryHash.h:9:17: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

        9 |     uint8_t hash[32];
          |                 ^
  • include/ota/FirmwareBinaryHash.h:9:18: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

        9 |     uint8_t hash[32];
          |                  ^
  • include/ota/FirmwareReleaseInfo.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/ota/FirmwareReleaseInfo.h:3:10: error: [clang-diagnostic-error]

    'cstdint' file not found

        3 | #include <cstdint>
          |          ^~~~~~~~~
  • include/ota/FirmwareReleaseInfo.h:7:10: warning: [cppcoreguidelines-pro-type-member-init]

    constructor does not initialize these fields: appBinaryUrl, appBinaryHash, filesystemBinaryUrl, filesystemBinaryHash

        7 |   struct FirmwareReleaseInfo {
          |          ^
        8 |     std::string appBinaryUrl;
          |                             
          |                             {}
        9 |     uint8_t appBinaryHash[32];
          |                              
          |                              {}
       10 |     std::string filesystemBinaryUrl;
          |                                    
          |                                    {}
       11 |     uint8_t filesystemBinaryHash[32];
          |                                     
          |                                     {}
  • include/ota/FirmwareReleaseInfo.h:9:26: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

        9 |     uint8_t appBinaryHash[32];
          |                          ^
  • include/ota/FirmwareReleaseInfo.h:9:27: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

        9 |     uint8_t appBinaryHash[32];
          |                           ^
  • include/ota/FirmwareReleaseInfo.h:11:33: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

       11 |     uint8_t filesystemBinaryHash[32];
          |                                 ^
  • include/ota/FirmwareReleaseInfo.h:11:34: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       11 |     uint8_t filesystemBinaryHash[32];
          |                                  ^
  • include/ota/OtaUpdateChannel.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/ota/OtaUpdateChannel.h:9:3: warning: [modernize-use-using]

    use 'using' instead of 'typedef'

        9 |   typedef OpenShock::Serialization::Configuration::OtaUpdateChannel OtaUpdateChannel;
          |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |   using OtaUpdateChannel = OpenShock::Serialization::Configuration::OtaUpdateChannel
  • include/ota/OtaUpdateChannel.h:11:15: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       11 |   inline bool TryParseOtaUpdateChannel(OtaUpdateChannel& channel, const char* str)
          |          ~~~~ ^                                                                   
          |          auto                                                                      -> bool
  • include/ota/OtaUpdateClient.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/ota/OtaUpdateClient.h:9:9: warning: [cppcoreguidelines-special-member-functions]

    class 'OtaUpdateClient' defines a destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator

        9 |   class OtaUpdateClient {
          |         ^
  • include/ota/OtaUpdateClient.h:14:10: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       14 |     bool Start();
          |     ~~~~ ^      
          |     auto         -> bool
  • include/ota/OtaUpdateManager.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/ota/OtaUpdateManager.h:7:22: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

        7 |   [[nodiscard]] bool Init();
          |                 ~~~~ ^     
          |                 auto        -> bool
  • include/ota/OtaUpdateManager.h:9:8: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

        9 |   bool TryStartFirmwareUpdate(const OpenShock::SemVer& version);
          |   ~~~~ ^                                                       
          |   auto                                                          -> bool
  • include/ota/OtaUpdateManager.h:11:20: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       11 |   FirmwareBootType GetFirmwareBootType();
          |   ~~~~~~~~~~~~~~~~ ^                    
          |   auto                                   -> FirmwareBootType
  • include/ota/OtaUpdateManager.h:12:8: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       12 |   bool IsValidatingApp();
          |   ~~~~ ^                
          |   auto                   -> bool
  • include/ota/OtaUpdateStep.h:1:1: warning: [portability-avoid-pragma-once]

    avoid 'pragma once' directive; use include guards instead

        1 | #pragma once
          | ^
  • include/ota/OtaUpdateStep.h:9:3: warning: [modernize-use-using]

    use 'using' instead of 'typedef'

        9 |   typedef OpenShock::Serialization::Configuration::OtaUpdateStep OtaUpdateStep;
          |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |   using OtaUpdateStep = OpenShock::Serialization::Configuration::OtaUpdateStep
  • include/ota/OtaUpdateStep.h:11:15: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       11 |   inline bool TryParseOtaUpdateStep(OtaUpdateStep& channel, const char* str)
          |          ~~~~ ^                                                             
          |          auto                                                                -> bool
  • src/GatewayClient.cpp:17:13: warning: [cppcoreguidelines-avoid-non-const-global-variables]

    variable 's_bootStatusSent' is non-const and globally accessible, consider making it const

       17 | static bool s_bootStatusSent = false;
          |             ^
  • src/GatewayClient.cpp:41:29: warning: [bugprone-easily-swappable-parameters]

    3 adjacent parameters of 'connect' of similar type are easily swapped by mistake

       41 | void GatewayClient::connect(const std::string& host, uint16_t port, const std::string& path)
          |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    src/GatewayClient.cpp:41:48: note: the first parameter in the range is 'host'
       41 | void GatewayClient::connect(const std::string& host, uint16_t port, const std::string& path)
          |                                                ^~~~
    src/GatewayClient.cpp:41:88: note: the last parameter in the range is 'path'
       41 | void GatewayClient::connect(const std::string& host, uint16_t port, const std::string& path)
          |                                                                                        ^~~~
    src/GatewayClient.cpp:41:54: note: 'const int &' and 'int' parameters accept and bind the same kind of values
       41 | void GatewayClient::connect(const std::string& host, uint16_t port, const std::string& path)
          |                                                      ^
  • src/GatewayClient.cpp:75:21: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       75 | bool GatewayClient::sendMessageTXT(std::string_view data)
          | ~~~~                ^                                    
          | auto                                                      -> bool
  • src/GatewayClient.cpp:84:21: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       84 | bool GatewayClient::sendMessageBIN(tcb::span<const uint8_t> data)
          | ~~~~                ^                                            
          | auto                                                              -> bool
  • src/GatewayClient.cpp:93:21: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       93 | bool GatewayClient::loop()
          | ~~~~                ^     
          | auto                       -> bool
  • src/GatewayClient.cpp:118:3: warning: [cppcoreguidelines-avoid-do-while]

    avoid do-while loops

      118 |   ESP_ERROR_CHECK(esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_GATEWAY_CLIENT_STATE_CHANGED, &m_state, sizeof(m_state), portMAX_DELAY));
          |   ^
    /home/runner/.platformio/packages/framework-arduinoespressif32/tools/sdk/esp32s3/include/esp_common/include/esp_err.h:115:28: note: expanded from macro 'ESP_ERROR_CHECK'
      115 | #define ESP_ERROR_CHECK(x) do {                                         \
          |                            ^
  • src/GatewayClient.cpp:123:24: warning: [readability-braces-around-statements]

    statement should be inside braces

      123 |   if (s_bootStatusSent) return;
          |                        ^       
          |                         {
  • src/GatewayClient.cpp:133:28: warning: [cppcoreguidelines-init-variables]

    variable 'updateStep' is not initialized

      133 |   OpenShock::OtaUpdateStep updateStep;
          |                            ^
  • src/GatewayClient.cpp:147:124: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this lambda

      147 |   s_bootStatusSent = Serialization::Gateway::SerializeBootStatusMessage(updateId, OtaUpdateManager::GetFirmwareBootType(), [this](tcb::span<const uint8_t> data) { return m_webSocket.sendBIN(data.data(), data.size()); });
          |                                                                                                                            ^
          |                                                                                                                                                                  -> void
  • src/captiveportal/CaptivePortalInstance.cpp:40:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       40 | static const char* const JSON_ERR_INTERNAL        = "{\"error\":\"InternalError\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"InternalError"})"
  • src/captiveportal/CaptivePortalInstance.cpp:41:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       41 | static const char* const JSON_ERR_MISSING_PARAM   = "{\"error\":\"MissingParam\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"MissingParam"})"
  • src/captiveportal/CaptivePortalInstance.cpp:42:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       42 | static const char* const JSON_ERR_INVALID_PIN     = "{\"error\":\"InvalidPin\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"InvalidPin"})"
  • src/captiveportal/CaptivePortalInstance.cpp:43:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       43 | static const char* const JSON_ERR_MISSING_SSID    = "{\"error\":\"MissingSsid\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"MissingSsid"})"
  • src/captiveportal/CaptivePortalInstance.cpp:44:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       44 | static const char* const JSON_ERR_INVALID_SSID    = "{\"error\":\"InvalidSsid\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"InvalidSsid"})"
  • src/captiveportal/CaptivePortalInstance.cpp:45:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       45 | static const char* const JSON_ERR_PASSWORD_SHORT  = "{\"error\":\"PasswordTooShort\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"PasswordTooShort"})"
  • src/captiveportal/CaptivePortalInstance.cpp:46:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       46 | static const char* const JSON_ERR_PASSWORD_LONG   = "{\"error\":\"PasswordTooLong\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"PasswordTooLong"})"
  • src/captiveportal/CaptivePortalInstance.cpp:47:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       47 | static const char* const JSON_ERR_CODE_REQUIRED   = "{\"error\":\"CodeRequired\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"CodeRequired"})"
  • src/captiveportal/CaptivePortalInstance.cpp:48:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       48 | static const char* const JSON_ERR_INVALID_CHANNEL = "{\"error\":\"InvalidChannel\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"InvalidChannel"})"
  • src/captiveportal/CaptivePortalInstance.cpp:49:53: warning: [modernize-raw-string-literal]

    escaped string literal can be written as a raw string literal

       49 | static const char* const JSON_ERR_RATE_LIMITED    = "{\"error\":\"RateLimited\"}";
          |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                     R"({"error":"RateLimited"})"
  • src/captiveportal/CaptivePortalInstance.cpp:51:32: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       51 | static OpenShock::RateLimiter& getAccountLinkRateLimiter()
          |        ~~~~~~~~~~~~~~~~~~~~~~~ ^                          
          |        auto                                                -> OpenShock::RateLimiter&
  • src/captiveportal/CaptivePortalInstance.cpp:53:34: warning: [readability-identifier-length]

    variable name 'rl' is too short, expected at least 3 characters

       53 |   static OpenShock::RateLimiter* rl = nullptr;
          |                                  ^
  • src/captiveportal/CaptivePortalInstance.cpp:55:5: warning: [cppcoreguidelines-owning-memory]

    assigning newly created 'gsl::owner<>' to non-owner 'OpenShock::RateLimiter *'

       55 |     rl = new OpenShock::RateLimiter();
          |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • src/captiveportal/CaptivePortalInstance.cpp:56:18: warning: [cppcoreguidelines-avoid-magic-numbers]

    60'000 is a magic number; consider replacing it with a named constant

       56 |     rl->addLimit(60'000, 5);    // 5 attempts per minute
          |                  ^
  • src/captiveportal/CaptivePortalInstance.cpp:56:26: warning: [cppcoreguidelines-avoid-magic-numbers]

    5 is a magic number; consider replacing it with a named constant

       56 |     rl->addLimit(60'000, 5);    // 5 attempts per minute
          |                          ^
  • src/captiveportal/CaptivePortalInstance.cpp:57:18: warning: [cppcoreguidelines-avoid-magic-numbers]

    300'000 is a magic number; consider replacing it with a named constant

       57 |     rl->addLimit(300'000, 10);  // 10 attempts per 5 minutes
          |                  ^
  • src/captiveportal/CaptivePortalInstance.cpp:57:27: warning: [cppcoreguidelines-avoid-magic-numbers]

    10 is a magic number; consider replacing it with a named constant

       57 |     rl->addLimit(300'000, 10);  // 10 attempts per 5 minutes
          |                           ^
  • src/captiveportal/CaptivePortalInstance.cpp:64:31: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       64 | static const esp_partition_t* getStaticPartition()
          |        ~~~~~~~~~~~~~~~~~~~~~~ ^                   
          |        auto                                        -> const esp_partition_t*
  • src/captiveportal/CaptivePortalInstance.cpp:79:20: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       79 | static const char* getPartitionHash()
          |        ~~~~~~~~~~~ ^                 
          |        auto                           -> const char*
  • src/captiveportal/CaptivePortalInstance.cpp:86:10: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

       86 |   static char hash[65];
          |          ^
  • src/captiveportal/CaptivePortalInstance.cpp:86:20: warning: [cppcoreguidelines-avoid-magic-numbers]

    65 is a magic number; consider replacing it with a named constant

       86 |   static char hash[65];
          |                    ^
  • src/captiveportal/CaptivePortalInstance.cpp:91:10: warning: [cppcoreguidelines-pro-bounds-array-to-pointer-decay]

    do not implicitly decay an array into a pointer; consider using gsl::array_view or an explicit cast instead

       91 |   return hash;
          |          ^
  • src/captiveportal/CaptivePortalInstance.cpp:98:5: warning: [readability-redundant-member-init]

    initializer for member 'm_fileSystem' is redundant

       98 |   , m_fileSystem()
          |     ^~~~~~~~~~~~~~
  • src/captiveportal/CaptivePortalInstance.cpp:108:8: warning: [cppcoreguidelines-init-variables]

    variable 'dnsStarted' is not initialized

      108 |   bool dnsStarted = m_dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
          |        ^                                      
          |                                                = false
  • src/captiveportal/CaptivePortalInstance.cpp:125:47: warning: [cppcoreguidelines-avoid-magic-numbers]

    10U is a magic number; consider replacing it with a named constant

      125 |     if (!m_fileSystem.begin(false, "/static", 10U, "static0")) {
          |                                               ^
  • src/captiveportal/CaptivePortalInstance.cpp:504:105: warning: [cppcoreguidelines-avoid-magic-numbers]

    8192 is a magic number; consider replacing it with a named constant

      504 |     if (TaskUtils::TaskCreateExpensive(Util::FnProxy<&CaptivePortal::CaptivePortalInstance::task>, TAG, 8192, this, 1, &m_taskHandle) != pdPASS) {  // PROFILED: 4-6KB stack usage
          |                                                                                                         ^
  • src/captiveportal/CaptivePortalInstance.cpp:523:44: warning: [readability-convert-member-functions-to-static]

    method 'task' can be made static

      523 | void CaptivePortal::CaptivePortalInstance::task()
          |                                            ^
  • src/captiveportal/CaptivePortalInstance.cpp:533:44: warning: [readability-convert-member-functions-to-static]

    method 'handleWebSocketClientConnected' can be made static

      533 | void CaptivePortal::CaptivePortalInstance::handleWebSocketClientConnected(uint8_t socketId)
          |                                            ^
          | static 
  • src/captiveportal/CaptivePortalInstance.cpp:551:44: warning: [readability-convert-member-functions-to-static]

    method 'handleWebSocketClientDisconnected' can be made static

      551 | void CaptivePortal::CaptivePortalInstance::handleWebSocketClientDisconnected(uint8_t socketId)
          |                                            ^
          | static 
  • src/captiveportal/CaptivePortalInstance.cpp:556:44: warning: [readability-convert-member-functions-to-static]

    method 'handleWebSocketEvent' can be made static

      556 | void CaptivePortal::CaptivePortalInstance::handleWebSocketEvent(uint8_t socketId, WebSocketMessageType type, tcb::span<const uint8_t> payload)
          |                                            ^
          | static 
  • src/captiveportal/CaptivePortalInstance.cpp:559:5: warning: [bugprone-branch-clone]

    switch has 2 consecutive identical branches

      559 |     case WebSocketMessageType::Connected:
          |     ^
    src/captiveportal/CaptivePortalInstance.cpp:564:12: note: last of these clones ends here
      564 |       break;
          |            ^
  • src/config/OtaUpdateConfig.cpp:10:1: warning: [cppcoreguidelines-pro-type-member-init]

    constructor does not initialize these fields: repoDomain, checkInterval, updateId

       10 | OtaUpdateConfig::OtaUpdateConfig()
          | ^
  • src/config/OtaUpdateConfig.cpp:24:1: warning: [cppcoreguidelines-pro-type-member-init]

    constructor does not initialize these fields: repoDomain, checkInterval, updateId

       24 | OtaUpdateConfig::OtaUpdateConfig(
          | ^
  • src/config/OtaUpdateConfig.cpp:54:23: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       54 | bool OtaUpdateConfig::FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config)
          | ~~~~                  ^                                                                           
          | auto                                                                                               -> bool
  • src/config/OtaUpdateConfig.cpp:76:96: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       76 | flatbuffers::Offset<OpenShock::Serialization::Configuration::OtaUpdateConfig> OtaUpdateConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const
          | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                  ^                                                                                   
          | auto                                                                                                                                                                                -> flatbuffers::Offset<OpenShock::Serialization::Configuration::OtaUpdateConfig>
  • src/config/OtaUpdateConfig.cpp:81:23: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       81 | bool OtaUpdateConfig::FromJSON(const cJSON* json)
          | ~~~~                  ^                          
          | auto                                              -> bool
  • src/config/OtaUpdateConfig.cpp:89:8: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       89 |   if (!cJSON_IsObject(json)) {
          |       ~^                   
          |                             == 0
  • src/config/OtaUpdateConfig.cpp:99:70: warning: [cppcoreguidelines-avoid-magic-numbers]

    30 is a magic number; consider replacing it with a named constant

       99 |   Internal::Utils::FromJsonU16(checkInterval, json, "checkInterval", 30);
          |                                                                      ^
  • src/config/OtaUpdateConfig.cpp:108:25: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

      108 | cJSON* OtaUpdateConfig::ToJSON(bool withSensitiveData) const
          | ~~~~~~                  ^                                   
          | auto                                                         -> cJSON*
  • src/config/OtaUpdateConfig.cpp:112:44: warning: [readability-implicit-bool-conversion]

    implicit conversion 'bool' -> 'cJSON_bool' (aka 'int')

      112 |   cJSON_AddBoolToObject(root, "isEnabled", isEnabled);
          |                                            ^        
          |                                            static_cast<cJSON_bool>( )
  • src/config/OtaUpdateConfig.cpp:115:49: warning: [readability-implicit-bool-conversion]

    implicit conversion 'bool' -> 'cJSON_bool' (aka 'int')

      115 |   cJSON_AddBoolToObject(root, "checkOnStartup", checkOnStartup);
          |                                                 ^             
          |                                                 static_cast<cJSON_bool>( )
  • src/config/OtaUpdateConfig.cpp:116:52: warning: [readability-implicit-bool-conversion]

    implicit conversion 'bool' -> 'cJSON_bool' (aka 'int')

      116 |   cJSON_AddBoolToObject(root, "checkPeriodically", checkPeriodically);
          |                                                    ^                
          |                                                    static_cast<cJSON_bool>( )
  • src/config/OtaUpdateConfig.cpp:118:57: warning: [readability-implicit-bool-conversion]

    implicit conversion 'bool' -> 'cJSON_bool' (aka 'int')

      118 |   cJSON_AddBoolToObject(root, "allowBackendManagement", allowBackendManagement);
          |                                                         ^                     
          |                                                         static_cast<cJSON_bool>( )
  • src/config/OtaUpdateConfig.cpp:119:56: warning: [readability-implicit-bool-conversion]

    implicit conversion 'bool' -> 'cJSON_bool' (aka 'int')

      119 |   cJSON_AddBoolToObject(root, "requireManualApproval", requireManualApproval);
          |                                                        ^                    
          |                                                        static_cast<cJSON_bool>( )
  • src/http/FirmwareCDN.cpp:18:13: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       18 | static bool parseLatestResponse(const std::string& jsonStr, OpenShock::SemVer& version, FirmwareReleaseInfo& release)
          |        ~~~~ ^                                                                                                        
          |        auto                                                                                                           -> bool
  • src/http/FirmwareCDN.cpp:28:8: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       28 |   if (!cJSON_IsString(versionObj) || versionObj->valuestring == nullptr) {
          |       ~^
          |       (                           == 0)
  • src/http/FirmwareCDN.cpp:42:8: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       42 |   if (!cJSON_IsObject(artifacts)) {
          |       ~^                        
          |                                  == 0
  • src/http/FirmwareCDN.cpp:49:8: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       49 |   if (!cJSON_IsArray(boardArtifacts)) {
          |       ~^                            
          |                                      == 0
  • src/http/FirmwareCDN.cpp:55:3: warning: [readability-isolate-declaration]

    multiple declarations in a single statement reduces readability

       55 |   bool foundApp = false, foundStaticFs = false;
          |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • src/http/FirmwareCDN.cpp:59:10: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       59 |     if (!cJSON_IsObject(artifact)) {
          |         ~^                       
          |                                   == 0
  • src/http/FirmwareCDN.cpp:67:10: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       67 |     if (!cJSON_IsString(typeObj) || !cJSON_IsString(urlObj) || !cJSON_IsString(hashObj)) {
          |         ~^
          |         (                        == 0)
  • src/http/FirmwareCDN.cpp:67:38: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       67 |     if (!cJSON_IsString(typeObj) || !cJSON_IsString(urlObj) || !cJSON_IsString(hashObj)) {
          |                                     ~^
          |                                     (                       == 0)
  • src/http/FirmwareCDN.cpp:67:65: warning: [readability-implicit-bool-conversion]

    implicit conversion 'cJSON_bool' (aka 'int') -> 'bool'

       67 |     if (!cJSON_IsString(typeObj) || !cJSON_IsString(urlObj) || !cJSON_IsString(hashObj)) {
          |                                                                ~^                      
          |                                                                (                        == 0)
  • src/http/FirmwareCDN.cpp:77:39: warning: [cppcoreguidelines-avoid-magic-numbers]

    64 is a magic number; consider replacing it with a named constant

       77 |       if (HexUtils::TryParseHex(hash, 64, release.appBinaryHash, 32) != 32) {
          |                                       ^
  • src/http/FirmwareCDN.cpp:77:66: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       77 |       if (HexUtils::TryParseHex(hash, 64, release.appBinaryHash, 32) != 32) {
          |                                                                  ^
  • src/http/FirmwareCDN.cpp:77:73: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       77 |       if (HexUtils::TryParseHex(hash, 64, release.appBinaryHash, 32) != 32) {
          |                                                                         ^
  • src/http/FirmwareCDN.cpp:85:39: warning: [cppcoreguidelines-avoid-magic-numbers]

    64 is a magic number; consider replacing it with a named constant

       85 |       if (HexUtils::TryParseHex(hash, 64, release.filesystemBinaryHash, 32) != 32) {
          |                                       ^
  • src/http/FirmwareCDN.cpp:85:73: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       85 |       if (HexUtils::TryParseHex(hash, 64, release.filesystemBinaryHash, 32) != 32) {
          |                                                                         ^
  • src/http/FirmwareCDN.cpp:85:80: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       85 |       if (HexUtils::TryParseHex(hash, 64, release.filesystemBinaryHash, 32) != 32) {
          |                                                                                ^
  • src/http/FirmwareCDN.cpp:109:69: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

      109 | HTTP::Response<HTTP::FirmwareCDN::LatestRelease> HTTP::FirmwareCDN::GetLatestRelease(OtaUpdateChannel channel, const std::string& repoDomain)
          | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                    ^                                                                        
          | auto                                                                                                                                          -> HTTP::Response<HTTP::FirmwareCDN::LatestRelease>
  • src/http/FirmwareCDN.cpp:111:15: warning: [cppcoreguidelines-init-variables]

    variable 'channelStr' is not initialized

      111 |   const char* channelStr;
          |               ^         
          |                          = nullptr
  • src/http/FirmwareCDN.cpp:137:40: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

      137 |   static const uint16_t s_acceptedCodes[] = {200, 304};
          |                                        ^
  • src/http/FirmwareCDN.cpp:161:56: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

      161 | HTTP::Response<FirmwareReleaseInfo> HTTP::FirmwareCDN::GetRelease(const OpenShock::SemVer& version, const std::string& repoDomain)
          | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                    ^                                                                          
          | auto                                                                                                                               -> HTTP::Response<FirmwareReleaseInfo>
  • src/http/FirmwareCDN.cpp:175:40: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

      175 |   static const uint16_t s_acceptedCodes[] = {200, 304};
          |                                        ^
  • src/main.cpp:25:6: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       25 | bool trySetup()
          | ~~~~ ^         
          | auto            -> bool
  • src/main.cpp:95:19: warning: [cppcoreguidelines-avoid-magic-numbers]

    115'200 is a magic number; consider replacing it with a named constant

       95 |   OS_SERIAL.begin(115'200);
          |                   ^
  • src/main.cpp:98:23: warning: [cppcoreguidelines-avoid-magic-numbers]

    115'200 is a magic number; consider replacing it with a named constant

       98 |   OS_SERIAL_USB.begin(115'200);
          |                       ^
  • src/main.cpp:123:16: warning: [cppcoreguidelines-avoid-magic-numbers]

    5 is a magic number; consider replacing it with a named constant

      123 |     vTaskDelay(5);  // 5 ticks update interval
          |                ^
  • src/main.cpp:130:71: warning: [cppcoreguidelines-avoid-magic-numbers]

    8192 is a magic number; consider replacing it with a named constant

      130 |   if (OpenShock::TaskUtils::TaskCreateExpensive(main_app, "main_app", 8192, nullptr, 1, nullptr) != pdPASS) {  // PROFILED: 6KB stack usage
          |                                                                       ^
  • src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp:15:3: warning: [readability-qualified-auto]

    'auto msg' can be declared as 'const auto *msg'

       15 |   auto msg = root->payload_as_OtaUpdateRequest();
          |   ^~~~
          |   const auto *
  • src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp:21:3: warning: [readability-qualified-auto]

    'auto semver' can be declared as 'const auto *semver'

       21 |   auto semver = msg->version();
          |   ^~~~
          |   const auto *
  • src/ota/OtaUpdateClient.cpp:29:13: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       29 | static bool sendProgressMessage(Serialization::Types::OtaUpdateProgressTask task, float progress)
          |        ~~~~ ^                                                                                    
          |        auto                                                                                       -> bool
  • src/ota/OtaUpdateClient.cpp:45:13: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       45 | static bool sendFailureMessage(std::string_view message, bool fatal = false)
          |        ~~~~ ^                                                               
          |        auto                                                                  -> bool
  • src/ota/OtaUpdateClient.cpp:61:13: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       61 | static bool flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32])
          |        ~~~~ ^                                                                                                               
          |        auto                                                                                                                  -> bool
  • src/ota/OtaUpdateClient.cpp:61:120: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

       61 | static bool flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32])
          |                                                                                                                        ^
  • src/ota/OtaUpdateClient.cpp:61:121: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       61 | static bool flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32])
          |                                                                                                                         ^
  • src/ota/OtaUpdateClient.cpp:65:94: warning: [readability-uppercase-literal-suffix]

    floating point literal has suffix 'f', which is not uppercase

       65 |   if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FlashingApplication, 0.0f)) {
          |                                                                                              ^  ~
          |                                                                                                 F
  • src/ota/OtaUpdateClient.cpp:69:24: warning: [bugprone-easily-swappable-parameters]

    3 adjacent parameters of 'operator()' of convertible types are easily swapped by mistake

       69 |   auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool {
          |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    src/ota/OtaUpdateClient.cpp:69:36: note: the first parameter in the range is 'current'
       69 |   auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool {
          |                                    ^~~~~~~
    src/ota/OtaUpdateClient.cpp:69:70: note: the last parameter in the range is 'progress'
       69 |   auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool {
          |                                                                      ^~~~~~~~
    src/ota/OtaUpdateClient.cpp:69:64: note: 'int' and 'float' may be implicitly converted
       69 |   auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool {
          |                                                                ^
  • src/ota/OtaUpdateClient.cpp:70:89: warning: [readability-uppercase-literal-suffix]

    floating point literal has suffix 'f', which is not uppercase

       70 |     OS_LOGD(TAG, "Flashing app partition: %u / %u (%.2f%%)", current, total, progress * 100.0f);
          |                                                                                         ^    ~
          |                                                                                              F
  • src/ota/OtaUpdateClient.cpp:81:101: warning: [readability-uppercase-literal-suffix]

    floating point literal has suffix 'f', which is not uppercase

       81 |   if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::MarkingApplicationBootable, 0.0f)) {
          |                                                                                                     ^  ~
          |                                                                                                        F
  • src/ota/OtaUpdateClient.cpp:94:13: warning: [modernize-use-trailing-return-type]

    use a trailing return type for this function

       94 | static bool flashFilesystemPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32])
          |        ~~~~ ^                                                                                                                      
          |        auto                                                                                                                         -> bool
  • src/ota/OtaUpdateClient.cpp:94:127: warning: [cppcoreguidelines-avoid-c-arrays]

    do not declare C-style arrays, use 'std::array' instead

       94 | static bool flashFilesystemPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32])
          |                                                                                                                               ^
  • src/ota/OtaUpdateClient.cpp:94:128: warning: [cppcoreguidelines-avoid-magic-numbers]

    32 is a magic number; consider replacing it with a named constant

       94 | static bool flashFilesystemPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32])
          |                                                                                                                                ^
  • src/ota/OtaUpdateClient.cpp:96:93: warning: [readability-uppercase-literal-suffix]

    floating point literal has suffix 'f', which is not uppercase

       96 |   if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::PreparingForUpdate, 0.0f)) {
          |                                                                                             ^  ~
          |                                                                                                F
  • src/ota/OtaUpdateClient.cpp:101:34: warning: [cppcoreguidelines-avoid-magic-numbers]

    5000U is a magic number; consider replacing it with a named constant

      101 |   if (!CaptivePortal::ForceClose(5000U)) {
          |                                  ^

Have any feedback or feature suggestions? Share it here.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hhvrc hhvrc moved this from Todo to In Review in Roadmap Mar 26, 2026
@hhvrc hhvrc changed the title refactor: Clean up OTA update manager refactor(ota): split OTA into Manager, Client, and FirmwareCDN Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

1 participant