[
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: ci\non:\n  push:\n    branches:\n      - master\n      - main\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: 3.x\n      - run: echo \"cache_id=$(date --utc '+%V')\" >> $GITHUB_ENV\n      - uses: actions/cache@v3\n        with:\n          key: mkdocs-material-${{ env.cache_id }}\n          path: .cache\n          restore-keys: |\n            mkdocs-material-\n      - run: pip install mkdocs-material\n      - run: mkdocs gh-deploy --force\n"
  },
  {
    "path": ".gitignore",
    "content": ".pio\n.clang_complete\n.gcc-flags.json\n*Thumbs.db\n/data/www\n/interface/build\n/interface/node_modules\n/interface/.eslintcache\n.vscode\nnode_modules\n/releases\n/src/certs\n/temp\n/build\n/lib/framework/WWWData.h\n*WWWData.h\nlib/framework/WWWData.h\nssl_certs/cacert.pem\n/logs\n.aid\n/scripts/__pycache__\n.DS_Store\n*.bak\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [WIP] - Next Release\n\n### Added\n\n- Add originID to StateUpdateResult update [#110](https://github.com/theelims/ESP32-sveltekit/pull/110)\n- Add originID to StateUpdateResult update [#110](https://github.com/theelims/ESP32-sveltekit/pull/110)\n- Ethernet Support [#113](https://github.com/theelims/ESP32-sveltekit/pull/113)\n\n### Changed\n\n- Changed the width of the confirm dialog.\n- SvelteKit bundling as single files to reduce heap consumption.\n- Rework of firmware upload [#107](https://github.com/theelims/ESP32-sveltekit/pull/107)\n\n### Fixes\n\n- WiFi reconnection issues [#109](https://github.com/theelims/ESP32-sveltekit/issues/109)\n- Blurred toast notifications [#114](https://github.com/theelims/ESP32-sveltekit/issues/114)\n\n## [0.6.0] - 2025-11-03\n\n> [!CAUTION]\n> This update has breaking changes!\n\n### Added\n\n- Added `GEMINI.md` to store notes about the repository for the Gemini CLI. We are now using the Gemini CLI for development.\n- Added a build script to create a merged firmware file to use with [ESP Web Tools](https://esphome.github.io/esp-web-tools/)\n- Added compatibility with ESP32-C6\n- Added getIP() function to WiFiSettingsService.\n- Added Arduino Log Colors\n- Possibility to add a loop callback to ESP32-Sveltekit to leverage its loop threat. Meant to include custom services so no separate task is needed for them.\n- Change wake-up pin in SleepService during runtime. It is also possible to use the internal pull-up or pull-down resistors now.\n- Get current connection status from ESP32-SvelteKit. Useful for status LED or displays.\n- Battery history graph to gauge battery consumption and device life.\n- Add a status topic (`online` or `offline`) to the MQTT client. It retains its message and sends `offline` as last will and testament, signalling all subscribers when it goes missing.\n- FeatureService sends updates through the event system.\n- WiFiSettingsService can set the WiFi station mode to offline, without deleting the list of networks.\n- Expands menu on selected subitem [#77](https://github.com/theelims/ESP32-sveltekit/pull/77)\n- Refactor System Status and Metrics, added PSRAM [#79](https://github.com/theelims/ESP32-sveltekit/pull/79)\n- Add /rest/coreDump endpoint [#87](https://github.com/theelims/ESP32-sveltekit/pull/87) & [#94](https://github.com/theelims/ESP32-sveltekit/pull/94)\n- Rate limiting for MQTT publish messages. Can be configured as factory setting or at runtime. `0` will disable the rate limiting.\n- Added [discord](https://discord.gg/MTn9mVUG5n) invite to readme.md and docs.\n- Created DraggableList component based on svelte-dnd-action (used in WiFi Settings) [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Extended Collapsible and SettingsCard components to support a dirty status [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Enhanced ConfirmDialog, InfoDialog and Toast components to support HTML content [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Added connection check to WebSocket store (socket.ts) and allow secure WebSocket connections [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Added a delayed reconnect function to WifiSettingsService.cpp to allow the last POST request from frontend (providing new network settings) to be properly responded to. In the previous implementation, the POST request was never responded to by the backend, as the connection was closed immediately upon receiving the request in the backend. This resulted in the user receiving no feedback about whether the settings update was successful, only a timeout that suggested something had gone wrong. [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Added scripts/prebuild_utils.py that allows other build scripts to be executed depending on the type of the executed task (e.g. I don't want to have the interface built or the certificate bundle created on clean tasks) [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Added build flag `-D TELEPLOT_TASKS` to plot task heap high water mark with teleplot. You can include this in your tasks as well:\n\n```cpp\n#ifdef TELEPLOT_TASKS\n        static int lastTime = 0;\n        if (millis() - lastTime > 1000)\n        {\n            lastTime = millis();\n            Serial.printf(\">ESP32SveltekitTask:%i:%i\\n\", millis(), uxTaskGetStackHighWaterMark(NULL));\n        }\n#endif\n```\n\n### Changed\n\n- Lightstate example uses simpler, less explicit constructor\n- MQTT library updated\n- Analytics task was refactored into a loop() function which is called by the ESP32-sveltekit main task.\n- Updated PsychicHttp to v1.2.1 incl. patches.\n- Updated to DaisyUI 5 and Tailwind CSS 4\n- Updated Svelte 5 --> see [Svelte 5 Migration Guide](https://svelte.dev/docs/svelte/v5-migration-guide)\n- Changed platform to [PIO Arduino](https://github.com/pioarduino/platform-espressif32) using Arduino 3 Core. Also upgrades ESP-IDF to v5.\n- ESPD_LOGx: replace first argument with TAG and define TAG as 🐼 [#85](https://github.com/theelims/ESP32-sveltekit/pull/85)\n- Replace rtc_get_reset_reason(0) with esp_reset_reason() [#86](https://github.com/theelims/ESP32-sveltekit/pull/86)\n- Default build_interface.py script to npm, if no lock file is found.\n- Replaced `svelte-dnd-list` with `svelte-dnd-action` as `svelte-dnd-list` creates build warnings and appears to no longer be maintained, while svelte-dnd-action is under active community development. [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Made Spinner component more flexible (to allow other texts than \"Loading...\" or no text at all) [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Reworked WiFi Settings (Station) dialog: added edit dialog for networks, rearranged UI components, used new DraggableList component [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n\n### Fixed\n\n- Ensure thread safety for client subscriptions [#58](https://github.com/theelims/ESP32-sveltekit/pull/58)\n- Deferred websocket event connection to after user validation & login [#72](https://github.com/theelims/ESP32-sveltekit/pull/72)\n- Wrong return type battery service\n- Wrong return types in various getService functions.\n- Add file.close in fileHandler handleRequest [#73](https://github.com/theelims/ESP32-sveltekit/pull/73)\n- Fixed bug in WiFiSettingsService preventing discovery of networks other than the first\n- Fixed mixup pull up and pull down when configuring wake up pin in SleepService.cpp\n- Wifi: Multiple edits bug resolved [#79](https://github.com/theelims/ESP32-sveltekit/pull/79)\n- Fixed broken link to Adafruit SSL Cert Store [#93](https://github.com/theelims/ESP32-sveltekit/issues/93)\n- Fixed JSON creation in WiFiSettingsService.h [#91](https://github.com/theelims/ESP32-sveltekit/pull/91)\n- Fixed preprocessor warning: usage of #ifdef with OR operator [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Fixed preprocessor warning: redefinition of ESP_PLATFORM [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Fixed deprecated usage of merge-bin and its parameters in scripts/merge_bin.py [#100](https://github.com/theelims/ESP32-sveltekit/pull/100)\n- Fixed Download OTA. Issues with certificate validation might remain, but build flag `-D DOWNLOAD_OTA_SKIP_CERT_VERIFY` allows to circumvent issue by sacrificing certificate validation.\n\n### Removed\n\n- Removed async workers in PsychicHttp, as these were not used, but caused linker errors.\n\n### Depreciate\n\n- Support for ESP Arduino 2 and ESP-IDF v4 will depreciate some time in the future. Try to migrate to the current Arduino 3 / ESP-IDF v5 based branch.\n\n### Migration Guide\n\n#### PIO Arduino & ESP-IDF 5\n\nThe firmware is based on the community maintained fork [PIO Arduino](https://github.com/pioarduino/platform-espressif32) of Arduino 3 for ESP32. Which is based on ESP-IDF 5. Please make sure all your dependencies and application code is compatible with ESP-IDF 5.\n\n#### Frontend\n\nSvelteKit was updated to v2 and Svelte to v5. Please check the migration guides for [SvelteKit 2](https://svelte.dev/docs/kit/migrating-to-sveltekit-2) and the [Svelte 5 Migration Guide](https://svelte.dev/docs/svelte/v5-migration-guide) for the changes required on your frontend code.\n\nTo migrate your frontend run\n\n```\nnpm install --force\nnpx sv migrate svelte-5\n```\n\nAlso DaisyUI and Tailwind CSS have been updated to their last major versions. Run the official Tailwind upgrade tool:\n\n```\nnpx @tailwindcss/upgrade\n```\n\nThis will migrate some of your svelte files to the new naming convention of Tailwind. For DaisyUI follow this [guide](https://daisyui.com/docs/upgrade/#changes-from-v4). Likely you'll need to redo all forms, as the components behave differently. Forms will need the `fieldset` class. Inputs will need an additional `w-full` to have the same behavior as before. And [labels](https://daisyui.com/components/label/) have a different syntax, too.\n\nThe themes are to be found in `app.css` now. Add them back if they had been changed from the default.\n\n### Acknowledgment\n\nMany thanks to @runeharlyk, @ewowi, @hmbacher, and @stamp who contributed significantly to this new release.\n\n## [0.5.0] - 2024-05-06\n\nChanges the Event Socket System to use a clearer message structure and MessagePack. Brings breaking changes to the `EventSocket.h` API.\n\nUpdated daisyUI to v4. This has changes in the colors and switches to OKLCH. Also button groups and input groups have been depreciated in favor of join. This might require changes to custom parts of the code. Please double check all websites if the still have the desired looks.\n\nUpdates ArduinoJSON from v6 to v7 to increase the available free heap. If you make use of ArduinoJSON, changes might be required.\n\n### Added\n\n- Debug buildflag to switch between MessagePack and JSON for event messages.\n- Show SSID of the current WiFi Station as tooltip of the RSSI icon.\n\n### Changed\n\n- Moved MQTT types to models.ts as well. [#49](https://github.com/theelims/ESP32-sveltekit/pull/49)\n- Updated daisyUI to 4.10.2 [#48](https://github.com/theelims/ESP32-sveltekit/pull/48)\n- Fixed spelling error in models.ts\n- Changed ArduinoJson from v6 to v7 increasing the free heap by ~40kb\n- Split NotificationService out of EventSocket into own class\n- Changed API of EventSocket.h. Now uses `void emitEvent(String event, JsonObject &jsonObject, const char *originId = \"\", bool onlyToSameOrigin = false);`.\n- Changed event socket message format to MessagePack\n\n### Fixed\n\n- Fixes to WiFi.svelte and models.ts to fix type errors and visibility rights.\n- Fixes bug in highlighting the menu when navigating with the browser (back/forward)\n- Made WiFi connection routine more robust by using BSSID. Ensures that the STA truly connects to the strongest hotspot, even if several hotspots are in reach.\n\n### Removed\n\n- Removed duplicate in ESP32SvelteKit.cpp [#47](https://github.com/theelims/ESP32-sveltekit/pull/47) and WiFi.svelte [#50](https://github.com/theelims/ESP32-sveltekit/pull/50)\n\n### Acknowledgment\n\nMany thanks to @runeharlyk who contributed significantly to the new event socket system and fixed many smaller issues with the front-end.\n\n## [0.4.0] - 2024-04-21\n\nThis upgrade might require one minor change as `MqttPubSub.h` and its class had been renamed to `MqttEndpoint.h` and `MqttEndoint` respectively. However, it is strongly advised, that you change all existing WebSocketServer endpoints to the new event socket system.\n\n> [!NOTE]\n> The new Event Socket system is likely to change with coming updates.\n\n### Added\n\n- Added build flag `-D SERIAL_INFO` to platformio.ini to enable / disable all `Serial.print()` statements. On some boards with native USB those Serial prints have been reported to block and make the server unresponsive.\n- Added a hook handler to StatefulService. Unlike an UPDATE a hook is called every time a state receives an updated, even if the result is UNCHANGED or ERROR.\n- Added missing include for S2 in SystemStatus.cpp [#23](https://github.com/theelims/ESP32-sveltekit/issues/23)\n- Added awareness of front end build script for all 3 major JS package managers. The script will auto-identify the package manager by the lock-file. [#40](https://github.com/theelims/ESP32-sveltekit/pull/40)\n- Added a new event socket to bundle the websocket server and the notifications events. This saves on open sockets and allows for concurrent visitors of the internal website. The normal websocket server endpoint remains as an option, should a pure websocket connection be desired. An EventEndpoint was added to use this with Stateful Services. [#29](https://github.com/theelims/ESP32-sveltekit/issues/29) and [#43](https://github.com/theelims/ESP32-sveltekit/pull/43)\n- TS Types definition in one central place for the frontend.\n\n### Changed\n\n- more generic board definition in platformio.ini [#20](https://github.com/theelims/ESP32-sveltekit/pull/20)\n- Renamed `MqttPubSub.h` and class to `MqttEndpoint.h` and class.\n- refactored MqttEndpoint.h into a single class to improve readability\n- Moves appName and copyright to `layout.ts` to keep customization in one place [#31](https://github.com/theelims/ESP32-sveltekit/pull/31)\n- Make event source use timeout for reconnect [#34](https://github.com/theelims/ESP32-sveltekit/pull/34)\n- Make each toasts disappear after timeout [#35](https://github.com/theelims/ESP32-sveltekit/pull/35)\n- Fixed version `platform = espressif32 @ 6.6.0` in platformio.ini\n- Analytics data limited to 1000 data points (roughly 33 minutes).\n- postcss.config.cjs as ESM module [#24](https://github.com/theelims/ESP32-sveltekit/issues/24)\n\n### Fixed\n\n- Fixed compile error with FLAG `-D SERVE_CONFIG_FILES`\n- Fixed typo in telemetry.ts [#38](https://github.com/theelims/ESP32-sveltekit/pull/38)\n- Fixed the development warning: `Loading /rest/features using 'window.fetch'. For best results, use the 'fetch' that is passed to your 'load' function:`\n\n### Removed\n\n- Duplicate method in FeatureService [#18](https://github.com/theelims/ESP32-sveltekit/pull/18)\n- Duplicate lines in Systems Settings view.\n- Removes duplicate begin [#36](https://github.com/theelims/ESP32-sveltekit/pull/36)\n- Temporary disabled OTA progress update due to crash with PsychicHttp [#32](https://github.com/theelims/ESP32-sveltekit/issues/32) until a fix is found.\n\n### Known Issues\n\n- On ESP32-C3 the security features should be disabled in features.ini: `-D FT_SECURITY=0`. If enabled the ESP32-C3 becomes extremely sluggish with frequent connection drops.\n\n## [0.3.0] - 2024-02-05\n\n> [!CAUTION]\n> This update has breaking changes!\n\nThis is a major change getting rid of all ESPAsyncTCP and ESPAsyncWebserver dependencies. Despite their popularity they are plagued with countless bugs, since years unmaintained, not SSL capable and simply not suitable for a production build. Although several attempts exist to fix the most pressing bugs even these libraries lead to frequent crashes. This new version replaces them with ESP-IDF based components. [PsychicHttp](https://github.com/hoeken/PsychicHttp) and [PsychicMqttClient](https://github.com/theelims/PsychicMqttClient) both wrap the ESP-IDF components in a familiar wrapper for easy porting of the code base. However, this will break existing code and will require some effort on your codebase. In return the stability is improved greatly and the RAM usage more friendly. Now e.g. running Bluetooth in parallel becomes possible.\n\n### Added\n\n- Added postscript to platform.io build process to copy, rename and calculate MD5 checksum of \\*.bin file. These files are ready for uploading to the Github Release page.\n- Added more information to SystemStatus API\n- Added generateToken API for security settings\n- Added Multi-WiFi capability. Add up to five WiFi configurations and connect to either strongest network (default), or by priority.\n- Added InfoDialog as a simpler version of the ConfirmDialog for a simple notification modal.\n- Added Adafruit certificate repository as the default choice for the X509 certificate bundle.\n\n### Changed\n\n- Better route protection for user page with deep link.\n- Changed build_interface.py script to check for modified files in the interface sources before re-building the interface. Saves some time on the compilation process.\n- Upload firmware binary allows uploading of MD5 checksum file in advance to verify downloaded firmware package.\n- GithubFirmwareManager checks against PIO build_target in filename to support Github OTA for binaries build for various targets. You should rename your old release \\*.bin files on the Github release pages for backward compatibility.\n- Changed MQTT Client to an ESP-IDF backed one which supports SSL/TLS X509 root CA bundles and transport over WS.\n- Changed the `PROGMEM_WWW` flag to `EMBED_WWW` as there is technically speaking no PROGMEM on ESP32's.\n- Updated dependencies to the latest version. Except SvelteKit.\n\n### Fixed\n\n- Fixed reactivity of System Status page.\n\n### Removed\n\n- Removed support for Arduino ESP OTA.\n- HttpEndpoints and Websocket Server without a securityManager are no longer possible.\n\n### Migrate from ESPAsyncWebServer to PsychicHttp\n\n#### Migrate `main.cpp`\n\nChange the server and ESPSvelteKit instances to PsychicHttpServer and give the ESP32SvelteKit constructor the number of http endpoints of your project.\n\n```\nPsychicHttpServer server;\nESP32SvelteKit esp32sveltekit(&server, 120);\n```\n\nRemove `server.begin();` in `void setup()`. This is handled by ESP32SvelteKit now.\n\n#### Migrate `platformio.ini`\n\nRemove the following `build_flags`:\n\n```ini\n    ; Increase queue size of SSE and WS\n    -D SSE_MAX_QUEUED_MESSAGES=64\n    -D WS_MAX_QUEUED_MESSAGES=64\n    -D CONFIG_ASYNC_TCP_RUNNING_CORE=0\n    -D NO_GLOBAL_ARDUINOOTA\n    -D PROGMEM_WWW\n```\n\nAdd the following `build_flags` and adjust to your app, if needed:\n\n```ini\n    -D BUILD_TARGET=\\\"$PIOENV\\\"\n    -D APP_NAME=\\\"ESP32-Sveltekit\\\" ; Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename\n    -D APP_VERSION=\\\"0.3.0\\\" ; semver compatible version string\n    -D EMBED_WWW\n```\n\nRemove the lib dependency `esphome/AsyncTCP-esphome @ ^2.0.0` and add `https://github.com/theelims/PsychicMqttClient.git`\n\nConsider adjusting `board_ssl_cert_source = adafruit`, so that the new MQTT client has universal SSL/TLS support with a wide range of CA root certificates.\n\n#### Migrate `factory_settings.ini`\n\nThe new MQTT client has slightly renamed factory settings:\n\n```ini\n  ; MQTT settings\n  -D FACTORY_MQTT_ENABLED=false\n  -D FACTORY_MQTT_URI=\\\"mqtts://mqtt.eclipseprojects.io:8883\\\"\n  -D FACTORY_MQTT_USERNAME=\\\"\\\" ; supports placeholders\n  -D FACTORY_MQTT_PASSWORD=\\\"\\\"\n  -D FACTORY_MQTT_CLIENT_ID=\\\"#{platform}-#{unique_id}\\\" ; supports placeholders\n  -D FACTORY_MQTT_KEEP_ALIVE=120\n  -D FACTORY_MQTT_CLEAN_SESSION=true\n```\n\nMax Topic Length is no longer needed.\n\n#### Custom Stateful Services\n\nAdapt the class constructor (`(PsychicHttpServer *server, ...`) to PsychicHttpServer.\n\nDue to the loading sequence HttpEndoint and WebsocketServer both have gotten a `begin()` function to register their http endpoints with the server. This must be called in your stateful services' own `begin()` function:\n\n```cpp\nvoid LightStateService::begin()\n{\n    _httpEndpoint.begin();\n    _webSocketServer.begin();\n    _state.ledOn = DEFAULT_LED_STATE;\n    onConfigUpdated();\n}\n```\n\n## [0.2.2] - 2023-10-08\n\n### Added\n\n- Status reports reset-reason & uptime.\n- AnalyticsService to draw graphs about heap usage and other time dependent values\n- Added ping to WebSocket Server\n- Use telemetry store with RSSI messages to gauge health of connection. Automatic reconnect for SSE and WS.\n- Added user supplied features to FeatureService\n- Compiler flag to let it serve the config JSONs for debug purposes\n- Hard fork of ESPAsyncWebserver as it is unmaintained to fix bugs and add features\n\n### Changed\n\n- Changed JSON format for websocket server and removed \"payload\" property. JSON is the same as for MQTT or HTTP now.\n- Changed features.ini to default `FT_SLEEP=0`\n- Updated dependencies to latest version.\n\n## [0.2.1] - 2023-09-11\n\n### Fixed\n\n- Fixed the boot loop issue for Arduino 6.4.0\n\n## [0.2.0] - 2023-08-03\n\n### Added\n\n- Introduced CHANGELOG.md\n- Added core temperature to the system status API\n- Added mDNS / Bonjour / zeroConf for better discoverability in local network\n- Added recovery mode which forces AP to spin up regardless from its settings\n- Added push notification service to show notification toasts on all clients\n- Added SSE to update RSSI in status bar on client\n- Added firmware version to System Status API\n- Added sleep service to send ESP32 into deep sleep. Wake-up with button using EXT1\n- Added battery service to show battery state of charge in the status bar. Uses SSE.\n- Added download firmware manager to pull firmware binaries e.g. from github release pages\n- modified generate_cert_bundle.py from Espressif included into the build process to automatically create SSL Root CA Bundle\n\n### Changed\n\n- Improved system status with more meaningful presentation of available data\n- Improved layout on small screens\n- Increased queue size for SSE and WS to 64 instead of 32\n- ESP32-SvelteKit loop()-function is its own task now\n- ArduinoOTA handle runs in own task now\n- AsyncTCP tasks run on Core 0 to move all networking related stuff to Core 0 and free up Core 1 for business logic\n- Compiler flag on which core ESP32-sveltekit tasks should run\n- Renamed WebSocketRxTx.h to WebSocketServer.h to create a distinction between WS Client and WS Server interfaces\n- Made code of LightStateExample slightly more verbose\n- getServer() returning a pointer to the AsyncWebServer instance.\n- Updated frontend dependencies and packages to newest version.\n\n### Depreciated\n\n- ArduinoOTA feature is set to depreciate. It is unstable with mDNS causing some reset loops until it finally settles.\n\n### Removed\n\n- `FT_PROJECT` feature flag removed.\n\n## [0.1.0] - 2023-05-18\n\nThis is the initial release of ESP32-sveltekit. With this it is feature complete to [rjwats/esp8266-react](https://github.com/rjwats/esp8266-react), where it forked from.\n\n### Added\n\n- Added copyright notes\n\n### Changed\n\n- Renaming into ESP32-sveltekit\n- Small changes to reflect the slightly different file structure of sveltekit\n- Build process for sveltekit\n\n### Removed\n\n- Dropping support for ESP8266\n"
  },
  {
    "path": "ESP32-sveltekit.code-workspace",
    "content": "{\n  \"folders\": [\n    {\n      \"path\": \".\"\n    }\n  ],\n  \"settings\": {\n    \"files.associations\": {\n      \"*.tcc\": \"cpp\",\n      \"algorithm\": \"cpp\",\n      \"esp32-hal-misc.c\": \"cpp\",\n      \"esp_crt_bundle.h\": \"c\",\n      \"functional\": \"cpp\",\n      \"array\": \"cpp\",\n      \"atomic\": \"cpp\",\n      \"bitset\": \"cpp\",\n      \"cctype\": \"cpp\",\n      \"clocale\": \"cpp\",\n      \"cmath\": \"cpp\",\n      \"cstdarg\": \"cpp\",\n      \"cstddef\": \"cpp\",\n      \"cstdint\": \"cpp\",\n      \"cstdio\": \"cpp\",\n      \"cstdlib\": \"cpp\",\n      \"cstring\": \"cpp\",\n      \"ctime\": \"cpp\",\n      \"cwchar\": \"cpp\",\n      \"cwctype\": \"cpp\",\n      \"deque\": \"cpp\",\n      \"list\": \"cpp\",\n      \"unordered_map\": \"cpp\",\n      \"vector\": \"cpp\",\n      \"exception\": \"cpp\",\n      \"iterator\": \"cpp\",\n      \"map\": \"cpp\",\n      \"memory\": \"cpp\",\n      \"memory_resource\": \"cpp\",\n      \"numeric\": \"cpp\",\n      \"optional\": \"cpp\",\n      \"random\": \"cpp\",\n      \"regex\": \"cpp\",\n      \"string\": \"cpp\",\n      \"string_view\": \"cpp\",\n      \"system_error\": \"cpp\",\n      \"tuple\": \"cpp\",\n      \"type_traits\": \"cpp\",\n      \"utility\": \"cpp\",\n      \"fstream\": \"cpp\",\n      \"initializer_list\": \"cpp\",\n      \"iosfwd\": \"cpp\",\n      \"istream\": \"cpp\",\n      \"limits\": \"cpp\",\n      \"new\": \"cpp\",\n      \"ostream\": \"cpp\",\n      \"sstream\": \"cpp\",\n      \"stdexcept\": \"cpp\",\n      \"streambuf\": \"cpp\",\n      \"cinttypes\": \"cpp\",\n      \"typeinfo\": \"cpp\",\n      \"unordered_set\": \"cpp\",\n      \"iomanip\": \"cpp\"\n    }\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "ESP32 SvelteKit is distributed with two licenses for different sections of the\ncode. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3\nand is therefore distributed with said license. The front end code is distributed\nunder the MIT License.\n\nMIT License\n\nCopyright (C) 2023 - 2024 theelims\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\nThis version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n0. Additional Definitions.\n\nAs used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n\"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\nAn \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\nA \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library. The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\nThe \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\nThe \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n1. Exception to Section 3 of the GNU GPL.\n\nYou may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n2. Conveying Modified Versions.\n\nIf you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\na) under this License, provided that you make a good faith effort to\nensure that, in the event an Application does not supply the\nfunction or data, the facility still operates, and performs\nwhatever part of its purpose remains meaningful, or\n\nb) under the GNU GPL, with none of the additional permissions of\nthis License applicable to that copy.\n\n3. Object Code Incorporating Material from Library Header Files.\n\nThe object code form of an Application may incorporate material from\na header file that is part of the Library. You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\na) Give prominent notice with each copy of the object code that the\nLibrary is used in it and that the Library and its use are\ncovered by this License.\n\nb) Accompany the object code with a copy of the GNU GPL and this license\ndocument.\n\n4. Combined Works.\n\nYou may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\na) Give prominent notice with each copy of the Combined Work that\nthe Library is used in it and that the Library and its use are\ncovered by this License.\n\nb) Accompany the Combined Work with a copy of the GNU GPL and this license\ndocument.\n\nc) For a Combined Work that displays copyright notices during\nexecution, include the copyright notice for the Library among\nthese notices, as well as a reference directing the user to the\ncopies of the GNU GPL and this license document.\n\nd) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\ne) Provide Installation Information, but only if you would otherwise\nbe required to provide such information under section 6 of the\nGNU GPL, and only to the extent that such information is\nnecessary to install and execute a modified version of the\nCombined Work produced by recombining or relinking the\nApplication with a modified version of the Linked Version. (If\nyou use option 4d0, the Installation Information must accompany\nthe Minimal Corresponding Source and Corresponding Application\nCode. If you use option 4d1, you must provide the Installation\nInformation in the manner specified by section 6 of the GNU GPL\nfor conveying Corresponding Source.)\n\n5. Combined Libraries.\n\nYou may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\na) Accompany the combined library with a copy of the same work based\non the Library, uncombined with any other library facilities,\nconveyed under the terms of this License.\n\nb) Give prominent notice with the combined library that part of it\nis a work based on the Library, and explaining where to find the\naccompanying uncombined form of the same work.\n\n6. Revised Versions of the GNU Lesser General Public License.\n\nThe Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\nIf the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "README.md",
    "content": "# ESP32 SvelteKit - Create Amazing IoT Projects\n\n<div style=\"flex\">\n<img src=\"/docs/media/Screenshot_light.png\" style=\"height:320px\"> \n<img src=\"/docs/media/Screenshot_mobile.png\" style=\"height:320px\"> \n</div>\n\nA simple and extensible framework for ESP32 based IoT projects with a feature-rich, beautiful, and responsive front-end build with [Sveltekit](https://kit.svelte.dev/), [TailwindCSS](https://tailwindcss.com/) and [DaisyUI](https://daisyui.com/). This is a project template to get you started in no time backed by a powerful back end service, an amazing front end served from the ESP32 and an easy to use build chain to get everything going.\n\nIt was forked from the fabulous [rjwats/esp8266-react](https://github.com/rjwats/esp8266-react) project, from where it inherited the mighty back end services.\n\n> **Tip**: This template repository is not meant to be used stand alone. If you're just looking for a WiFi manager there are plenty of options available. This is a starting point when you need a rich web UI.\n\n## Features\n\n### :butterfly: Beautiful UI powered by DaisyUI and TailwindCSS\n\nBeautiful, responsive UI which works equally well on desktop and on mobile. Gently animated for a snappy and modern feeling without ever being obtrusive or in the way. Easy theming with DaisyUI and media-queries to respect the users wish for a light or dark theme.\n\n### :t-rex: Low Memory Footprint and Easy Customization by Courtesy of SvelteKit\n\nSvelteKit is ideally suited to be served from constrained devices like an ESP32. It's unique approach leads to very slim files. No bloatware like other popular JS frameworks. Not only the low memory footprint make it ideal but the developer experience is also outstanding letting you customize the front end with ease. Adapt and add functionality as you need it. The back end has you covered as well.\n\n### :telephone: Rich Communication Interfaces\n\nComes with a rich set of communication interfaces to cover most standard needs of an IoT application. Like MQTT client, HTTP RESTful API, a WebSocket based Event Socket and a classic Websocket Server. All communication channels are stateful and fully synchronized. Changes propagate and are communicated to all other participants. The states can be persisted on the file system as well. For accurate time keeping time can by synchronized over NTP.\n\n### :file_cabinet: WiFi Provisioning and Management\n\nNaturally ESP32 SvelteKit comes with rich features to manage all your WiFi needs. From pulling up an access point for provisioning or as fall back, to fully manage your WiFi networks. Scan for available networks and connect to them. Advanced configuration options like static IP are on board as well.\n\n### :old_key: Secured API and User Management\n\nManage different user of your app with two authorization levels. An administrator and a guest user. Authenticate their API calls with a JWT token. Manage the user's profile from the admin interface. Use at own risk, as it is neither secure without the ability to use TLS/SSL encryption on the ESP32 server, nor very convenient, as only an admin can change passwords.\n\n### :airplane: OTA Upgrade Service\n\nThe framework can provide two different channels for Over-the-Air updates. Either by uploading a \\*.bin file from the web interface. Or by pulling a firmware image from an update server. This is implemented with the github release page as an example. It is even possible to have different build environments at the same time and the Github OTA process pulls the correct binary.\n\n### :building_construction: Automated Build Chain\n\nThe automated build chain takes out the pain and tears of getting all the bits and pieces play nice together. The repository contains a PlatformIO project at its heart. A SvelteKit project for the frontend code and a mkdocs project for the documentation go alongside. The PlatformIO build tools not only build the SvelteKit frontend with Vite, but also ensure that the build results are gzipped and find their way into the flash memory of the ESP32. You have two choices to serve the frontend either from the flash partition, or embedded into the firmware binary. The latter is much more friendly if your frontend code should be distributed OTA as well, leaving all configuration files intact.\n\n### :icecream: Compatible with all ESP32 Flavours\n\nThe code runs on many variants of the ESP32 chip family. From the plain old ESP32, the ESP32-S3 and ESP32-C3. Other ESP32 variants might work, but haven't been tested. Sorry, no support for the older ESP8266. Go with one of the ESP32's instead.\n\n## Visit the Project Site\n\n[https://theelims.github.io/ESP32-sveltekit/](https://theelims.github.io/ESP32-sveltekit/)\n\n## Join our [Discord](https://discord.gg/MTn9mVUG5n)\n\n## Libraries Used\n\n- [SvelteKit](https://kit.svelte.dev/)\n- [Tailwind CSS](https://tailwindcss.com/)\n- [DaisyUI](https://daisyui.com/)\n- [tabler ICONS](https://tabler-icons.io/)\n- [unplugin-icons](https://github.com/antfu/unplugin-icons)\n- [svelte-modals](https://svelte-modals.mattjennings.io/)\n- [svelte-dnd-action](https://github.com/isaacHagoel/svelte-dnd-action)\n- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)\n- [PsychicHttp](https://github.com/hoeken/PsychicHttp)\n- [PsychicMqttClient](https://github.com/theelims/PsychicMqttClient)\n\n## Licensing\n\nESP32 SvelteKit is distributed with two licenses for different sections of the code. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3 and is therefore distributed with said license. The front end code is distributed under the MIT License. See the [LICENSE](LICENSE) for a full text of both licenses.\n"
  },
  {
    "path": "docs/buildprocess.md",
    "content": "# Build Process\n\nThe build process is controlled by [platformio.ini](https://github.com/theelims/ESP32-sveltekit/platformio.ini) and automates the build of the front end website with Vite as well as the binary compilation for the ESP32 firmware. Whenever PlatformIO is building a new binary it will call the python script [build_interface.py](https://github.com/theelims/ESP32-sveltekit/scripts/build_interface.py) to action. It will check the frontend files for changes. If necessary it will start the Vite build and gzip the resulting files either to the `data/` directory or embed them into a header file. In case the WWW files go into a LITTLEFS partition a file system image for the flash is created for the default build environment and upload to the ESP32.\n\n## Changing the JS package manager\n\nThis project uses NPM as the default package manager. However, many users might have different preferences and like to use YARN or PNPM instead. Just switch the interface to one of the other package managers. The build script identify the package manager by the presence of its lock-file and start the vite build process accordingly.\n\n## Serving from Flash or Embedding into the Binary\n\nThe front end website can be served either from the LITTLEFS partition of the flash, or embedded into the firmware binary (default). Later has the advantage that only one binary needs to be distributed easing the OTA process. Further more this is desirable if you like to preserve the settings stored in the LITTLEFS partition, or have other files there that need to survive a firmware update. To serve from the LITTLEFS partition instead please comment the following build flag out:\n\n```ini\nbuild_flags =\n    ...\n    -D EMBED_WWW\n```\n\n### Partitioning\n\nIf you choose to embed the frontend it becomes part of the firmware binary (default). As many ESP32 modules only come with 4MB built-in flash this results in the binary being too large for the reserved flash. Therefor a partition scheme with a larger section for the executable code is selected. However, this limits the LITTLEFS partition to 200kb. There are a great number of [default partition tables](https://github.com/espressif/arduino-esp32/tree/master/tools/partitions) for Arduino-ESP32 to choose from. If you have 8MB or 16MB flash this would be your first choice. If you don't need OTA you can choose a partition scheme without OTA.\n\nShould you want to deploy the frontend from the flash's LITTLEFS partition on a 4MB chip you need to comment out the following two lines. Otherwise the 200kb will not be large enough to host the front end code.\n\n```ini\nboard_build.partitions = min_spiffs.csv\n```\n\n## Selecting Features\n\nMany of the framework's built in features may be enabled or disabled as required at compile time. This can help save sketch space and memory if your project does not require the full suite of features. The access point and WiFi management features are \"core features\" and are always enabled. Feature selection may be controlled with the build flags defined in [features.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/features.ini).\n\nCustomize the settings as you see fit. A value of 0 will disable the specified feature:\n\n```ini\n  -D FT_SECURITY=1\n  -D FT_MQTT=1\n  -D FT_NTP=1\n  -D FT_UPLOAD_FIRMWARE=1\n  -D FT_DOWNLOAD_FIRMWARE=1\n  -D FT_SLEEP=1\n  -D FT_BATTERY=1\n  -D FT_ETHERNET=1\n```\n\n| Flag                 | Description                                                                                                                                                                                                              |\n| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| FT_SECURITY          | Controls whether the [security features](statefulservice.md#security-features) are enabled. Disabling this means you won't need to authenticate to access the device and all authentication predicates will be bypassed. |\n| FT_MQTT              | Controls whether the MQTT features are enabled. Disable this if your project does not require MQTT support.                                                                                                              |\n| FT_NTP               | Controls whether network time protocol synchronization features are enabled. Disable this if your project does not require accurate time.                                                                                |\n| FT_UPLOAD_FIRMWARE   | Controls whether the manual upload firmware feature is enabled. Disable this if you won't be manually uploading firmware.                                                                                                |\n| FT_DOWNLOAD_FIRMWARE | Controls whether the firmware download feature is enabled. Disable this if you won't firmware pulled from a server.                                                                                                      |\n| FT_SLEEP             | Controls whether the deep sleep feature is enabled. Disable this if your device is not battery operated or you don't need to place it in deep sleep to save energy.                                                      |\n| FT_BATTERY           | Controls whether the battery state of charge shall be reported to the clients. Disable this if your device is not battery operated.                                                                                      |\n| FT_ETHERNET          | Controls whether an ethernet interface will be used. Disable this if your device has no ethernet interface connected.                                                                                      |\n\nIn addition custom features might be added or removed at runtime. See [Custom Features](statefulservice.md#custom-features) on how to use this in your application.\n\n## Factory Settings\n\nThe framework has built-in factory settings which act as default values for the various configurable services where settings are not saved on the file system. These settings can be overridden using the build flags defined in [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini). All strings entered here must be escaped, especially special characters.\n\nCustomize the settings as you see fit, for example you might configure your home WiFi network as the factory default:\n\n```ini\n  -D FACTORY_WIFI_SSID=\\\"My\\ Awesome\\ WiFi\\ Network\\\"\n  -D FACTORY_WIFI_PASSWORD=\\\"secret\\\"\n  -D FACTORY_WIFI_HOSTNAME=\\\"awesome_light_controller\\\"\n```\n\n### Default access point settings\n\nBy default, the factory settings configure the device to bring up an access point on start up which can be used to configure the device:\n\n- SSID: ESP32-Sveltekit\n- Password: esp-sveltekit\n\n### Security settings and user credentials\n\nBy default, the factory settings configure two user accounts with the following credentials:\n\n| Username | Password |\n| -------- | -------- |\n| admin    | admin    |\n| guest    | guest    |\n\nIt is recommended that you change the user credentials from their defaults to better protect your device. You can do this in the user interface, or by modifying [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini) as mentioned above.\n\n### Customizing the factory time zone setting\n\nChanging factory time zone setting is a common requirement. This requires a little effort because the time zone name and POSIX format are stored as separate values for the moment. The time zone names and POSIX formats are contained in the UI code in [timezones.ts](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/connections/timezones.ts). Take the appropriate pair of values from there, for example, for Los Angeles you would use:\n\n```ini\n  -D FACTORY_NTP_TIME_ZONE_LABEL=\\\"America/Los_Angeles\\\"\n  -D FACTORY_NTP_TIME_ZONE_FORMAT=\\\"PST8PDT,M3.2.0,M11.1.0\\\"\n```\n\n### Placeholder substitution\n\nVarious settings support placeholder substitution, indicated by comments in [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini). This can be particularly useful where settings need to be unique, such as the Access Point SSID or MQTT client id. The following placeholders are supported:\n\n| Placeholder  | Substituted value                                                     |\n| ------------ | --------------------------------------------------------------------- |\n| #{platform}  | The microcontroller platform, e.g. \"esp32\" or \"esp32c3\"               |\n| #{unique_id} | A unique identifier derived from the MAC address, e.g. \"0b0a859d6816\" |\n| #{random}    | A random number encoded as a hex string, e.g. \"55722f94\"              |\n\n## Other Build Flags\n\n### Cross-Origin Resource Sharing\n\nIf you need to enable Cross-Origin Resource Sharing (CORS) on the ESP32 server just uncomment the following build flags:\n\n```ini\nbuild_flags =\n...\n  ; Uncomment to configure Cross-Origin Resource Sharing\n  -D ENABLE_CORS\n  -D CORS_ORIGIN=\\\"*\\\"\n```\n\nThis will add the `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials` headers to any request made.\n\n### ESP32 `CORE_DEBUG_LEVEL`\n\nThe ESP32 Arduino Core and many other libraries use the ESP Logging tools. To enable these debug and error messages from deep inside your libraries uncomment the following build flag.\n\n```ini\nbuild_flags =\n...\n\t-D CORE_DEBUG_LEVEL=5\n```\n\nIt accepts values from 5 (Verbose) to 1 (Errors) for different information depths to be logged on the serial terminal. If commented out there won't be debug messages from the core libraries. For a production build you should comment this out.\n\n### Serve Config Files\n\nBy enabling this build flag the ESP32 will serve all config files stored on the LittleFS flash partition under `http:\\\\[IP]\\config\\[filename].json`. This can be helpful to troubleshoot problems. However, it is strongly advised to disable this for production builds.\n\n```ini\nbuild_flags =\n...\n  -D SERVE_CONFIG_FILES\n```\n\n### Serial Info\n\nIn some circumstances it might be beneficial to not print any information on the serial consol (Serial1 or USB CDC). By commenting out the following build flag ESP32-Sveltekit will not print any information on the serial console.\n\n```ini\nbuild_flags =\n...\n  -D SERIAL_INFO\n```\n\n## SSL Root Certificate Store\n\nSome features like firmware download or the MQTT client require a SSL connection. For that the SSL Root CA certificate must be known to the ESP32. The build system contains a python script derived from Espressif ESP-IDF building a certificate store containing one or more certificates. In order to create the store you must uncomment the three lines below in `platformio.ini`.\n\n```ini\nextra_scripts =\n    pre:scripts/generate_cert_bundle.py\nboard_build.embed_files = src/certs/x509_crt_bundle.bin\nboard_ssl_cert_source = adafruit\n```\n\nThe script will download a public certificate store from Mozilla (`board_ssl_cert_source = mozilla`) or a repository curated by Adafruit (`board_ssl_cert_source = adafruit`) or (`board_ssl_cert_source = adafruit-full`), builds a binary containing all certs and embeds this into the firmware. This will add ~65kb to the firmware image. Should you only need a few known certificates you can place their `*.pem` or `*.der` files in the [ssl_certs](https://github.com/theelims/ESP32-sveltekit/blob/main/ssl_certs) folder and change `board_ssl_cert_source = folder`. Then only these certificates will be included in the store. This is especially useful, if you only need to connect to know servers and need to shave some kb off the firmware image:\n\n!!! info\n\n     To enable SSL the feature `FT_NTP=1` must be enabled as well.\n\n!!! bug\n\n    At the moment there is a bug with the certificate bundle when using the firmware download e.g. from Github. By using the build flag `-D DOWNLOAD_OTA_SKIP_CERT_VERIFY` you may skip certificate validation to keep OTA working. Only OTA seems affected, not MQTT. Keep in mind, that this voids the main security feature of SSL and allows man-in-the-middle attacks.\n\n## Vite and LittleFS 32 Character Limit\n\nThe static files for the website are build using vite. By default vite adds a unique hash value to all filenames for improved caching performance. However, LittleFS on the ESP32 is limited to filenames with 32 characters. This restricts the number of characters available for the user to name svelte files. To give a little bit more headroom a vite-plugin removes all hash values, as they offer no benefit on an ESP32. However, have the 32 character limit in mind when naming files. Excessively long names may still cause some issues when building the LittleFS binary.\n\n## Merged Firmware File for Web Flasher\n\nThe PIO build system calls a script `merge_bin.py` to create a merged firmware binary ready to be used with [ESP Web Tools](https://esphome.github.io/esp-web-tools/). The file is located under the PIO build folder. Typically `build/merged/{APP_NAME}_{$PIOENV}_{APP_VERSION}.bin`.\n"
  },
  {
    "path": "docs/components.md",
    "content": "# Components\n\nThe project includes a number of components to create the user interface. Even though DaisyUI has a huge set of components, it is often beneficial to recreate them as a Svelte component. This offers a much better integration into the Svelte way of doing things, is less troublesome with animations and results in a overall better user experience.\n\n## Collapsible\n\nA collapsible container to hide / show content by clicking on the arrow button.\n\n```ts\nimport Collapsible from \"$lib/components/Collapsible.svelte\";\n```\n\nIt exports a closed / open state with `export open` which you can use to determine the mounting behavior of the component.\n\n### Slots\n\nThe component has two slots. A named slot `title` for the collapsible title and the main slot for the content that can be hidden or shown.\n\n```\n<Collapsible open={false} class=\"shadow-lg\" on:closed={doSomething}>\n  <span slot=\"title\">Title</span>\n  ...\n</Collapsible>\n```\n\nThe `class` attribute may be used as normal to style the container. By default there is no special styling like background or shadows to accentuate the container element.\n\n### Events\n\nThe collapsible component dispatches two events. `on:closed` when the collapsible is closed and `on:opened` when it is opened. You can bind to them as to any other event.\n\n## InputPassword\n\nThis is an input field specifically for passwords. It comes with an \"eye\"-button on the right border to toggle the visibility of the password. It natively blends into the style from DaisyUI.\n\n```ts\nimport InputPassword from \"$lib/components/InputPassword.svelte\";\n```\n\nYou may use it like any other form element:\n\n```\n<InputPassword id=\"pwd\" bind:value={password} />\n```\n\n## RSSIIndicator\n\nThis shows the popular WiFi strength indicator icon with differently highlighted circles depending on the received signal strength (RSSI) of the WiFi signal. In addition it can display the signal strength in raw \"dBm\" as an indicator badge.\n\n```ts\nimport RssiIndicator from \"$lib/components/RSSIIndicator.svelte\";\n```\n\nJust use and style as you please. It doesn't have any slots or events.\n\n```\n<RssiIndicator showDBm={true} rssi_dbm={-85} class=\"text-base-content h-10 w-10\" />\n```\n\nTwo exports control the behavior of the component. `rssi_dbm` accepts a negative number of the raw RSSI in dBm and is used to determine how many circles of reception should be shown. An optional boolean `showDBm` (defaults to `false`) shows the indicator badge with the dBm value.\n\n## Settings Card\n\nA Settings Card is in many ways similar to a [collapsible](#collapsible). However, it is styled and is the main element of many settings menus. It also accepts an icon in a dedicate slot and unlike collapsible has no events.\n\n```ts\nimport SettingsCard from \"$lib/components/SettingsCard.svelte\";\n```\n\n### Slots\n\nThree slots are available. Besides the main slot for the content there is a named slot for the `title` and s second one for the `icon`.\n\n```\n<SettingsCard collapsible={true} open={false}>\n\t<Icon slot=\"icon\" class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t<span slot=\"title\">Title</span>\n    ...\n</SettingsCard>\n```\n\nThe component exports two properties to determine its behavior. `collapsible` is a boolean describing wether the component should behave like a collapsible in the first place. `open` is a boolean as well and if set true shows the full content of the body on mount.\n\n## Spinner\n\nA small component showing an animated spinner which can be used while waiting for data.\n\n```ts\nimport Spinner from \"$lib/components/Spinner.svelte\";\n```\n\nNo slots, no events, no properties. Just use `<Spinner/>` whenever something is loading.\n\n## Toast Notifications\n\nToast notifications are implemented as a writable store and are easy to use from any script section. They are an easy way to feedback to the user. To use them just import the notifications store\n\n```ts\nimport { notifications } from \"$lib/components/toasts/notifications\";\n```\n\nand call one of the 4 toast methods:\n\n| Method                                             | Description                                         |\n| -------------------------------------------------- | --------------------------------------------------- |\n| `notification.error(msg:string, timeout:number)`   | :octicons-x-circle-16: Shows an error message       |\n| `notification.warning(msg:string, timeout:number)` | :octicons-alert-16: Shows a warning message         |\n| `notification.info(msg:string, timeout:number)`    | :octicons-info-16: Shows an info message            |\n| `notification.success(msg:string, timeout:number)` | :octicons-check-circle-16: Shows as success message |\n\nEach method takes an `msg`-string as an argument, which will be shown as the message body. It accepts HTML to enrich your toasts, if you should desire to do so. The `timeout` argument specifies how many milliseconds the toast notification shall be shown to the user.\n\n## Github Update Dialog\n\nThis is a modal showing the update progress, possible error messages and makes a full page refresh 5 seconds after the OTA was successful.\n\n## Update Indicator\n\nThe update indicator is a small widget shown in the upper right corner of the status bar. It indicates the availability of a newer firmware release then the current one. Upon pressing the icon it will automatically update the firmware to the latest release. By default this works through the Github Latest Release API. This must be customized should you use a different update server. Have a look at the [source file](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/components/GithubUpdateDialog.svelte) to see what portions to update.\n\n## Info Dialog\n\nShows a modal on the UI which must be deliberately dismissed. It features a `title` and a `message` property. The dismiss button can be customized via the `dismiss` property with a label and an icon. `onDismiss` call back must close the modal and can be used to do something when closing the info dialog.\n\n```ts\nimport InfoDialog from \"$lib/components/InfoDialog.svelte\";\n\nmodals.open(InfoDialog, {\n  title: 'You have a new Info',\n  message:\n    'Something really important happened that justifies showing you a modal which must be clicked away.',\n  dismiss: { label: 'OK', icon: Check },\n  onDismiss: () => modals.close();\n});\n```\n\nThis modal is based on [svelte-modals](https://svelte-modals.mattjennings.io/) where you can find further information.\n\n## Confirm Dialog\n\nShows a confirm modal on the UI which must be confirmed to proceed, or can be canceled. It features a `title` and a `message` property. The `confirm` and `cancel` buttons can be customized via the `labels` property with a label and an icon. `onConfirm` call back must close the modal and can be used to trigger further actions.\n\n```ts\nimport ConfirmDialog from \"$lib/components/ConfirmDialog.svelte\";\n\nmodals.open(ConfirmDialog, {\n  title: \"Confirm what you are doing\",\n  message: \"Are you sure you want to proceed? This could break stuff!\",\n  labels: {\n    cancel: { label: \"Abort\", icon: Cancel },\n    confirm: { label: \"Confirm\", icon: Check },\n  },\n  onConfirm: () => modals.close(),\n});\n```\n\nThis modal is based on [svelte-modals](https://svelte-modals.mattjennings.io/) where you can find further information.\n"
  },
  {
    "path": "docs/gettingstarted.md",
    "content": "# Getting Started\n\n## Prerequisites\n\nThis project has quite a complicated build chain to prepare the frontend code for the ESP32. You will need to install some tools to make this all work, starting with a powerful code editor.\n\n### Softwares to Install\n\nPlease install the following software, if you haven't already:\n\n- [VSCode](https://code.visualstudio.com/) - IDE for development\n- [Node.js](https://nodejs.org) - For building the interface with npm\n\n### VSCode Plugins and Setups\n\nPlease install the following mandatory VSCode Plugins:\n\n- [PlatformIO](https://platformio.org/) - Embedded development platform\n- [Prettier](https://prettier.io/) - Automated code formatter\n- Svelte for VS Code - Makes working with Svelte much easier\n- Svelte Intellisense - Another Svelte tool\n- Tailwind CSS Intellisense - Makes working with Tailwind CSS much easier\n- [Prettier plugin for Tailwind CSS](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) - Automatically sorts the Tailwind classes into their recommended order\n\nLastly, if you want to make use of Materials for MkDocs as your documentation engine, install [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) by typing the following into the VSCode terminal:\n\n```bash\npip install mkdocs-material\n```\n\n!!! tip\n\n    You might need to run this as administrator, if you getting an error message.\n\n### Project Structure\n\n| Resource                                                                               | Description                                                      |\n| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| [.github/](https://github.com/theelims/ESP32-sveltekit/blob/main/.github)              | Github CI pipeline to deploy MkDocs to gh-pages                  |\n| [docs/](https://github.com/theelims/ESP32-sveltekit/blob/main/docs)                    | MkDocs documentation files                                       |\n| [interface/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface)          | SvelteKit based front end                                        |\n| [lib/framework/](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework)  | C++ back end for the ESP32 device                                |\n| [src/](https://github.com/theelims/ESP32-sveltekit/blob/main/src)                      | The main.cpp and demo project to get you started                 |\n| [scripts/](https://github.com/theelims/ESP32-sveltekit/tree/main/scripts)              | Scripts that build the interface as part of the platformio build |\n| [platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) | PlatformIO project configuration file                            |\n| [mkdocs.yaml](https://github.com/theelims/ESP32-sveltekit/blob/main/mkdocs.yaml)       | MkDocs project configuration file                                |\n\n## Setting up PlatformIO\n\n### Setup Build Target\n\n!!! danger \"Do not use the PlatformIO UI for editing platformio.ini\"\n\n    It is tempting to use the PlatformIO user interface to add dependencies or parameters to platformio.ini. However, doing so will remove all \"irrelevant\" information like comments from the file. Please edit the file directly in the editor.\n\n[platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) is the central file controlling the whole build process. It comes pre-configure with a few boards which have different ESP32 chips. It needs to be adapted to the board you want to program.\n\n```ini\n[platformio]\n...\ndefault_envs = esp32-s3-devkitc-1\n...\n\n[env:adafruit_feather_esp32_v2]\nboard = adafruit_feather_esp32_v2\nboard_build.mcu = esp32\n\n[env:lolin_c3_mini]\nboard = lolin_c3_mini\nboard_build.mcu = esp32c3\n\n[env:esp32-s3-devkitc-1]\nboard = esp32-s3-devkitc-1\nboard_build.mcu = esp32s3\n```\n\nIf your board is not listed in the platformio.ini you may look in the [official board list](https://docs.platformio.org/en/latest/boards/index.html#espressif-32) for supported boards and add their information accordingly. Either delete the obsolete `[env:...]` sections, or change your board as `default_envs = ...`.\n\n!!! info \"Default setup is for an ESP32-S3-DevKitC/M board\"\n\n    The projects platformio.ini defaults for an ESP32-S3-DevKitC/M board by Espressif connected to the UART USB port. If you use an other board and the projects shows an undesired behavior it is likely that some parts do not match with pin definitions.\n\n### Build & Upload Process\n\nAfter you've changed [platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) to suit your board you can upload the sample code to your board. This will download all ESP32 libraries and execute `node install` to install all node packages as well. Select your board's environment under the PlatformIO tab and hit `Upload and Monitor`.\n\n![PIO Build](media/PIO-upload.png)\n\nThe first build process will take a while. After a couple of minutes you can see the ESP32 outputting information on the terminal. Some of the python scripts might need to install additional packages. In that case the first build process will fail. Just run it a second time.\n\n!!! tip \"Use several terminals in parallel\"\n\n    VSCode allows you to have more then one terminal running at the same time. You can dedicate one terminal to the serial monitor, while having the development server running in an other terminal.\n\n## Setting up SvelteKit\n\n### Setup Proxy for Development\n\nTo ease the frontend development you can deploy the back end code on an ESP32 board and pass the websocket and REST API calls through the development server's proxy.\nThe [vite.config.ts](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/vite.config.ts) file defines the location of the services which the development server will proxy. This is defined by the \"target\" property, which will need to be changed to the the IP address or hostname of the device running the firmware. Change this for both, \"http://\" and \"ws://\".\n\n```ts\nproxy: {\n    // Proxying REST: http://localhost:5173/rest/bar -> http://192.168.1.83/rest/bar\n    '/rest': {\n    target: 'http://192.168.1.83',\n    changeOrigin: true,\n    },\n    // Proxying websockets ws://localhost:5173/ws -> ws://192.168.1.83/ws\n    '/ws': {\n    target: 'ws://192.168.1.83',\n    changeOrigin: true,\n    ws: true,\n    },\n},\n```\n\n!!! tip\n\n    You must restart the development server for changes of the proxy location to come into effect.\n\n### Development Server\n\nThe interface comes with Vite as a development server. It allows hot module reloading reflecting code changes to the front end instantly in your browser. Open a new terminal session and execute the following commands:\n\n```bash\ncd interface\nnpm run dev\n```\n\nFollow the link to access the front end in your browser.\n\n## Setup Material for mkdocs\n\nMaterial for MkDocs allows you to create great technical documentation pages just from markup. If you don't want to use it just delete the `.github` and `docs` folder, as well as `mkdocs.yaml`.\n\nOtherwise initiate the github CI pipeline by committing and pushing to your repository once. This triggers the automatic build. After a few minutes a new branch `gh-pages` containing the static website with your documentation should appear. To deploy it go to your github repository go under settings and complete the following steps.\n![Deploy on gh-pages](media/mkdocs_gh-pages.PNG)\n\n### Development Server\n\nMkDocs comes with a build-in development server which supports hot reload as well. Open a new terminal session in VSCode and type\n\n```\nmkdocs serve\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nhide:\n  - navigation\n  - toc\n---\n\n# ESP32 SvelteKit - Create Amazing IoT Projects\n\n<div style=\"flex\">\n<img src=\"media/Screenshot_light.png\" style=\"height:480px\"> \n<img src=\"media/Screenshot_mobile.png\" style=\"height:480px\"> \n</div>\n\nA simple and extensible framework for ESP32 based IoT projects with a feature-rich, beautiful, and responsive front-end build with [Sveltekit](https://kit.svelte.dev/), [TailwindCSS](https://tailwindcss.com/) and [DaisyUI](https://daisyui.com/). This is a project template to get you started in no time backed by a powerful back end service, an amazing front end served from the ESP32 and an easy to use build chain to get everything going.\n\nIt was forked from the fabulous [rjwats/esp8266-react](https://github.com/rjwats/esp8266-react) project, from where it inherited the mighty back end services.\n\n!!! info\n\n    This template repository is not meant to be used stand alone. If you're just looking for a WiFi manager there are plenty of options available. This is a starting point when you need a rich web UI.\n\n## Features\n\n### :butterfly: Beautiful UI powered by DaisyUI and TailwindCSS\n\nBeautiful, responsive UI which works equally well on desktop and on mobile. Gently animated for a snappy and modern feeling without ever being obtrusive or in the way. Easy theming with DaisyUI and media-queries to respect the users wish for a light or dark theme.\n\n### :simple-svelte: Low Memory Footprint and Easy Customization by Courtesy of SvelteKit\n\nSvelteKit is ideally suited to be served from constrained devices like an ESP32. It's unique approach leads to very slim files. No bloatware like other popular JS frameworks. Not only the low memory footprint make it ideal but the developer experience is also outstanding letting you customize the front end with ease. Adapt and add functionality as you need it. The back end has you covered as well.\n\n### :telephone: Rich Communication Interfaces\n\nComes with a rich set of communication interfaces to cover most standard needs of an IoT application. Like MQTT client, HTTP RESTful API, a WebSocket based Event Socket and a classic Websocket Server. All communication channels are stateful and fully synchronized. Changes propagate and are communicated to all other participants. The states can be persisted on the file system as well. For accurate time keeping time can by synchronized over NTP.\n\n### :file_cabinet: WiFi Provisioning and Management\n\nNaturally ESP32 SvelteKit comes with rich features to manage all your WiFi needs. From pulling up an access point for provisioning or as fall back, to fully manage your WiFi networks. Scan for available networks and connect to them. Advanced configuration options like static IP are on board as well.\n\n### :people_with_bunny_ears_partying: Secured API and User Management\n\nManage different user of your app with two authorization levels. An administrator and a guest user. Authenticate their API calls with a JWT token. Manage the user's profile from the admin interface. Use at own risk, as it is neither secure without the ability to use TLS/SSL encryption on the ESP32 server, nor very convenient, as only an admin can change passwords.\n\n### :airplane: OTA Upgrade Service\n\nThe framework can provide two different channels for Over-the-Air updates. Either by uploading a \\*.bin file from the web interface. Or by pulling a firmware image from an update server. This is implemented with the github release page as an example. It is even possible to have different build environments at the same time and the Github OTA process pulls the correct binary.\n\n### :construction_site: Automated Build Chain\n\nThe automated build chain takes out the pain and tears of getting all the bits and pieces play nice together. The repository contains a PlatformIO project at its heart. A SvelteKit project for the frontend code and a mkdocs project for the documentation go alongside. The PlatformIO build tools not only build the SvelteKit frontend with Vite, but also ensure that the build results are gzipped and find their way into the flash memory of the ESP32. You have two choices to serve the frontend either from the flash partition, or embedded into the firmware binary. The latter is much more friendly if your frontend code should be distributed OTA as well, leaving all configuration files intact.\n\n### :fontawesome-solid-microchip: Compatible with all ESP32 Flavours\n\nThe code runs on all variants of the ESP32 chip family. From the plain old ESP32, the ESP32-S3 and ESP32-C3. Other ESP32 variants might work, but haven't been tested. Sorry, no support for the older ESP8266. Go with one of the ESP32's instead.\n\n[Let's get started!](gettingstarted.md)\n\n## License\n\nESP32 SvelteKit is distributed with two licenses for different sections of the code. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3 and is therefore distributed with said license. The front end code is distributed under the MIT License. See the [LICENSE](https://github.com/theelims/ESP32-sveltekit/blob/main/LICENSE) for a full text of both licenses.\n"
  },
  {
    "path": "docs/restfulapi.md",
    "content": "# RESTful API\n\nThe back end exposes a number of API endpoints which are referenced in the table below.\n\n| Method | Request URL                             | Authentication     | POST JSON Body                                                                                                                                                                                                                     | Info                                                                                    |\n| ------ | --------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |\n| GET    | /rest/features                          | `NONE_REQUIRED`    | none                                                                                                                                                                                                                               | Tells the client which features of the UI should be use                                 |\n| GET    | /rest/mqttStatus                        | `IS_AUTHENTICATED` | none                                                                                                                                                                                                                               | Current MQTT connection status                                                          |\n| GET    | /rest/mqttSettings                      | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Currently used MQTT settings                                                            |\n| POST   | /rest/mqttSettings                      | `IS_ADMIN`         | `{\"enabled\":false,\"uri\":\"mqtt://192.168.1.12:1883\",\"username\":\"\",\"password\":\"\",\"client_id\":\"esp32-f412fa4495f8\",\"keep_alive\":120,\"clean_session\":true,\"message_interval_ms\":0}`                                                    | Update MQTT settings with new parameters                                                |\n| GET    | /rest/ntpStatus                         | `IS_AUTHENTICATED` | none                                                                                                                                                                                                                               | Current NTP connection status                                                           |\n| GET    | /rest/ntpSettings                       | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Current NTP settings                                                                    |\n| POST   | /rest/ntpSettings                       | `IS_ADMIN`         | `{\"enabled\": true,\"server\": \"time.google.com\",\"tz_label\": \"Europe/London\",\"tz_format\": \"GMT0BST,M3.5.0/1,M10.5.0\"}`                                                                                                                | Update the NTP settings                                                                 |\n| GET    | /rest/apStatus                          | `IS_AUTHENTICATED` | none                                                                                                                                                                                                                               | Current AP status and client information                                                |\n| GET    | /rest/apSettings                        | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Current AP settings                                                                     |\n| POST   | /rest/apSettings                        | `IS_ADMIN`         | `{\"provision_mode\": 1,\"ssid\": \"ESP32-SvelteKit-e89f6d20372c\",\"password\": \"esp-sveltekit\",\"channel\": 1,\"ssid_hidden\": false,\"max_clients\": 4,\"local_ip\": \"192.168.4.1\",\"gateway_ip\": \"192.168.4.1\",\"subnet_mask\": \"255.255.255.0\"}` | Update AP settings                                                                      |\n| GET    | /rest/wifiStatus                        | `IS_AUTHENTICATED` | none                                                                                                                                                                                                                               | Current status of the wifi client connection                                            |\n| GET    | /rest/scanNetworks                      | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Async Scan for Networks in Range                                                        |\n| GET    | /rest/listNetworks                      | `IS_ADMIN`         | none                                                                                                                                                                                                                               | List networks in range after successful scanning. Otherwise triggers scanning.          |\n| GET    | /rest/wifiSettings                      | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Current WiFi settings                                                                   |\n| POST   | /rest/wifiSettings                      | `IS_ADMIN`         | `{\"hostname\":\"esp32-f412fa4495f8\",\"connection_mode\":1,\"wifi_networks\":[{\"ssid\":\"YourSSID\",\"password\":\"YourPassword\",\"static_ip_config\":false}]}`                                                                                   | Update WiFi settings and credentials                                                    |\n| GET    | /rest/systemStatus                      | `IS_AUTHENTICATED` | none                                                                                                                                                                                                                               | Get system information about the ESP.                                                   |\n| POST   | /rest/restart                           | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Restart the ESP32                                                                       |\n| POST   | /rest/factoryReset                      | `IS_ADMIN`         | none                                                                                                                                                                                                                               | Reset the ESP32 and all settings to their default values                                |\n| POST   | /rest/uploadFirmware                    | `IS_ADMIN`         | none                                                                                                                                                                                                                               | File upload of firmware.bin                                                             |\n| POST   | /rest/signIn                            | `NONE_REQUIRED`    | `{\"password\": \"admin\",\"username\": \"admin\"}`                                                                                                                                                                                        | Signs a user in and returns access token                                                |\n| GET    | /rest/securitySettings                  | `IS_ADMIN`         | none                                                                                                                                                                                                                               | retrieves all user information and roles                                                |\n| POST   | /rest/securitySettings                  | `IS_ADMIN`         | `{\"jwt_secret\": \"734cb5bb-5597b722\", \"users\": [{\"username\": \"admin\", \"password\": \"admin\", \"admin\": true}, {\"username\": \"guest\", \"password\": \"guest\", \"admin\": false, }]}`                                                          | retrieves all user information and roles                                                |\n| GET    | /rest/verifyAuthorization               | `NONE_REQUIRED`    | none                                                                                                                                                                                                                               | Verifies the content of the auth bearer token                                           |\n| GET    | /rest/generateToken?username={username} | `IS_ADMIN`         | `{\"token\": \"734cb5bb-5597b722\"}`                                                                                                                                                                                                   | Generates a new JWT token for the user from username                                    |\n| POST   | /rest/sleep                             | `IS_AUTHENTICATED` | none                                                                                                                                                                                                                               | Puts the device in deep sleep mode                                                      |\n| POST   | /rest/downloadUpdate                    | `IS_ADMIN`         | `{\"download_url\": \"https://github.com/theelims/ESP32-sveltekit/releases/download/v0.1.0/firmware_esp32s3.bin\"}`                                                                                                                    | Download link for OTA. This requires a valid SSL certificate and will follow redirects. |\n| GET    | /rest/coreDump                          | `IS_AUTHENTICATED` | Text                                                                                                                                                                                                                               | Core dump of the last crash.                                                            |\n"
  },
  {
    "path": "docs/statefulservice.md",
    "content": "# Developing with the Framework\n\nThe back end is a set of REST endpoints hosted by a [PsychicHttp](https://github.com/hoeken/PsychicHttp) instance. The ['lib/framework'](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework) directory contains the majority of the back end code. The framework contains a number of useful utility classes which you can use when extending it. The project also comes with a demo project to give you some help getting started.\n\nThe framework's source is split up by feature, for example [WiFiScanner.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/WiFiScanner.h) implements the end points for scanning for available networks where as [WiFiSettingsService.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/WiFiSettingsService.h) handles configuring the WiFi settings and managing the WiFi connection.\n\n## Initializing the framework\n\nThe ['src/main.cpp'](https://github.com/theelims/ESP32-sveltekit/blob/main/src/main.cpp) file constructs the web server and initializes the framework. You can add endpoints to the server here to support your IoT project. The main loop is also accessible so you can run your own code easily.\n\nThe following code creates the web server and esp32sveltekit framework:\n\n```cpp\nPsychicHttpServer server;\nESP32SvelteKit esp32sveltekit(&server, 120);\n```\n\nESP32SvelteKit is instantiated with a reference to the server and a number of HTTP endpoints. The underlying ESP-IDF HTTP Server statically allocates memory for each endpoint and needs to know how many there are. Best is to inspect your WWWData.h file for the number of Endpoints from SvelteKit (currently 60), the framework itself has 37 endpoints, and Lighstate Demo has 7 endpoints. Each `_server.on()` counts as an endpoint. Don't forget to add a couple of spare, just in case. Each HttpEndpoint adds 2 endpoints, if CORS is enabled it adds an other endpoint for the CORS preflight request.\n\nNow in the `setup()` function the initialization is performed:\n\n```cpp\nvoid setup() {\n  // start serial and filesystem\n  Serial.begin(SERIAL_BAUD_RATE);\n\n  // start the framework and demo project\n  esp32sveltekit.begin();\n}\n```\n\n`server.begin()` is called by ESP32-SvelteKit, as the start-up sequence is crucial.\n\n## Stateful Service\n\nThe framework promotes a modular design and exposes features you may re-use to speed up the development of your project. Where possible it is recommended that you use the features the frameworks supplies. These are documented in this section and a comprehensive example is provided by the demo project.\n\nThe following diagram visualizes how the framework's modular components fit together, each feature is described in detail below.\n\n![framework diagram](media/framework.png)\n\nThe [StatefulService.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/StatefulService.h) class is responsible for managing state. It has an API which allows other code to update or respond to updates in the state it manages. You can define a data class to hold state, then build a StatefulService class to manage it. After that you may attach HTTP endpoints, WebSockets or MQTT topics to the StatefulService instance to provide commonly required features.\n\nHere is a simple example of a state class and a StatefulService to manage it:\n\n```cpp\nclass LightState {\n public:\n  bool on = false;\n  uint8_t brightness = 255;\n};\n\nclass LightStateService : public StatefulService<LightState> {\n};\n```\n\n### Update Handler\n\nYou may listen for changes to state by registering an update handler callback. It is possible to remove an update handler later if required.\n\n```cpp\n// register an update handler\nupdate_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler(\n  [&](const String& originId) {\n    Serial.print(\"The light's state has been updated by: \");\n    Serial.println(originId);\n  }\n);\n\n// remove the update handler\nlightStateService.removeUpdateHandler(myUpdateHandler);\n```\n\nAn \"originId\" is passed to the update handler which may be used to identify the origin of an update. The default origin values the framework provides are:\n\n| Origin                     | Description                                     |\n| -------------------------- | ----------------------------------------------- |\n| http                       | An update sent over REST (HttpEndpoint)         |\n| mqtt                       | An update sent over MQTT (MqttEndpoint)         |\n| websocketserver:{clientId} | An update sent over WebSocket (WebSocketServer) |\n\n### Hook Handler\n\nSometimes if can be desired to hook into every update of an state, even if the StateUpdateResult is `StateUpdateResult::UNCHANGED` and the update handler isn't called. In such cases you can use the hook handler. Similarly it can be removed later.\n\n```cpp\n// register an update handler\nhook_handler_id_t myHookHandler = lightStateService.addHookHandler(\n  [&](const String& originId, StateUpdateResult &result) {\n    Serial.printf(\"The light's state has been updated by: %s with result %d\\n\", originId, result);\n  }\n);\n\n// remove the update handler\nlightStateService.removeHookHandler(myHookHandler);\n```\n\n### Read & Update State\n\nStatefulService exposes a read function which you may use to safely read the state. This function takes care of protecting against parallel access to the state in multi-core environments such as the ESP32.\n\n```cpp\nlightStateService.read([&](LightState& state) {\n  digitalWrite(LED_PIN, state.on ? HIGH : LOW); // apply the state update to the LED_PIN\n});\n```\n\nStatefulService also exposes an update function which allows the caller to update the state with a callback. This function automatically calls the registered update handlers if the state has been changed. The example below changes the state of the light (turns it on) using the arbitrary origin \"timer\" and returns the \"CHANGED\" state update result, indicating that a change was made:\n\n```cpp\nlightStateService.update([&](LightState& state) {\n   if (state.on) {\n    return StateUpdateResult::UNCHANGED; // lights were already on, return UNCHANGED\n  }\n  state.on = true;  // turn on the lights\n  return StateUpdateResult::CHANGED; // notify StatefulService by returning CHANGED\n}, \"timer\");\n```\n\nThere are three possible return values for an update function which are as follows:\n\n| Origin                       | Description                                                              |\n| ---------------------------- | ------------------------------------------------------------------------ |\n| StateUpdateResult::CHANGED   | The update changed the state, propagation should take place if required  |\n| StateUpdateResult::UNCHANGED | The state was unchanged, propagation should not take place               |\n| StateUpdateResult::ERROR     | There was an error updating the state, propagation should not take place |\n\n### JSON Serialization\n\nWhen reading or updating state from an external source (HTTP, WebSockets, or MQTT for example) the state must be marshalled into a serializable form (JSON). SettingsService provides two callback patterns which facilitate this internally:\n\n| Callback         | Signature                                               | Purpose                                                                           |\n| ---------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------- |\n| JsonStateReader  | void read(T& settings, JsonObject& root)                | Reading the state object into a JsonObject                                        |\n| JsonStateUpdater | StateUpdateResult update(JsonObject& root, T& settings) | Updating the state from a JsonObject, returning the appropriate StateUpdateResult |\n\nThe static functions below can be used to facilitate the serialization/deserialization of the light state:\n\n```cpp\nclass LightState {\n public:\n  bool on = false;\n  uint8_t brightness = 255;\n\n  static void read(LightState& state, JsonObject& root) {\n    root[\"on\"] = state.on;\n    root[\"brightness\"] = state.brightness;\n  }\n\n  static StateUpdateResult update(JsonObject& root, LightState& state) {\n    state.on = root[\"on\"] | false;\n    state.brightness = root[\"brightness\"] | 255;\n    return StateUpdateResult::CHANGED;\n  }\n};\n```\n\nFor convenience, the StatefulService class provides overloads of its `update` and `read` functions which utilize these functions.\n\nRead the state to a JsonObject using a serializer:\n\n```cpp\nJsonObject jsonObject = jsonDocument.to<JsonObject>();\nlightStateService->read(jsonObject, LightState::read);\n```\n\nUpdate the state from a JsonObject using a deserializer:\n\n```cpp\nJsonObject jsonObject = jsonDocument.as<JsonObject>();\nlightStateService->update(jsonObject, LightState::update, \"timer\");\n```\n\n### HTTP RESTful Endpoint\n\nThe framework provides an [HttpEndpoint.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/HttpEndpoint.h) class which may be used to register GET and POST handlers to read and update the state over HTTP. You may construct an HttpEndpoint as a part of the StatefulService or separately if you prefer.\n\nThe code below demonstrates how to extend the LightStateService class to provide an endpoint:\n\n```cpp\nclass LightStateService : public StatefulService<LightState> {\n public:\n  LightStateService(PsychicHttpServer* server, ESP32SvelteKit *sveltekit) :\n      _httpEndpoint(LightState::read, LightState::update, this, server, \"/rest/lightState\", sveltekit->getSecurityManager(),AuthenticationPredicates::IS_AUTHENTICATED) {\n  }\n\n  void begin(); {\n    _httpEndpoint.begin();\n  }\n\n private:\n  HttpEndpoint<LightState> _httpEndpoint;\n};\n```\n\nEndpoint security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate must be provided, even if no secure endpoint is required. The placeholder project shows how endpoints can be secured.\n\nTo register the HTTP endpoints with the web server the function `_httpEndpoint.begin()` must be called in the custom StatefulService Class' own `void begin()` function.\n\n### File System Persistence\n\n[FSPersistence.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/FSPersistence.h) allows you to save state to the filesystem. FSPersistence automatically writes changes to the file system when state is updated. This feature can be disabled by calling `disableUpdateHandler()` if manual control of persistence is required.\n\nThe code below demonstrates how to extend the LightStateService class to provide persistence:\n\n```cpp\nclass LightStateService : public StatefulService<LightState> {\n public:\n  LightStateService(ESP32SvelteKit *sveltekit) :\n      _fsPersistence(LightState::read, LightState::update, this, sveltekit->getFS(), \"/config/lightState.json\") {\n  }\n\n private:\n  FSPersistence<LightState> _fsPersistence;\n};\n```\n\n### Event Socket Endpoint\n\n[EventEndpoint.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/EventEndpoint.h) wraps the [Event Socket](#event-socket) into an endpoint compatible with a stateful service. The client may subscribe and unsubscribe to this event to receive updates or push updates to the ESP32. The current state is synchronized upon subscription.\n\nThe code below demonstrates how to extend the LightStateService class to provide an WebSocket:\n\n```cpp\nclass LightStateService : public StatefulService<LightState> {\n public:\n  LightStateService(ESP32SvelteKit *sveltekit) :\n      _eventEndpoint(LightState::read, LightState::update, this, sveltekit->getSocket(), \"led\") {}\n\n  void begin()\n  {\n    _eventEndpoint.begin();\n  }\n\n private:\n  EventEndpoint<LightState> _eventEndpoint;\n};\n```\n\nTo register the event endpoint with the event socket the function `_eventEndpoint.begin()` must be called in the custom StatefulService Class' own `void begin()` function.\n\nSince all events run through one websocket connection it is not possible to use the [securityManager](#security-features) to limit access to individual events. The security defaults to `AuthenticationPredicates::IS_AUTHENTICATED`.\n\n### WebSocket Server\n\n[WebSocketServer.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/WebSocketServer.h) allows you to read and update state over a WebSocket connection. WebSocketServer automatically pushes changes to all connected clients when state is updated.\n\nThe code below demonstrates how to extend the LightStateService class to provide an WebSocket:\n\n```cpp\nclass LightStateService : public StatefulService<LightState> {\n public:\n  LightStateService(PsychicHttpServer* server, ESP32SvelteKit *sveltekit) :\n      _webSocket(LightState::read, LightState::update, this, server, \"/ws/lightState\", sveltekit->getSecurityManager(), AuthenticationPredicates::IS_AUTHENTICATED), {\n  }\n\n  void begin() {\n    _webSocketServer.begin();\n  }\n\n private:\n  WebSocketServer<LightState> _webSocketServer;\n};\n```\n\nWebSocket security is provided by authentication predicates which are [documented below](#security-features). The SecurityManager and authentication predicate must be provided, even if no secure endpoint is required. The placeholder project shows how endpoints can be secured.\n\nTo register the WS endpoint with the web server the function `_webSocketServer.begin()` must be called in the custom StatefulService Class' own `void begin()` function.\n\n### MQTT Client\n\nThe framework includes an MQTT client which can be configured via the UI. MQTT requirements will differ from project to project so the framework exposes the client for you to use as you see fit. The framework does however provide a utility to interface StatefulService to a pair of pub/sub (state/set) topics. This utility can be used to synchronize state with software such as Home Assistant.\n\n[MqttEndpoint.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/MqttEndpoint.h) allows you to publish and subscribe to synchronize state over a pair of MQTT topics. MqttEndpoint automatically pushes changes to the \"pub\" topic and reads updates from the \"sub\" topic.\n\nThe code below demonstrates how to extend the LightStateService class to interface with MQTT:\n\n```cpp\n\nclass LightStateService : public StatefulService<LightState> {\n public:\n  LightStateService(ESP32SvelteKit *sveltekit) :\n      _mqttEndpoint(LightState::read,\n                  LightState::update,\n                  this,\n                  sveltekit->getMqttClient(),\n                  \"homeassistant/light/my_light/set\",\n                  \"homeassistant/light/my_light/state\") {\n  }\n\n private:\n  MqttEndpoint<LightState> _mqttEndpoint;\n};\n```\n\nYou can re-configure the pub/sub topics at runtime as required:\n\n```cpp\n_mqttEndpoint.configureBroker(\"homeassistant/light/desk_lamp/set\", \"homeassistant/light/desk_lamp/state\");\n```\n\nThe demo project allows the user to modify the MQTT topics via the UI so they can be changed without re-flashing the firmware.\n\n## Event Socket\n\nBeside RESTful HTTP Endpoints the Event Socket System provides a convenient communication path between the client and the ESP32. It uses a single WebSocket connection to synchronize state and to push realtime data to the client. The client needs to subscribe to the topics he is interested. Only clients who have an active subscription will receive data. Every authenticated client may make use of this system as the security settings are set to `AuthenticationPredicates::IS_AUTHENTICATED`.\n\n### Message Format\n\nThe event messages exchanged between the ESP32 and its clients consists of an \"event\" head and the \"data\" payload. For the LightState example a message looks like this in JSON representation:\n\n```JSON\n{\n  \"event\": \"led\",\n  \"data\": {\n    \"led_on\": true\n  }\n}\n```\n\nTo save on bandwidth the event message is encoded as binary [MessagePack](https://msgpack.org/) instead of a JSON.\n\nTo subscribe the client has to send the following message (as MessagePack):\n\n```JSON\n{\n  \"event\": \"subscribe\",\n  \"data\": \"analytics\"\n}\n```\n\n### Emit an Event\n\nThe Event Socket provides an `emitEvent()` function to push data to all subscribed clients. This is used by various esp32sveltekit classes to push real time data to the client. First an event must be registered with the Event Socket by calling `_socket.registerEvent(\"CustomEvent\");`. Only then clients may subscribe to this custom event and you're entitled to emit event data:\n\n```cpp\nvoid emitEvent(String event, JsonObject &jsonObject, const char *originId = \"\", bool onlyToSameOrigin = false);\n```\n\nThe latter function allowing a selection of the recipient. If `onlyToSameOrigin = false` the payload is distributed to all subscribed clients, except the `originId`. If `onlyToSameOrigin = true` only the client with `originId` will receive the payload. This is used by the [EventEndpoint](#event-socket-endpoint) to sync the initial state when a new client subscribes.\n\n### Receive an Event\n\nA callback or lambda function can be registered to receive an ArduinoJSON object and the originId of the client sending the data:\n\n```cpp\n_socket.onEvent(\"CostumEvent\",[&](JsonObject &root, int originId)\n{\n  bool ledState = root[\"led_on\"];\n});\n```\n\n### Get Notified on Subscriptions\n\nSimilarly a callback or lambda function may be registered to get notified when a client subscribes to an event:\n\n```cpp\n_socket.onSubscribe(\"CostumEvent\",[&](const String &originId)\n{\n  Serial.println(\"New Client subscribed: \" + originId);\n});\n```\n\nThe boolean parameter provided will always be `true`.\n\n### Push Notifications to All Clients\n\nIt is possibly to send push notifications to all clients by using the Event Socket. These will be displayed as toasts an the client side. Either directly call\n\n```cpp\nesp32sveltekit.getNotificationService()->pushNotification(\"Pushed a message!\", PUSHINFO);\n```\n\nor keep a local pointer to the `EventSocket` instance. It is possible to send `PUSHINFO`, `PUSHWARNING`, `PUSHERROR` and `PUSHSUCCESS` events to all clients.\n\n## Security features\n\nThe framework has security features to prevent unauthorized use of the device. This is driven by [SecurityManager.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/SecurityManager.h).\n\nOn successful authentication, the /rest/signIn endpoint issues a [JSON Web Token (JWT)](https://jwt.io/) which is then sent using Bearer Authentication. For this add an `Authorization`-Header to the request with the Content `Bearer {JWT-Secret}`. The framework come with built-in predicates for verifying a users access privileges. The built in AuthenticationPredicates can be found in [SecurityManager.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/SecurityManager.h) and are as follows:\n\n| Predicate        | Description                                   |\n| ---------------- | --------------------------------------------- |\n| NONE_REQUIRED    | No authentication is required.                |\n| IS_AUTHENTICATED | Any authenticated principal is permitted.     |\n| IS_ADMIN         | The authenticated principal must be an admin. |\n\nYou can use the security manager to wrap any request handler function with an authentication predicate:\n\n```cpp\nserver->on(\"/rest/someService\", HTTP_GET,\n  _securityManager->wrapRequest(std::bind(&SomeService::someService, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)\n);\n```\n\nIn case of a websocket connection the JWT token is supplied as a search parameter in the URL when establishing the connection:\n\n```\n/ws/lightState?access_token={JWT Token}\n```\n\n## Placeholder substitution\n\nVarious settings support placeholder substitution, indicated by comments in [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini). This can be particularly useful where settings need to be unique, such as the Access Point SSID or MQTT client id. Strings must be properly escaped in the ini-file. The following placeholders are supported:\n\n| Placeholder  | Substituted value                                                     |\n| ------------ | --------------------------------------------------------------------- |\n| #{platform}  | The microcontroller platform, e.g. \"esp32\" or \"esp32c3\"               |\n| #{unique_id} | A unique identifier derived from the MAC address, e.g. \"0b0a859d6816\" |\n| #{random}    | A random number encoded as a hex string, e.g. \"55722f94\"              |\n\nYou may use SettingValue::format in your own code if you require the use of these placeholders. This is demonstrated in the demo project:\n\n```cpp\n  static StateUpdateResult update(JsonObject& root, LightMqttSettings& settings) {\n    settings.mqttPath = root[\"mqtt_path\"] | SettingValue::format(\"homeassistant/light/#{unique_id}\");\n    settings.name = root[\"name\"] | SettingValue::format(\"light-#{unique_id}\");\n    settings.uniqueId = root[\"unique_id\"] | SettingValue::format(\"light-#{unique_id}\");\n    return StateUpdateResult::CHANGED;\n  }\n```\n\n## Accessing settings and services\n\nThe framework supplies access to various features via getter functions:\n\n| SettingsService              | Description                                        |\n| ---------------------------- | -------------------------------------------------- |\n| getFS()                      | The filesystem used by the framework               |\n| getSecurityManager()         | The security manager - detailed above              |\n| getSecuritySettingsService() | Configures the users and other security settings   |\n| getWiFiSettingsService()     | Configures and manages the WiFi network connection |\n| getAPSettingsService()       | Configures and manages the Access Point            |\n| getNTPSettingsService()      | Configures and manages the network time            |\n| getMqttSettingsService()     | Configures and manages the MQTT connection         |\n| getMqttClient()              | Provides direct access to the MQTT client instance |\n| getNotificationEvents()      | Lets you send push notifications to all clients    |\n| getSleepService()            | Send the ESP32 into deep sleep                     |\n| getBatteryService()          | Update battery information on the client           |\n\nThe core features use the [StatefulService.h](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework/StatefulService.h) class and therefore you can change settings or observe changes to settings through the read/update API.\n\nInspect the current WiFi settings:\n\n```cpp\nesp32sveltekit.getWiFiSettingsService()->read([&](WiFiSettings& wifiSettings) {\n  Serial.print(\"The ssid is:\");\n  Serial.println(wifiSettings.ssid);\n});\n```\n\nConfigure the WiFi SSID and password manually:\n\n```cpp\nesp32sveltekit.getWiFiSettingsService()->update([&](WiFiSettings& wifiSettings) {\n  wifiSettings.ssid = \"MyNetworkSSID\";\n  wifiSettings.password = \"MySuperSecretPassword\";\n  return StateUpdateResult::CHANGED;\n}, \"myapp\");\n```\n\nObserve changes to the WiFiSettings:\n\n```cpp\nesp32sveltekit.getWiFiSettingsService()->addUpdateHandler(\n  [&](const String& originId) {\n    Serial.println(\"The WiFi Settings were updated!\");\n  }\n);\n```\n\n## Other functions provided\n\n### MDNS Instance Name\n\nESP32 SvelteKit uses mDNS / Bonjour to advertise its services into the local network. You can set the mDNS instance name property by calling\n\n```cpp\nesp32sveltekit.setMDNSAppName(\"ESP32 SvelteKit Demo App\");\n```\n\nmaking the entry a little bit more verbose. This must be called before `esp32sveltekit.begin();`. If you want to advertise further services just include `#include <ESPmNDS.h>` and use `MDNS.addService()` regularly.\n\n### Use ESP32-SvelteKit loop() Function\n\nUnder some circumstances custom services might want to do something periodically. One solution would be to use a dedicated task or RTOS timer for this. Or you can leverage the ESP32-SvelteKit loop-function and have it executed as a callback every 20ms.\n\n```cpp\nesp32sveltekit.addLoopFunction(callback)\n```\n\n### Factory Reset\n\nA factory reset can not only be evoked from the API, but also by calling\n\n```cpp\nesp32sveltekit.factoryReset();\n```\n\nfrom your code. This will erase the complete settings folder, wiping out all settings. This can be a last fall back mode if somebody has forgotten his credentials.\n\n### Recovery Mode\n\nThere is also a recovery mode present which will force the creation of an access point. By calling\n\n```cpp\nesp32sveltekit.recoveryMode();\n```\n\nwill force a start of the AP regardless of the AP settings. It will not change the the AP settings. To exit the recovery mode restart the device or change the AP settings in the UI.\n\n### Power Down with Deep Sleep\n\nThis API service can place the ESP32 in the lowest power deep sleep mode consuming only a few µA. It uses the EXT1 wakeup source, so the ESP32 can be woken up with a button or from a peripherals interrupt. Consult the [ESP-IDF Api Reference](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/sleep_modes.html#_CPPv428esp_sleep_enable_ext1_wakeup8uint64_t28esp_sleep_ext1_wakeup_mode_t) which GPIOs can be used for this. The RTC will also be powered down, so an external pull-up or pull-down resistor is required. It is not possible to persist variable state through the deep sleep. To optimize the deep sleep power consumption it is advisable to use the callback function to put pins with external pull-up's or pull-down's in a special isolated state to prevent current leakage. Please consult the [ESP-IDF Api Reference](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/sleep_modes.html#configuring-ios-deep-sleep-only) for this.\n\nThe settings wakeup pin definition and the signal polarity need to be defined in [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini):\n\n```ini\n; Deep Sleep Configuration\n-D WAKEUP_PIN_NUMBER=38 ; pin number to wake up the ESP\n-D WAKEUP_SIGNAL=0 ; 1 for wakeup on HIGH, 0 for wakeup on LOW\n```\n\nIn addition it is possible to change this as well at runtime by calling:\n\n```cpp\nesp32sveltekit.getSleepService()->setWakeUpPin(int pin, bool level, pinTermination termination = pinTermination::FLOATING);\n```\n\nWith this function it is also possible to configure the internal pull-up or pull-down resistor for this RTC pin. Albeit this might increase the deep sleep current slightly.\n\nA callback function can be attached and triggers when the ESP32 is requested to go into deep sleep. This allows you to safely deal with the power down event. Like persisting software state by writing to the flash, tiding up or notify a remote server about the immanent disappearance.\n\n```cpp\nesp32sveltekit.getSleepService()->attachOnSleepCallback();\n```\n\nAlso the code can initiate the power down deep sleep sequence by calling:\n\n```cpp\nesp32sveltekit.getSleepService()->sleepNow();\n```\n\n### Battery State of Charge\n\nA small helper class let's you update the battery icon in the status bar. This is useful if you have a battery operated IoT device. It must be enabled in [features.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/features.ini). It uses the [Event Socket](#event-socket) and exposes two functions that can be used to update the clients.\n\n```cpp\nesp32sveltekit.getBatteryService()->updateSOC(float stateOfCharge); // update state of charge in percent (0 - 100%)\nesp32sveltekit.getBatteryService()->setCharging(boolean isCharging); // notify the client that the device is charging\n```\n\n### ESP32-SvelteKit Connection Status\n\nEspecially for a cases like a colored status LED it can be useful to have a quick indication of the connection status. By calling:\n\n```cpp\nConnectionStatus status = esp32sveltekit.getConnectionStatus();\n```\n\nthe current connection status can be accessed. The following stats are available:\n\n| Status        | Description                                                                     |\n| ------------- | ------------------------------------------------------------------------------- |\n| OFFLINE       | Device is completely offline                                                    |\n| AP            | Access Point is available, but no client is connected                           |\n| AP_CONNECTED  | Access Point is used and at least 1 client is connected                         |\n| STA           | Device connected to a WiFi Station                                              |\n| STA_CONNECTED | Device connected to a WiFi Station and at least 1 client is connected           |\n| STA_MQTT      | Device connected to a WiFi Station and the device is connected to a MQTT server |\n\n### Custom Features\n\nYou may use the compile time feature service also to enable or disable custom features at runtime and thus control the frontend. A custom feature can only be added during initializing the ESP32 and ESP32-SvelteKit. The frontend queries the features only when first loading the page. Thus the frontend must be refreshed for the changes to become effective.\n\n```cpp\nesp32sveltekit.getFeatureService()->addFeature(\"custom_feature\", true); // or false to disable it\n```\n\n## OTA Firmware Updates\n\nESP32-SvelteKit offers two different ways to roll out firmware updates to field devices. If the frontend should be updated as well it is necessary to embed it into the firmware binary by activating `-D EMBED_WWW`.\n\n### Firmware Upload\n\nEnabling `FT_UPLOAD_FIRMWARE=1` in [features.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/features.ini) creates a REST endpoint that one can post a firmware binary to. The frontend has a file drop zone to upload a new firmware binary from the browser.\n\n### Firmware Download from Update Server\n\nBy enabling `FT_DOWNLOAD_FIRMWARE=1` in [features.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/features.ini) one can POST a link to a firmware binary which is downloaded for the OTA process. This feature requires SSL and is thus dependent on `FT_NTP=1`. The Frontend contains an implementation which uses GitHub's Releases section as the update server. By specifying a firmware version in [platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) one can make use of semantic versioning to determine the correct firmware:\n\n```ini\n    -D BUILD_TARGET=\"$PIOENV\"\n    -D APP_NAME=\\\"ESP32-Sveltekit\\\" ; Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename\n    -D APP_VERSION=\\\"0.3.0\\\" ; semver compatible version string\n```\n\nA build script copies the firmware binary files for all build environment to `build/firmware`. It renames them into `{APP_NAME}_{$PIOENV}_{APP_VERSION}.bin`. It also creates a MD5 checksum file for verification during the OTA process. These files can be used as attachment on the GitHub release pages.\n\n!!! info\n\n    This feature could be unstable on single-core members of the ESP32 family.\n\n#### Custom Update Server\n\nIf Github is not desired as the update server this can be easily modified to any other custom server. The REST API will accept any valid HTTPS-Link. However, SSL is mandatory and may require a different Root CA Certificate then Github to validate correctly.\nFollow the instructions here how to change the [SSL CA Certificate](buildprocess.md#ssl-root-certificate-for-download-ota).\n\nIf you use a custom update server you must also adapt the [frontend](structure.md#custom-update-server) code to suit your needs.\n"
  },
  {
    "path": "docs/stores.md",
    "content": "# Stores\n\n## User\n\nThe user store holds the current users credentials, if the security features are enabled. Just import it as you would use with any svelte store:\n\n```ts\nimport { user } from \"$lib/stores/user\";\n```\n\nYou can subscribe to it like to any other store with `$user` and it has the following properties:\n\n| Property             | Type      | Description                                       |\n| -------------------- | --------- | ------------------------------------------------- |\n| `$user.bearer_token` | `String`  | The JWT token to authorize a user at the back end |\n| `$user.username`     | `String`  | Username of the current user                      |\n| `$user.admin`        | `Boolean` | `true` if the current user has admin privileges   |\n\nIn addition to the properties it provides two methods for initializing the user credentials and to invalidate them. `user.init()` takes a valid JWT toke as an argument and extracts the user privileges and username from it. `user.invalidate()` invalidates the user credentials and redirects to the login pages\n\n!!! warning \"User credentials are stored in the browsers local storage\"\n\n    The user credentials including the JWT token are stored in the browsers local storage. Any javascript executed on the browser can access this making it extremely vulnerable to XSS attacks. Also the HTTP connection between ESP32 and front end is not encrypted making it possible for everyone to read the JWT token in the same network. Fixing these severe security issues is on the todo list for upcoming releases.\n\n## Event Socket\n\nThe [Event Socket System](statefulservice.md#event-socket) is conveniently provided as a Svelte store. Import the store, subscribe to the data interested with `socket.on`. To unsubscribe simply call `socket.off`. Data can be sent to the ESP32 by calling `socket.sendEvent`\n\n```ts\nimport { socket } from \"$lib/stores/socket\";\n\nlet lightState: LightState = { led_on: false };\n\nonMount(() => {\n  socket.on<LightState>(\"led\", (data) => {\n    lightState = data;\n  });\n});\n\nonDestroy(() => socket.off(\"led\"));\n\nsocket.sendEvent(\"led\", lightState);\n```\n\nSubscribing to an invalid event will only create a warning in the ESP_LOG on the serial console of the ESP32.\n\n## Telemetry\n\nThe telemetry store can be used to update telemetry data like RSSI via the [Event Socket](statefulservice.md#event-socket) system.\n\n```ts\nimport { telemetry } from \"$lib/stores/telemetry\";\n```\n\nIt exposes the following properties you can subscribe to:\n\n| Property                           | Type      | Description                                 |\n| ---------------------------------- | --------- | ------------------------------------------- |\n| `$telemetry.rssi.rssi`             | `Number`  | The RSSI signal strength of the WiFi in dBm |\n| `$telemetry.rssi.ssid`             | `String`  | Name of the connected WiFi station          |\n| `$telemetry.rssi.connected`        | `Boolean` | Connection status of the WiFi               |\n| `$telemetry.battery.soc`           | `Number`  | Battery state of charge                     |\n| `$telemetry.battery.charging`      | `Boolean` | Is battery connected to charger             |\n| `$telemetry.download_ota.status`   | `String`  | Status of OTA                               |\n| `$telemetry.download_ota.progress` | `Number`  | Progress of OTA                             |\n| `$telemetry.download_ota.error`    | `String`  | Error Message of OTA                        |\n| `$telemetry.ethernet.connected`    | `Boolean` | Connection status of the ethernet interface |\n\n## Analytics\n\nThe analytics store holds a log of heap and other debug information via the [Event Socket](statefulservice.md#event-socket) system.\n\n```ts\nimport { analytics } from \"$lib/stores/analytics\";\n```\n\nIt exposes an array of the following properties you can subscribe to:\n\n| Property                    | Type     | Description                                    |\n| --------------------------- | -------- | ---------------------------------------------- |\n| `$analytics.uptime`         | `Number` | Uptime of the chip in seconds since last reset |\n| `$analytics.free_heap`      | `Number` | Current free heap                              |\n| `$analytics.min_free_heap`  | `Number` | Minimum free heap that has been                |\n| `$analytics.max_alloc_heap` | `Number` | Biggest continues free chunk of heap           |\n| `$analytics.fs_used`        | `Number` | Bytes used on the file system                  |\n| `$analytics.fs_total`       | `Number` | Total bytes of the file system                 |\n| `$analytics.core_temp`      | `Number` | Core temperature (on some chips)               |\n\nBy default there is one data point every 2 seconds. It holds 1000 data points worth roughly 33 Minutes of data.\n"
  },
  {
    "path": "docs/structure.md",
    "content": "# Customizing the Front End\n\nThe actual code for the front end is located under [interface/src/](https://github.com/theelims/ESP32-sveltekit/tree/main/interface/src) and divided into the \"routes\" folder and a \"lib\" folder for assets, stores and components.\n\n| Resource                                                                                                      | Description                                                    |\n| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [routes/](https://github.com/theelims/ESP32-sveltekit/tree/main/interface/src/routes/)                        | Root of the routing system                                     |\n| [routes/connections/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/connections) | Setting and status pages for MQTT, NTP, etc.                   |\n| [routes/demo/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/demo/)              | The lightstate demo                                            |\n| [routes/system/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/system/)          | Status page for ESP32 and OTA settings                         |\n| [routes/user/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/user/)              | Edit and add users and change passwords                        |\n| [routes/wifi/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/wifi/)              | Status and settings for WiFi station and AP                    |\n| [lib/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/)                              | Library folder for stores, components and assets               |\n| [lib/assets/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/assets/)                | Assets like pictures                                           |\n| [lib/components/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/components/)        | Reusable components like modals, password input or collapsible |\n| [lib/stores](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/stores/)                 | Svelte stores for common access to data                        |\n\n## Features\n\nThe back end provides a JSON which features of the back end are enabled by the [feature selection](buildprocess.md#selecting-features). It is fetched with the page load and made available in the `pages`-store and can be accessed on any site with `page.data.features`. It is used to hide any disabled setting element.\n\n## Delete `demo/` Project\n\nThe light state demo project is included by default to demonstrate the use of the backend and front end. It demonstrates the use of the MQTT-API, websocket API and REST API to switch on the build in LED of the board. [routes/connections/mqtt/MQTTConfig.svelte](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/connections/mqtt/MQTTConfig.svelte) is also part of the 'demo/' Project. You can reuse this to set your own MQTT topics, or delete it. Do not forget to adjust `+page.svelte` as well. Use it as an example how to create your own custom API and access it from the front end. It can be deleted safely after it has been [removed from the menu](#adapt-the-menu) as well.\n\n## Create your root `+page.svelte`\n\nThe root page of the front end is located under [routes/+page.svelte](https://github.com/theelims/ESP32-sveltekit/tree/main/interface/src/routes/+page.svelte). This should be the central place of your app and can be accessed at any time by pressing the logo and app name in the side menu. Just override it to suit your needs.\n\n## Customize the Main Menu\n\nThe main menu is located in [routes/menu.svelte](https://github.com/theelims/ESP32-sveltekit/tree/main/interface/src/routes/menu.svelte) as a svelte component and defines the main menu including a menu footer.\n\n### Menu Footer\n\nThe main menu comes with a small footer to add your copyright notice plus links to github and your discord server where users can find help. The `active`-flag is used to disable an element in the UI. Most of these global parameters are set in the [routes/+layout.ts](https://github.com/theelims/ESP32-sveltekit/tree/main/interface/src/routes/+layout.ts).\n\n```ts\nconst discord = { href: \".\", active: false };\n```\n\n### Menu Structure\n\nThe menu consists of an array of menu items. These are defined as follows:\n\n```ts\n{\n    title: 'Demo App',\n    icon: Control,\n    href: '/demo',\n    feature: page.data.features.project,\n},\n```\n\n- Where `title` refers to the page title. It must be identical to `page.data.title` as defined in the `+page.ts` in any of your routes. If they do not match the corresponding menu item is not highlighted on first page load or a page refresh. A minimum `+page.ts` looks like this:\n\n```ts\nimport type { PageLoad } from \"./$types\";\n\nexport const load = (async ({ fetch }) => {\n  return {\n    title: \"Demo App\",\n  };\n}) satisfies PageLoad;\n```\n\n- `icon` must be an icon component giving the menu items icon.\n- `href` is the link to the route the menu item refers to.\n- `feature` takes a bool and should be set to `true`. It is used by the [feature selector](#features) to hide a menu entry of it is not present on the back end.\n\n## Advanced Customizations\n\nOn the root level there are two more files which you can customize to your needs.\n\n### Login Page\n\n`login.svelte` is a component showing the login screen, when the security features are enabled. By default it shows the app's logo and the login prompt. Change it as you need it.\n\n### Status Bar\n\n`statusbar.svelte` contains the top menu bar which you can customize to show state information about your app and IoT device. By default it shows the active menu title and the hamburger icon on small screens.\n\n## Github Firmware Update\n\nIf the feature `FT_DOWNLOAD_FIRMWARE` is enabled, ESP32 SvelteKit pulls the Github Release section through the Github API for firmware updates once per hour. Also the firmware update menu shows all available firmware releases allowing the user to up- and downgrade has they please. If you're using the Github releases section you must first tell the frontend your correct path to your github repository as described [here](sveltekit.md#changing-the-app-name).\n\nAlso you must make use of couple build flags in [platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini):\n\n```ini\n    -D BUILD_TARGET=\\\"$PIOENV\\\"\n    -D APP_NAME=\\\"ESP32-Sveltekit\\\" ; Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename\n    -D APP_VERSION=\\\"0.3.0\\\" ; semver compatible version string\n```\n\nOut of these flags the [rename_fw.py](https://github.com/theelims/ESP32-sveltekit/blob/main/scripts/rename_fw.py) script will copy and rename the firmware binary to `/build/firmware/{APP_NAME}_{$PIOENV}_{APP_VERSION}.bin`. In addition it will also create a corresponding MD5 checksum file. These files are ready to be uploaded to the Github release page without any further changes. The frontend searches for the firmware binary which matches the build environment and uses this as the update link. This allows you to serve different build targets (e.g. different boards) from the same release page.\n\n### Custom Update Server\n\nThe frontend and backend code can be easily adjusted to suit a custom update server. For the backend the changes are described [here](statefulservice.md#custom-update-server). On the frontend only two files must be adapted and changed to switch to a custom update server: [/lib/components/UpdateIndicator.svelte](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/components/UpdateIndicator.svelte) and [/routes/system/update/GithubFirmwareManager.svelte](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/system/update/GithubFirmwareManager.svelte).\n\n!!! info\n\n    The update server must provide the firmware download through SSL encryption.\n"
  },
  {
    "path": "docs/sveltekit.md",
    "content": "# Getting Started with SvelteKit\n\nSvelteKits unique approach makes it perfect suitable for constraint server. It builds very small files shipping only the minimum required amount of java script. This keeps the memory footprint low so that rich applications can be build with just the 4MB flash of many ESP32 chips.\n\nHowever, since SvelteKit is designed with server-side rendering first, there are some catches and pitfalls one must avoid. Especially as nearly all tutorials found on SvelteKit heavily make use of the combined front and back end features.\n\n## Limitations of `adapter-static`\n\nTo build a website that can be served from an ESP32 `adapter-static` is used. This means no server functions can be used. The front end is build as a Single-Page Application (SPA) instead. However, SvelteKit will pre-render sites at build time, even if SSR and pre-rendering are disabled. This leads to some restrictions that must be taken into consideration:\n\n- You can't use any server-side logic like `+page.server.ts`, `+layout.server.ts` or `+server.ts` files in your project.\n\n- The load function in `+page.ts` gets executed on the server and the client. If you try to access browser resources in the load function this will fail. Use a more traditional way like fetching the data in the `+page.svelte` with the `onMount(() => {})` callback.\n\n## Customizing and Theming\n\n### Changing the App Name\n\n[+layout.ts](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/%2Blayout.ts) bundles a few globally customizable properties like github repository, app name and copyright notice:\n\n```js\nexport const load = (async () => {\n\tconst result = await fetch('/rest/features');\n\tconst item = await result.json();\n\treturn {\n\t\tfeatures: item,\n\t\ttitle: 'ESP32-SvelteKit',\n\t\tgithub: 'theelims/ESP32-sveltekit',\n\t\tcopyright: '2024 theelims',\n\t\tappName: 'ESP32 SvelteKit'\n\t};\n}) satisfies LayoutLoad;\n```\n\nIn [menu.svelte](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/menu.svelte) there is additionally the possibility to add a discord invite, which is disabled by default.\n\n```js\nconst discord = { href: \".\", active: false };\n```\n\nThere is also a manifest file which contains the app name to use when adding the app to a mobile device, so you may wish to also edit [interface/static/manifest.json](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/static/manifest.json):\n\n```json\n{\n  \"name\": \"ESP32 SvelteKit\",\n  \"icons\": [\n    {\n      \"src\": \"/favicon.png\",\n      \"sizes\": \"48x48 72x72 96x96 128x128 256x256\"\n    }\n  ],\n  \"start_url\": \"/\",\n  \"display\": \"fullscreen\",\n  \"orientation\": \"any\"\n}\n```\n\n### Changing the App Icon and Favicon\n\nYou can replace the apps favicon which is located at [interface/static/favicon.png](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/static/favicon.png) with one of your preference. A 256 x 256 PNG is recommended for best compatibility.\n\nAlso the Svelte Logo can be replaced with your own. It is located under [interface/src/lib/assets/logo.png](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/assets/logo.png).\n\n### Daisy UI Themes\n\nThe overall theme of the front end is defined by [DaisyUI](https://daisyui.com/docs/themes/) and can be easily changed according to their documentation. Either by selecting one of the standard themes of DaisyUI, or creating your own. By default the `corporate` and `business` for dark are defined in [app.css](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/app.css):\n\n```js\n@plugin \"daisyui\" {\n    themes: corporate --default, business --prefersdark;\n}\n```\n\n#### Opinionated use of Shadows\n\nThe front end makes some use of colored shadows with the `shadow-primary` and `shadow-secondary` DaisyUI classes. Just use the search and replace function to change this to a more neutral look, if you find the color too much.\n\n#### Color Scheme Helper\n\nSome JS modules do not accept DaisyUI/TailwindCSS color class names. A small helper function can be imported and used to convert any CSS variable name for a DaisyUI color into OKCHL. That way modules like e.g. Charts.js can be styled in the current color scheme in a responsive manner.\n\n```js\nimport { daisyColor } from \"$lib/DaisyUiHelper\";\n\nborderColor: daisyColor('--color-primary'),\nbackgroundColor: daisyColor('--color-primary', 50),\n```\n\n## TS Types Definition\n\nAll types used throughout the front end are exported from [models.ts](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/types/models.ts). It is a convenient location to add your custom types once you expand the front end.\n"
  },
  {
    "path": "factory_settings.ini",
    "content": "; The indicated settings support placeholder substitution as follows:\n;\n;  #{platform} - The microcontroller platform, e.g. \"esp32\" or \"esp32c3\"\n;  #{unique_id} - A unique identifier derived from the MAC address, e.g. \"0b0a859d6816\"\n;  #{random} - A random number encoded as a hex string, e.g. \"55722f94\"\n\n[factory_settings]\nbuild_flags =\n  ; WiFi settings\n  -D FACTORY_WIFI_SSID=\\\"\\\"\n  -D FACTORY_WIFI_PASSWORD=\\\"\\\"\n  -D FACTORY_WIFI_HOSTNAME=\\\"#{platform}-#{unique_id}\\\" ; supports placeholders\n  -D FACTORY_WIFI_RSSI_THRESHOLD=-80 ; dBm, -80 is a good value for most applications\n\n  ; Access point settings\n  -D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED\n  -D FACTORY_AP_SSID=\\\"ESP32-SvelteKit-#{unique_id}\\\" ; 1-64 characters, supports placeholders\n  -D FACTORY_AP_PASSWORD=\\\"esp-sveltekit\\\" ; 8-64 characters\n  -D FACTORY_AP_CHANNEL=1\n  -D FACTORY_AP_SSID_HIDDEN=false\n  -D FACTORY_AP_MAX_CLIENTS=4\n  -D FACTORY_AP_LOCAL_IP=\\\"192.168.4.1\\\"\n  -D FACTORY_AP_GATEWAY_IP=\\\"192.168.4.1\\\"\n  -D FACTORY_AP_SUBNET_MASK=\\\"255.255.255.0\\\"\n\n  ; User credentials for admin and guest user\n  -D FACTORY_ADMIN_USERNAME=\\\"admin\\\"\n  -D FACTORY_ADMIN_PASSWORD=\\\"admin\\\"\n  -D FACTORY_GUEST_USERNAME=\\\"guest\\\"\n  -D FACTORY_GUEST_PASSWORD=\\\"guest\\\"\n\n  ; NTP settings\n  -D FACTORY_NTP_ENABLED=true\n  -D FACTORY_NTP_TIME_ZONE_LABEL=\\\"Europe/Berlin\\\"\n  -D FACTORY_NTP_TIME_ZONE_FORMAT=\\\"GMT0BST,M3.5.0/1,M10.5.0\\\"\n  -D FACTORY_NTP_SERVER=\\\"time.google.com\\\"\n\n  ; MQTT settings\n  -D FACTORY_MQTT_ENABLED=false\n  -D FACTORY_MQTT_URI=\\\"mqtts://broker.hivemq.com:8883\\\"\n  -D FACTORY_MQTT_USERNAME=\\\"\\\" ; supports placeholders\n  -D FACTORY_MQTT_PASSWORD=\\\"\\\"\n  -D FACTORY_MQTT_CLIENT_ID=\\\"#{platform}-#{unique_id}\\\" ; supports placeholders\n  -D FACTORY_MQTT_KEEP_ALIVE=120\n  -D FACTORY_MQTT_CLEAN_SESSION=true\n  -D FACTORY_MQTT_STATUS_TOPIC=\\\"esp32sveltekit/#{unique_id}/status\\\" ; supports placeholders\n  -D FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS=0\n\n  ; JWT Secret\n  -D FACTORY_JWT_SECRET=\\\"#{random}-#{random}\\\" ; supports placeholders\n\n  ; Deep Sleep Configuration\n  -D WAKEUP_PIN_NUMBER=0 ; pin number to wake up the ESP\n  -D WAKEUP_SIGNAL=0 ; 1 for wakeup on HIGH, 0 for wakeup on LOW\n\n\n"
  },
  {
    "path": "features.ini",
    "content": "[features]\nbuild_flags = \n  -D FT_SECURITY=1\n  -D FT_MQTT=1\n  -D FT_NTP=1\n  -D FT_UPLOAD_FIRMWARE=1\n  -D FT_DOWNLOAD_FIRMWARE=1 ; requires FT_NTP=1\n  -D FT_SLEEP=1\n  -D FT_BATTERY=0\n  -D FT_ANALYTICS=1\n  -D FT_COREDUMP=1\n;  -D FT_ETHERNET=1 ; ethernet feature should be enabled in the board config as not every board supports ethernet\n"
  },
  {
    "path": "interface/.eslintignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\n\n# Ignore files for PNPM, NPM and YARN\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "interface/.eslintrc.cjs",
    "content": "module.exports = {\n\troot: true,\n\tparser: '@typescript-eslint/parser',\n\textends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],\n\tplugins: ['svelte3', '@typescript-eslint'],\n\tignorePatterns: ['*.cjs'],\n\toverrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],\n\tsettings: {\n\t\t'svelte3/typescript': () => require('typescript')\n\t},\n\tparserOptions: {\n\t\tsourceType: 'module',\n\t\tecmaVersion: 2020\n\t},\n\tenv: {\n\t\tbrowser: true,\n\t\tes2017: true,\n\t\tnode: true\n\t}\n};\n"
  },
  {
    "path": "interface/.gitignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n"
  },
  {
    "path": "interface/.npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": "interface/.prettierignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\n\n# Ignore files for PNPM, NPM and YARN\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "interface/.prettierrc",
    "content": "{\n\t\"useTabs\": true,\n\t\"singleQuote\": true,\n\t\"trailingComma\": \"none\",\n\t\"printWidth\": 100,\n\t\"plugins\": [\"prettier-plugin-svelte\"],\n\t\"pluginSearchDirs\": [\".\"],\n\t\"overrides\": [{ \"files\": \"*.svelte\", \"options\": { \"parser\": \"svelte\" } }]\n}\n"
  },
  {
    "path": "interface/LICENSE",
    "content": "ESP32-SvelteKit is distributed with two licenses for different sections of the\ncode. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3\nand is therefore distributed said license. The front end code is distributed\nunder the MIT License.\n\nMIT License\n\nCopyright (C) 2023 - 2024 theelims\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "interface/package.json",
    "content": "{\n\t\"name\": \"ESP32-Sveltekit Template\",\n\t\"version\": \"0.2.0\",\n\t\"private\": true,\n\t\"scripts\": {\n\t\t\"dev\": \"vite dev --host\",\n\t\t\"build\": \"vite build\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"check\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\",\n\t\t\"check:watch\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch\",\n\t\t\"lint\": \"prettier --plugin-search-dir . --check . && eslint .\",\n\t\t\"format\": \"prettier --plugin-search-dir . --write .\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@iconify-json/tabler\": \"^1.2.19\",\n\t\t\"@sveltejs/adapter-static\": \"^3.0.8\",\n\t\t\"@sveltejs/kit\": \"^2.22.3\",\n\t\t\"@sveltejs/vite-plugin-svelte\": \"^4.0.4\",\n\t\t\"@tailwindcss/vite\": \"^4.1.11\",\n\t\t\"@types/msgpack-lite\": \"^0.1.11\",\n\t\t\"@typescript-eslint/eslint-plugin\": \"^8.36.0\",\n\t\t\"@typescript-eslint/parser\": \"^8.36.0\",\n\t\t\"daisyui\": \"^5.0.46\",\n\t\t\"eslint\": \"^9.30.1\",\n\t\t\"eslint-config-prettier\": \"^10.1.5\",\n\t\t\"eslint-plugin-svelte\": \"^3.10.1\",\n\t\t\"prettier\": \"^3.6.2\",\n\t\t\"prettier-plugin-svelte\": \"^3.4.0\",\n\t\t\"prettier-plugin-tailwindcss\": \"^0.6.14\",\n\t\t\"svelte\": \"^5.35.5\",\n\t\t\"svelte-check\": \"^4.2.2\",\n\t\t\"svelte-focus-trap\": \"^1.2.0\",\n\t\t\"tailwindcss\": \"^4.1.11\",\n\t\t\"terser\": \"^5.44.0\",\n\t\t\"tslib\": \"^2.8.1\",\n\t\t\"typescript\": \"^5.8.3\",\n\t\t\"unplugin-icons\": \"^22.1.0\",\n\t\t\"vite\": \"^5.4.19\"\n\t},\n\t\"type\": \"module\",\n\t\"dependencies\": {\n\t\t\"chart.js\": \"^4.5.0\",\n\t\t\"chartjs-adapter-luxon\": \"^1.3.1\",\n\t\t\"compare-versions\": \"^6.1.1\",\n\t\t\"jwt-decode\": \"^4.0.0\",\n\t\t\"luxon\": \"^3.7.1\",\n\t\t\"msgpack-lite\": \"^0.1.26\",\n\t\t\"svelte-dnd-action\": \"^0.9.65\",\n\t\t\"svelte-modals\": \"^2.0.1\"\n\t}\n}\n"
  },
  {
    "path": "interface/src/app.css",
    "content": "@import \"tailwindcss\";\n@plugin \"daisyui\" {\n    themes: corporate --default, business --prefersdark;\n}\n\n/*\n  The default border color has changed to `currentcolor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentcolor);\n  }\n}\n"
  },
  {
    "path": "interface/src/app.d.ts",
    "content": "// See https://kit.svelte.dev/docs/types#app\n// for information about these interfaces\n\n/// <reference types=\"@sveltejs/kit\" />\n/// <reference types=\"unplugin-icons/types/svelte\" />\n\ndeclare global {\n\tnamespace App {\n\t\t// interface Error {}\n\t\t// interface Locals {}\n\t\t// interface PageData {}\n\t\t// interface Platform {}\n\t}\n}\n\nexport {};\n"
  },
  {
    "path": "interface/src/app.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<link rel=\"icon\" href=\"%sveltekit.assets%/favicon.png\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width\" />\n\t\t%sveltekit.head%\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">%sveltekit.body%</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "interface/src/lib/DaisyUiHelper.ts",
    "content": "export function daisyColor(name: string, opacity: number = 100) {\n\tconst color = getComputedStyle(document.documentElement).getPropertyValue(name);\n\t// console.debug(`daisyColor: name=${name}, color=${color}, opacity=${opacity}`);\n\t// console.debug(`${color}`);\n\t// add transparency to the color if opacity is less than 100\n\tif (opacity < 100) {\n\t\t// Convert opacity to a percentage\n\t\tconst alpha = Math.min(Math.max(Math.round(opacity), 0), 100) / 100;\n\t\t// Remove any existing alpha value and trailing ')' from the oklch color\n\t\tconst oklchColor = color.replace(/(\\/\\s*\\d+(\\.\\d+)?\\))|\\)$/, '').trim();\n\t\t// Append the new alpha value\n\t\t// console.debug(`oklchColor: ${oklchColor} / ${alpha})`);\n\t\treturn `${oklchColor} / ${alpha})`;\n\t}\n\treturn `${color}`; //   / ${Math.min(Math.max(Math.round(opacity), 0), 100)}%)`;\n}\n"
  },
  {
    "path": "interface/src/lib/components/BatteryIndicator.svelte",
    "content": "<script lang=\"ts\">\n\timport Battery0 from '~icons/tabler/battery';\n\timport Battery25 from '~icons/tabler/battery-1';\n\timport Battery50 from '~icons/tabler/battery-2';\n\timport Battery75 from '~icons/tabler/battery-3';\n\timport Battery100 from '~icons/tabler/battery-4';\n\timport BatteryCharging from '~icons/tabler/battery-charging-2';\n\n\tlet { charging = false, soc = 100, class: className = '' } = $props();\n</script>\n\n<div class=\"tooltip tooltip-bottom\" data-tip=\"{soc} %\">\n\t{#if charging}\n\t\t<BatteryCharging class=\"{className} -rotate-90 animate-pulse\" />\n\t{:else if soc > 75}\n\t\t<Battery100 class=\"{className} -rotate-90\" />\n\t{:else if soc > 55}\n\t\t<Battery75 class=\"{className} -rotate-90\" />\n\t{:else if soc > 30}\n\t\t<Battery50 class=\"{className} -rotate-90\" />\n\t{:else if soc > 5}\n\t\t<Battery25 class=\"{className} -rotate-90\" />\n\t{:else}\n\t\t<Battery0 class=\"{className} text-error -rotate-90 animate-pulse\" />\n\t{/if}\n</div>\n"
  },
  {
    "path": "interface/src/lib/components/Collapsible.svelte",
    "content": "<script lang=\"ts\">\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport Down from '~icons/tabler/chevron-down';\n\timport Alert from '~icons/tabler/alert-hexagon';\n\n\tinterface Props {\n\t\topen?: boolean;\n\t\topened?: any;\n\t\tclosed?: any;\n\t\tcollapsible?: boolean;\n\t\ticon?: import('svelte').Snippet;\n\t\ttitle?: import('svelte').Snippet;\n\t\tchildren?: import('svelte').Snippet;\n\t\tclass?: string;\n\t\tisDirty?: boolean;\n\t}\n\n\tlet {\n\t\topen = $bindable(false),\n\t\topened,\n\t\tclosed,\n\t\ticon,\n\t\ttitle,\n\t\tchildren,\n\t\tclass: className = '',\n\t\tisDirty = false\n\t}: Props = $props();\n\n\tfunction openCollapsible() {\n\t\topen = !open;\n\t\tif (open) {\n\t\t\tif (opened) opened();\n\t\t} else {\n\t\t\tif (closed) closed();\n\t\t}\n\t}\n</script>\n\n<div class=\"{className} relative grid w-full max-w-2xl self-center overflow-hidden\">\n\t{#if isDirty}\n\t\t<div class=\"absolute left-0 top-0 w-1.5 h-full bg-red-300\"></div>\n\t{/if}\n\t<div class=\"min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium\">\n\t\t<span class=\"inline-flex items-center\">\n\t\t\t{@render icon?.()}\n\t\t\t{@render title?.()}\n\t\t\t{#if isDirty}\n\t\t\t\t<div data-tip=\"There are unsaved changes.\" class=\"tooltip tooltip-right tooltip-error\">\n\t\t\t\t\t<Alert class=\"text-error lex-shrink-0 ml-2 h-6 w-6 self-end cursor-help\" />\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t</span>\n\t\t<button class=\"btn btn-circle btn-ghost btn-sm\" onclick={() => openCollapsible()}>\n\t\t\t<Down\n\t\t\t\tclass=\"text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open\n\t\t\t\t\t? 'rotate-180'\n\t\t\t\t\t: ''}\"\n\t\t\t/>\n\t\t</button>\n\t</div>\n\t{#if open}\n\t\t<div\n\t\t\tclass=\"flex flex-col gap-2 p-4 pt-0\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t{@render children?.()}\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "interface/src/lib/components/ConfirmDialog.svelte",
    "content": "<script lang=\"ts\">\n\timport { modals } from 'svelte-modals';\n\timport { focusTrap } from 'svelte-focus-trap';\n\timport { fly } from 'svelte/transition';\n\timport Cancel from '~icons/tabler/x';\n\timport Check from '~icons/tabler/check';\n\n\t// provided by <Modals />\n\n\tinterface Props {\n\t\tisOpen?: boolean;\n\t\ttitle: string;\n\t\tmessage: string;\n\t\tonConfirm: any;\n\t\tlabels?: any;\n\t}\n\n\tlet {\n\t\tisOpen,\n\t\ttitle,\n\t\tmessage,\n\t\tonConfirm,\n\t\tlabels = {\n\t\t\tcancel: { label: 'Cancel', icon: Cancel },\n\t\t\tconfirm: { label: 'OK', icon: Check }\n\t\t}\n\t}: Props = $props();\n</script>\n\n{#if isOpen}\n\t{@const SvelteComponent = labels?.confirm.icon}\n\t<div\n\t\trole=\"dialog\"\n\t\tclass=\"pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4\"\n\t\ttransition:fly={{ y: 50 }}\n\t\tuse:focusTrap\n\t>\n\t\t<div\n\t\t\tclass=\"rounded-box bg-base-100 shadow-secondary/30 pointer-events-auto flex w-full max-w-xs sm:max-w-sm md:max-w-md flex-col justify-between p-4 shadow-lg overflow-hidden\"\n\t\t>\n\t\t\t<h2 class=\"text-base-content text-start text-2xl font-bold break-words\">{title}</h2>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<p class=\"text-base-content mb-1 text-start break-words whitespace-normal\">{@html message}</p>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<div class=\"flex justify-end gap-2\">\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn btn-primary inline-flex items-center\"\n\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\tmodals.close();\n\t\t\t\t\t}}><labels.cancel.icon class=\"h-5 w-5\" /><span>{labels?.cancel.label}</span></button\n\t\t\t\t>\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn btn-warning text-warning-content inline-flex items-center\"\n\t\t\t\t\tonclick={onConfirm}\n\t\t\t\t\t><SvelteComponent class=\"h-5 w-5\" /><span>{labels?.confirm.label}</span></button\n\t\t\t\t>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/lib/components/DraggableList.svelte",
    "content": "<script lang=\"ts\">\n\timport { dndzone } from 'svelte-dnd-action';\n\timport type { Snippet } from 'svelte';\n\n\tinterface Props {\n\t\titems: any[];\n\t\tonReorder?: (reorderedItems: any[]) => void;\n\t\tflipDurationMs?: number;\n\t\tdragDisabled?: boolean;\n\t\tclass?: string;\n\t\tchildren: Snippet<[{ item: any; index: number; originalItem: any }]>;\n\t}\n\n\tlet {\n\t\titems,\n\t\tonReorder = () => {},\n\t\tflipDurationMs = 200,\n\t\tdragDisabled = false,\n\t\tclass: className = '',\n\t\tchildren\n\t}: Props = $props();\n\n\t// Create a state array with IDs for drag-and-drop functionality\n\tlet itemsWithIds: any[] = $state([]);\n\n\t// Update the drag-and-drop array whenever items change\n\t$effect(() => {\n\t\titemsWithIds = items.map((item, index) => ({\n\t\t\t...item,\n\t\t\tid: item.id || `dnd-item-${index}-${Date.now()}` // Generate unique ID with timestamp\n\t\t}));\n\t});\n\n\tfunction handleSort(e: any) {\n\t\t// Update the visual drag-and-drop array immediately\n\t\titemsWithIds = e.detail.items;\n\t}\n\n\tfunction handleFinalizeSort(e: any) {\n\t\t// Remove only temporary IDs, preserve original device IDs\n\t\tconst reorderedItems = e.detail.items.map((item: any) => {\n\t\t\t// If this is a temporary ID we added (string starting with 'dnd-item-'), remove it\n\t\t\tif (typeof item.id === 'string' && item.id.startsWith('dnd-item-')) {\n\t\t\t\tconst { id, ...itemWithoutTempId } = item;\n\t\t\t\treturn itemWithoutTempId;\n\t\t\t}\n\t\t\t// Otherwise, keep the item as-is (preserving original numeric IDs)\n\t\t\treturn item;\n\t\t});\n\n\t\t// Call the parent's reorder handler\n\t\tonReorder(reorderedItems);\n\t}\n\n</script>\n\n<section\n\tuse:dndzone={{\n\t\titems: itemsWithIds,\n\t\tflipDurationMs,\n\t\tdropTargetStyle: {}, // This is to actively clear default styles\n\t\tdropTargetClasses: ['dragzone-outline'], // This applies custom styling\n\t\tdragDisabled\n\t}}\n\tonconsider={handleSort}\n\tonfinalize={handleFinalizeSort}\n\tclass={className}\n>\n\t{#each itemsWithIds as item, index (item.id)}\n\t\t{@render children({ item, index, originalItem: items[index] })}\n\t{/each}\n</section>\n\n<style>\n\t@reference \"$src/app.css\";\n\t:global(.dragzone-outline) {\n\t\t@apply outline-solid outline-2 outline-(--color-primary);\n\t}\n\t:global(#dnd-action-dragged-el) {\n\t\t@apply outline-solid outline-2 outline-current;\n\n\t}\n</style>\n"
  },
  {
    "path": "interface/src/lib/components/FirmwareUpdateDialog.svelte",
    "content": "<script lang=\"ts\">\n\timport { modals, onBeforeClose } from 'svelte-modals';\n\timport { focusTrap } from 'svelte-focus-trap';\n\timport { fly } from 'svelte/transition';\n\timport { telemetry } from '$lib/stores/telemetry';\n\timport Cancel from '~icons/tabler/x';\n\timport Check from '~icons/tabler/check';\n\timport AlertCircle from '~icons/tabler/alert-circle';\n\timport Refresh from '~icons/tabler/refresh';\n\timport Loader from '~icons/tabler/loader-2';\n\n\tinterface Props {\n\t\tisOpen: boolean;\n\t\ttitle?: string;\n\t}\n\n\tlet { isOpen, title = 'Updating Firmware' }: Props = $props();\n\n\t// Use telemetry store for all status information\n\tlet currentStatus = $derived($telemetry.ota_status.status);\n\tlet currentProgress = $derived($telemetry.ota_status.progress);\n\tlet currentError = $derived($telemetry.ota_status.error);\n\n\tlet updating = $derived(\n\t\tcurrentStatus === 'preparing' || currentStatus === 'progress' || currentStatus === 'none'\n\t);\n\n\tlet displayMessage = $derived.by(() => {\n\t\tif (currentStatus === 'error') return currentError || 'Update failed.';\n\t\tif (currentStatus === 'finished') return 'Update finished successfully.';\n\t\tif (currentStatus === 'progress') return 'Updating...';\n\t\tif (currentStatus === 'preparing') return 'Preparing...';\n\t\treturn 'Waiting for update...';\n\t});\n\n\tconst RELOAD_COUNTDOWN_SECONDS = 10;\n\t\n\tlet timerId: number | undefined = $state();\n\tlet countdown: number = $state(RELOAD_COUNTDOWN_SECONDS);\n\n\tfunction hardReload() {\n\t\t// Hard reload with cache bust to load new firmware UI\n\t\twindow.location.href = window.location.href.split('?')[0] + '?t=' + Date.now();\n\t}\n\n\t$effect(() => {\n\t\tif (currentStatus === 'finished') {\n\t\t\tcountdown = RELOAD_COUNTDOWN_SECONDS;\n\n\t\t\t// Single timer: countdown and reload when reaching zero\n\t\t\ttimerId = setInterval(() => {\n\t\t\t\tcountdown--;\n\t\t\t\tif (countdown <= 0) {\n\t\t\t\t\tclearInterval(timerId);\n\t\t\t\t\tmodals.closeAll();\n\t\t\t\t\thardReload();\n\t\t\t\t}\n\t\t\t}, 1000) as unknown as number;\n\n\t\t\treturn () => {\n\t\t\t\tif (timerId) clearInterval(timerId);\n\t\t\t};\n\t\t}\n\t});\n\n\tonBeforeClose(() => {\n\t\tif (updating) {\n\t\t\treturn false; // Prevent closing during update\n\t\t}\n\t\t// Reset status when closing error dialog to allow fresh start\n\t\tif (currentStatus === 'error') {\n\t\t\ttelemetry.setOTAStatus({ status: 'none', progress: 0, error: '' });\n\t\t}\n\t\treturn true;\n\t});\n</script>\n\n{#if isOpen}\n\t<div\n\t\trole=\"dialog\"\n\t\tclass=\"pointer-events-none fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm\"\n\t\ttransition:fly={{ y: 50, duration: 300 }}\n\t\tuse:focusTrap\n\t>\n\t\t<div\n\t\t\tclass=\"bg-base-100 shadow-2xl rounded-2xl pointer-events-auto flex max-h-full w-full max-w-md flex-col border border-base-300\"\n\t\t>\n\t\t\t<!-- Header -->\n\t\t\t<div class=\"flex items-center justify-between p-6 pb-4\">\n\t\t\t\t<h2 class=\"text-base-content text-xl font-bold\">{title}</h2>\n\t\t\t</div>\n\n\t\t\t<!-- Progress Content -->\n\t\t\t<div class=\"flex flex-col items-center justify-center px-6 py-8 space-y-6\">\n\t\t\t\t{#if currentStatus === 'progress' || currentStatus === 'preparing' || currentStatus === 'none'}\n\t\t\t\t\t<!-- Radial Progress or Indeterminate Spinner -->\n\t\t\t\t\t<div class=\"relative\">\n\t\t\t\t\t\t{#if currentStatus === 'progress' && currentProgress > 0}\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclass=\"radial-progress text-primary\"\n\t\t\t\t\t\t\t\tstyle=\"--value:{currentProgress}; --size:10rem; --thickness: 0.5rem;\"\n\t\t\t\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<span class=\"text-3xl font-bold\">{currentProgress}%</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t<!-- Indeterminate spinner matching radial size -->\n\t\t\t\t\t\t\t<div class=\"flex items-center justify-center w-40 h-40\">\n\t\t\t\t\t\t\t\t<Loader class=\"text-primary h-16 w-16 animate-spin stroke-2\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\t\t\t\t{:else if currentStatus === 'finished'}\n\t\t\t\t\t<!-- Success Icon -->\n\t\t\t\t\t<div class=\"flex items-center justify-center w-24 h-24 rounded-full bg-success/10\">\n\t\t\t\t\t\t<Check class=\"h-16 w-16 text-success\" />\n\t\t\t\t\t</div>\n\t\t\t\t{:else if currentStatus === 'error'}\n\t\t\t\t\t<!-- Error Icon -->\n\t\t\t\t\t<div class=\"flex items-center justify-center w-24 h-24 rounded-full bg-error/10\">\n\t\t\t\t\t\t<AlertCircle class=\"h-16 w-16 text-error\" />\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\n\t\t\t\t<!-- Status Message -->\n\t\t\t\t<p\n\t\t\t\t\tclass=\"text-center text-lg {currentStatus === 'error'\n\t\t\t\t\t\t? 'text-error'\n\t\t\t\t\t\t: 'text-base-content/70'}\"\n\t\t\t\t>\n\t\t\t\t\t{displayMessage}\n\t\t\t\t</p>\n\n\t\t\t\t{#if currentStatus === 'finished'}\n\t\t\t\t\t<p class=\"text-sm text-base-content/50 text-center\">\n\t\t\t\t\t\tPage will reload automatically in {countdown}\n\t\t\t\t\t\t{countdown === 1 ? 'second' : 'seconds'}...\n\t\t\t\t\t</p>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t<!-- Footer -->\n\t\t\t<div class=\"flex justify-end gap-2 p-6 pt-4 border-t border-base-300\">\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn btn-sm {currentStatus === 'finished' ? 'btn-primary' : currentStatus === 'error' ? 'btn-error' : 'btn-ghost'}\"\n\t\t\t\t\tdisabled={updating}\n\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\tif (timerId) clearInterval(timerId);\n\t\t\t\t\t\tif (currentStatus === 'finished') {\n\t\t\t\t\t\t\tmodals.closeAll();\n\t\t\t\t\t\t\thardReload();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tmodals.closeAll();\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{#if currentStatus === 'finished'}\n\t\t\t\t\t\t<Refresh class=\"h-4 w-4\" />\n\t\t\t\t\t\tRefresh Now\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<Cancel class=\"h-4 w-4\" />\n\t\t\t\t\t\tCancel\n\t\t\t\t\t{/if}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/lib/components/InfoDialog.svelte",
    "content": "<script lang=\"ts\">\n\timport { modals } from 'svelte-modals';\n\timport { focusTrap } from 'svelte-focus-trap';\n\timport { fly } from 'svelte/transition';\n\timport Check from '~icons/tabler/check';\n\n\t// provided by <Modals />\n\n\tinterface Props {\n\t\tisOpen: boolean;\n\t\ttitle: string;\n\t\tmessage: string;\n\t\tonDismiss: any;\n\t\tdismiss?: any;\n\t}\n\n\tconst {\n\t\tisOpen,\n\t\ttitle,\n\t\tmessage,\n\t\tonDismiss,\n\t\tdismiss = { label: 'Dismiss', icon: Check }\n\t}: Props = $props();\n</script>\n\n{#if isOpen}\n\t<div\n\t\trole=\"dialog\"\n\t\tclass=\"pointer-events-none fixed inset-0 z-50 flex items-center justify-center\"\n\t\ttransition:fly={{ y: 50 }}\n\t\tuse:focusTrap\n\t>\n\t\t<div\n\t\t\tclass=\"rounded-box bg-base-100 shadow-secondary/30 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg\"\n\t\t>\n\t\t\t<h2 class=\"text-base-content text-start text-2xl font-bold\">{title}</h2>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<p class=\"text-base-content mb-1 text-start\">{@html message}</p>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<div class=\"flex justify-end gap-2\">\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn btn-warning text-warning-content inline-flex items-center\"\n\t\t\t\t\tonclick={onDismiss}\n\t\t\t\t\t><dismiss.icon class=\"mr-2 h-5 w-5\" /><span>{dismiss.label}</span></button\n\t\t\t\t>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/lib/components/InputPassword.svelte",
    "content": "<script lang=\"ts\">\n\tlet show = $state(false);\n\tlet type = $derived(show ? 'text' : 'password');\n\n\tinterface Props {\n\t\tvalue?: string;\n\t\tid?: string;\n\t}\n\n\tlet { value = $bindable('') as string, id = '' as string }: Props = $props();\n\n\tfunction handleInput(e: any) {\n\t\tvalue = e.target.value;\n\t}\n</script>\n\n<div class=\"relative\">\n\t<input {type} class=\"input input-bordered w-full\" {value} oninput={handleInput} {id} />\n\t<div class=\"absolute inset-y-0 right-0 flex items-center pr-1\">\n\t\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tclass=\"text-base-content/50 h-6 {show ? 'block' : 'hidden'}\"\n\t\t\tonclick={() => (show = false)}\n\t\t\trole=\"button\"\n\t\t\taria-label=\"Hide password\"\n\t\t\ttabindex=\"0\"\n\t\t\twidth=\"40\"\n\t\t\theight=\"40\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tstroke-width=\"2\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tfill=\"none\"\n\t\t\tstroke-linecap=\"round\"\n\t\t\tstroke-linejoin=\"round\"\n\t\t>\n\t\t\t<path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t<path d=\"M10.585 10.587a2 2 0 0 0 2.829 2.828\" />\n\t\t\t<path\n\t\t\t\td=\"M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87\"\n\t\t\t/>\n\t\t\t<path d=\"M3 3l18 18\" />\n\t\t</svg>\n\n\t\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tclass=\"text-base-content/50 h-6 {show ? 'hidden' : 'block'}\"\n\t\t\tonclick={() => (show = true)}\n\t\t\trole=\"button\"\n\t\t\taria-label=\"Show password\"\n\t\t\ttabindex=\"0\"\n\t\t\twidth=\"40\"\n\t\t\theight=\"40\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tstroke-width=\"2\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tfill=\"none\"\n\t\t\tstroke-linecap=\"round\"\n\t\t\tstroke-linejoin=\"round\"\n\t\t>\n\t\t\t<path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t<path d=\"M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0\" />\n\t\t\t<path d=\"M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6\" />\n\t\t</svg>\n\t</div>\n</div>\n"
  },
  {
    "path": "interface/src/lib/components/RSSIIndicator.svelte",
    "content": "<script lang=\"ts\">\n\timport WiFi from '~icons/tabler/wifi';\n\timport WiFi0 from '~icons/tabler/wifi-0';\n\timport WiFi1 from '~icons/tabler/wifi-1';\n\timport WiFi2 from '~icons/tabler/wifi-2';\n\n\tlet { showDBm = false, rssi_dbm = 0, ssid = '', class: className = '' } = $props();\n\n\t$effect(() => {\n\t\tif (ssid === '') {\n\t\t\tssid = 'Unknown';\n\t\t}\n\t});\n</script>\n\n<div class=\"indicator\">\n\t<div class=\"tooltip tooltip-left\" data-tip={ssid}>\n\t\t{#if showDBm}\n\t\t\t<span class=\"indicator-item indicator-start badge badge-accent badge-outline badge-xs\">\n\t\t\t\t{rssi_dbm} dBm\n\t\t\t</span>\n\t\t{/if}\n\t\t{#if rssi_dbm >= -55}\n\t\t\t<WiFi class={className} />\n\t\t{:else if rssi_dbm >= -75}\n\t\t\t<div class=\"{className} relative\">\n\t\t\t\t<WiFi class=\"absolute inset-0 h-full w-full opacity-30\" />\n\t\t\t\t<WiFi2 class=\"absolute inset-0 h-full w-full\" />\n\t\t\t</div>\n\t\t{:else if rssi_dbm >= -85}\n\t\t\t<div class=\"{className} relative\">\n\t\t\t\t<WiFi class=\"absolute inset-0 h-full w-full opacity-30\" />\n\t\t\t\t<WiFi1 class=\"absolute inset-0 h-full w-full\" />\n\t\t\t</div>\n\t\t{:else}\n\t\t\t<div class=\"{className} relative\">\n\t\t\t\t<WiFi class=\"absolute inset-0 h-full w-full opacity-30\" />\n\t\t\t\t<WiFi0 class=\"absolute inset-0 h-full w-full\" />\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</div>\n"
  },
  {
    "path": "interface/src/lib/components/SettingsCard.svelte",
    "content": "<script lang=\"ts\">\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport Down from '~icons/tabler/chevron-down';\n\timport Alert from '~icons/tabler/alert-hexagon';\n\n\tinterface Props {\n\t\topen?: boolean;\n\t\tcollapsible?: boolean;\n\t\ticon?: import('svelte').Snippet;\n\t\ttitle?: import('svelte').Snippet;\n\t\tchildren?: import('svelte').Snippet;\n\t\tmaxwidth?: string;\n\t\tisDirty?: boolean;\n\t}\n\n\tlet {\n\t\topen = $bindable(true),\n\t\tcollapsible = true,\n\t\ticon,\n\t\ttitle,\n\t\tchildren,\n\t\tmaxwidth = 'max-w-2xl',\n\t\tisDirty = false\n\t}: Props = $props();\n</script>\n\n{#if collapsible}\n\t<div\n\t\tclass=\"bg-base-200 rounded-box shadow-primary/50 relative grid w-full {maxwidth} self-center overflow-hidden shadow-lg m-10\"\n\t>\n\t\t{#if isDirty}\n\t\t\t<div class=\"absolute left-0 top-0 w-1.5 h-full bg-red-300\"></div>\n\t\t{/if}\n\t\t<div\n\t\t\tclass=\"min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium\"\n\t\t>\n\t\t\t<span class=\"inline-flex items-center\">\n\t\t\t\t{@render icon?.()}\n\t\t\t\t{@render title?.()}\n\t\t\t\t{#if isDirty}\n\t\t\t\t\t<div data-tip=\"There are unsaved changes.\" class=\"tooltip tooltip-right tooltip-error\">\n\t\t\t\t\t\t<Alert class=\"text-error lex-shrink-0 ml-2 h-6 w-6 self-end cursor-help\" />\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</span>\n\t\t\t<button\n\t\t\t\tclass=\"btn btn-circle btn-ghost btn-sm\"\n\t\t\t\tonclick={() => {\n\t\t\t\t\topen = !open;\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Down\n\t\t\t\t\tclass=\"text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open\n\t\t\t\t\t\t? 'rotate-180'\n\t\t\t\t\t\t: ''}\"\n\t\t\t\t/>\n\t\t\t</button>\n\t\t</div>\n\t\t{#if open}\n\t\t\t<div\n\t\t\t\tclass=\"flex flex-col gap-2 p-4 pt-0\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t{@render children?.()}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n{:else}\n\t<div\n\t\tclass=\"bg-base-200 rounded-box shadow-primary/50 relative grid w-full {maxwidth} self-center overflow-hidden shadow-lg m-10\"\n\t>\n\t\t{#if isDirty}\n\t\t\t<div class=\"absolute left-0 top-0 w-1.5 h-full bg-red-300\"></div>\n\t\t{/if}\n\t\t<div class=\"min-h-16 w-full p-4 text-xl font-medium\">\n\t\t\t<span class=\"inline-flex items-center\">\n\t\t\t\t{@render icon?.()}\n\t\t\t\t{@render title?.()}\n\t\t\t\t{#if isDirty}\n\t\t\t\t\t<div data-tip=\"There are unsaved changes.\" class=\"tooltip tooltip-right tooltip-error\">\n\t\t\t\t\t\t<Alert class=\"text-error lex-shrink-0 ml-2 h-6 w-6 self-end cursor-help\" />\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</span>\n\t\t</div>\n\t\t<div class=\"flex flex-col gap-2 p-4 pt-0\">\n\t\t\t{@render children?.()}\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/lib/components/Spinner.svelte",
    "content": "<script lang=\"ts\">\n\timport Loader from '~icons/tabler/loader-2';\n\n\tlet { text = \"Loading...\"} = $props();\n\n</script>\n\n<div class=\"flex w-full flex-col items-center justify-center p-6\">\n\t<Loader class=\"text-primary h-14 w-auto animate-spin stroke-2\" />\n\t<p class=\"text-xl\">{text}</p>\n</div>\n"
  },
  {
    "path": "interface/src/lib/components/UpdateIndicator.svelte",
    "content": "<script lang=\"ts\">\n\timport { page } from '$app/state';\n\timport { modals } from 'svelte-modals';\n\timport type { ModalComponent } from 'svelte-modals';\n\timport { user } from '$lib/stores/user';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport Firmware from '~icons/tabler/refresh-alert';\n\timport Cancel from '~icons/tabler/x';\n\timport CloudDown from '~icons/tabler/cloud-download';\n\timport FirmwareUpdateDialog from '$lib/components/FirmwareUpdateDialog.svelte';\n\timport { compareVersions } from 'compare-versions';\n\timport { onMount } from 'svelte';\n\n\tinterface Props {\n\t\tupdate?: boolean;\n\t}\n\n\tlet { update = $bindable(false) }: Props = $props();\n\n\tlet firmwareVersion: string = $state('');\n\tlet firmwareDownloadLink: string;\n\n\tasync function getGithubAPI() {\n\t\tconst githubUrl = `https://api.github.com/repos/${page.data.github}/releases/latest`;\n\t\ttry {\n\t\t\tconst response = await fetch(githubUrl, {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\taccept: 'application/vnd.github+json',\n\t\t\t\t\t'X-GitHub-Api-Version': '2022-11-28'\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (response.status !== 200) {\n\t\t\t\tnotifications.error('Failed to fetch latest release from GitHub.', 5000);\n\t\t\t\tthrow new Error(`Failed to fetch latest release from ${githubUrl}`);\n\t\t\t}\n\t\t\tconst results = await response.json();\n\n\t\t\tupdate = false;\n\t\t\tfirmwareVersion = '';\n\n\t\t\tif (compareVersions(results.tag_name, page.data.features.firmware_version) === 1) {\n\t\t\t\t// iterate over assets and find the correct one\n\t\t\t\tfor (let i = 0; i < results.assets.length; i++) {\n\t\t\t\t\t// check if the asset is of type *.bin\n\t\t\t\t\tif (\n\t\t\t\t\t\tresults.assets[i].name.includes('.bin') &&\n\t\t\t\t\t\tresults.assets[i].name.includes(page.data.features.firmware_built_target)\n\t\t\t\t\t) {\n\t\t\t\t\t\tupdate = true;\n\t\t\t\t\t\tfirmwareVersion = results.tag_name;\n\t\t\t\t\t\tfirmwareDownloadLink = results.assets[i].browser_download_url;\n\t\t\t\t\t\tnotifications.info('Firmware update available.', 5000);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn(error);\n\t\t}\n\t}\n\n\tasync function postGithubDownload(url: string) {\n\t\ttry {\n\t\t\tconst apiResponse = await fetch('/rest/downloadUpdate', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ download_url: url })\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tonMount(() => {\n\t\tif (page.data.features.download_firmware && (!page.data.features.security || $user.admin)) {\n\t\t\tgetGithubAPI();\n\t\t\tconst interval = setInterval(\n\t\t\t\tasync () => {\n\t\t\t\t\tgetGithubAPI();\n\t\t\t\t},\n\t\t\t\t60 * 60 * 1000\n\t\t\t); // once per hour\n\t\t}\n\t});\n\n\tfunction confirmGithubUpdate(url: string) {\n\t\tmodals.open(ConfirmDialog as unknown as ModalComponent<any>, {\n\t\t\ttitle: 'Confirm flashing new firmware to the device',\n\t\t\tmessage: 'Are you sure you want to overwrite the existing firmware with a new one?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Update', icon: CloudDown }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tpostGithubDownload(url);\n\t\t\t\tmodals.open(FirmwareUpdateDialog, {\n\t\t\t\t\ttitle: 'Downloading Firmware'\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n</script>\n\n{#if update}\n\t<button\n\t\tclass=\"btn btn-square btn-ghost h-9 w-9\"\n\t\tonclick={() => confirmGithubUpdate(firmwareDownloadLink)}\n\t>\n\t\t<span\n\t\t\tclass=\"indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1\"\n\t\t\t>{firmwareVersion}</span\n\t\t>\n\t\t<Firmware class=\"h-7 w-7\" />\n\t</button>\n{/if}\n"
  },
  {
    "path": "interface/src/lib/components/toasts/Toast.svelte",
    "content": "<script>\n\timport { flip } from 'svelte/animate';\n\timport { fly } from 'svelte/transition';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport error from '~icons/tabler/circle-x';\n\timport success from '~icons/tabler/circle-check';\n\timport warning from '~icons/tabler/alert-triangle';\n\timport info from '~icons/tabler/info-circle';\n\n\t/** @type {{theme?: any, icon?: any}} */\n\tlet {\n\t\ttheme = {\n\t\t\terror: 'alert-error',\n\t\t\tsuccess: 'alert-success',\n\t\t\twarning: 'alert-warning',\n\t\t\tinfo: 'alert-info'\n\t\t},\n\t\ticon = {\n\t\t\terror: error,\n\t\t\tsuccess: success,\n\t\t\twarning: warning,\n\t\t\tinfo: info\n\t\t}\n\t} = $props();\n</script>\n\n<div class=\"toast toast-end z-[100] mr-4\">\n\t{#each $notifications as notification (notification.id)}\n\t\t{@const SvelteComponent = icon[notification.type]}\n\t\t<div\n\t\t\tanimate:flip={{ duration: 400 }}\n\t\t\tclass=\"alert animate-none {theme[notification.type]}\"\n\t\t\tin:fly={{ y: 100, duration: 400 }}\n\t\t\tout:fly={{ x: 100, duration: 400 }}\n\t\t>\n\t\t\t<SvelteComponent class=\"h-6 w-6 shrink-0\" />\n\t\t\t<span>{@html notification.message}</span>\n\t\t</div>\n\t{/each}\n</div>\n"
  },
  {
    "path": "interface/src/lib/components/toasts/notifications.ts",
    "content": "import { writable, derived, type Writable } from 'svelte/store';\n\ntype StateType = 'info' | 'success' | 'warning' | 'error';\n\ntype State = {\n\tid: string;\n\ttype: StateType;\n\tmessage: string;\n};\n\nfunction createNotificationStore() {\n\tconst state: State[] = [];\n\tconst notifications = writable(state);\n\tconst { subscribe } = notifications;\n\n\tfunction send(message: string, type: StateType = 'info', timeout: number) {\n\t\tconst id = generateId();\n\t\tsetTimeout(() => {\n\t\t\tnotifications.update((state) => {\n\t\t\t\treturn state.filter((n) => n.id !== id);\n\t\t\t});\n\t\t}, timeout);\n\t\tnotifications.update((state) => {\n\t\t\treturn [...state, { id, type, message }];\n\t\t});\n\t}\n\n\treturn {\n\t\tsubscribe,\n\t\tsend,\n\t\terror: (msg: string, timeout: number) => send(msg, 'error', timeout),\n\t\twarning: (msg: string, timeout: number) => send(msg, 'warning', timeout),\n\t\tinfo: (msg: string, timeout: number) => send(msg, 'info', timeout),\n\t\tsuccess: (msg: string, timeout: number) => send(msg, 'success', timeout)\n\t};\n}\n\nfunction generateId() {\n\treturn '_' + Math.random().toString(36).substr(2, 9);\n}\n\nexport const notifications = createNotificationStore();\n"
  },
  {
    "path": "interface/src/lib/stores/analytics.ts",
    "content": "import { type Analytics } from '$lib/types/models';\nimport { writable } from 'svelte/store';\n\nlet analytics_data = {\n\tuptime: <number[]>[],\n\tfree_heap: <number[]>[],\n\tused_heap: <number[]>[],\n\ttotal_heap: <number[]>[],\n\tmin_free_heap: <number[]>[],\n\tmax_alloc_heap: <number[]>[],\n\tfs_used: <number[]>[],\n\tfs_total: <number[]>[],\n\tcore_temp: <number[]>[],\n\tfree_psram: <number[]>[],\n\tused_psram: <number[]>[],\n\tpsram_size: <number[]>[],\n};\n\nconst maxAnalyticsData = 1000; // roughly 33 Minutes of data at 1 update per 2 seconds\n\nfunction createAnalytics() {\n\tconst { subscribe, update } = writable(analytics_data);\n\n\treturn {\n\t\tsubscribe,\n\t\taddData: (content: Analytics) => {\n\t\t\tupdate((analytics_data) => ({\n\t\t\t\t...analytics_data,\n\t\t\t\tuptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),\n\t\t\t\tfree_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(-maxAnalyticsData),\n\t\t\t\tused_heap: [...analytics_data.used_heap, content.used_heap / 1000].slice(-maxAnalyticsData),\n\t\t\t\ttotal_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(\n\t\t\t\t\t-maxAnalyticsData\n\t\t\t\t),\n\t\t\t\tmin_free_heap: [...analytics_data.min_free_heap, content.min_free_heap / 1000].slice(\n\t\t\t\t\t-maxAnalyticsData\n\t\t\t\t),\n\t\t\t\tmax_alloc_heap: [...analytics_data.max_alloc_heap, content.max_alloc_heap / 1000].slice(\n\t\t\t\t\t-maxAnalyticsData\n\t\t\t\t),\n\t\t\t\tfs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(-maxAnalyticsData),\n\t\t\t\tfs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(-maxAnalyticsData),\n\t\t\t\tcore_temp: [...analytics_data.core_temp, content.core_temp].slice(-maxAnalyticsData),\n\t\t\t\tfree_psram: [...analytics_data.free_psram, content.free_psram / 1000].slice(-maxAnalyticsData),\n\t\t\t\tused_psram: [...analytics_data.used_psram, content.used_psram / 1000].slice(-maxAnalyticsData),\n\t\t\t\tpsram_size: [...analytics_data.psram_size, content.psram_size / 1000].slice(-maxAnalyticsData),\n\t\t\t}));\n\t\t}\n\t};\n}\n\nexport const analytics = createAnalytics();\n"
  },
  {
    "path": "interface/src/lib/stores/battery.ts",
    "content": "import { type Battery } from '$lib/types/models';\nimport { writable } from 'svelte/store';\n\nlet battery_history = {\n    soc: <number[]>[],\n    charging: <number[]>[],\n    timestamp: <number[]>[]\n};\n\nconst maxAnalyticsData = 3600; // roughly 5 Hours of data at 1 update per 5 seconds\n\nfunction createBatteryHistory() {\n\tconst { subscribe, update } = writable(battery_history);\n\n\treturn {\n\t\tsubscribe,\n\t\taddData: (content: Battery) => {\n\t\t\tupdate((battery_history) => ({\n\t\t\t\t...battery_history,\n\t\t\t\tsoc: [...battery_history.soc, content.soc].slice(-maxAnalyticsData),\n\t\t\t\tcharging: [...battery_history.charging, content.charging ? 1 : 0].slice(-maxAnalyticsData),\n\t\t\t\ttimestamp: [...battery_history.timestamp, Date.now()].slice(-maxAnalyticsData)\n\t\t\t}));\n\t\t}\n\t};\n}\n\nexport const batteryHistory = createBatteryHistory();\n"
  },
  {
    "path": "interface/src/lib/stores/socket.ts",
    "content": "import { writable } from 'svelte/store';\nimport msgpack from 'msgpack-lite';\n\nfunction createWebSocket() {\n\tlet listeners = new Map<string, Set<(data?: unknown) => void>>();\n\tconst { subscribe, set } = writable(false);\n\tconst socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;\n\ttype SocketEvent = (typeof socketEvents)[number];\n\tlet unresponsiveTimeoutId: number;\n\tlet reconnectTimeoutId: number;\n\tlet ws: WebSocket;\n\tlet socketUrl: string | URL;\n\tlet event_use_json = false;\n\n\tfunction init(url: string | URL, use_json: boolean = false) {\n\t\tsocketUrl = url;\n\t\tevent_use_json = use_json;\n\t\tconnect();\n\t}\n\n\tfunction disconnect(reason: SocketEvent, event?: Event) {\n\t\t//console.log('disconnect', reason, event);\n\t\tws.close();\n\t\tset(false);\n\t\tclearTimeout(unresponsiveTimeoutId);\n\t\tclearTimeout(reconnectTimeoutId);\n\t\tlisteners.get(reason)?.forEach((listener) => listener(event));\n\t\treconnectTimeoutId = setTimeout(connect, 1000);\n\t}\n\n\tfunction connect() {\n\t\t//console.log('connect');\n\t\tws = new WebSocket(socketUrl);\n\t\tws.binaryType = 'arraybuffer';\n\t\tws.onopen = (ev) => {\n\t\t\tset(true);\n\t\t\tclearTimeout(reconnectTimeoutId);\n\t\t\tlisteners.get('open')?.forEach((listener) => listener(ev));\n\t\t\tfor (const event of listeners.keys()) {\n\t\t\t\tif (socketEvents.includes(event as SocketEvent)) continue;\n\t\t\t\tsendEvent('subscribe', event);\n\t\t\t}\n\t\t};\n\t\tws.onmessage = (message) => {\n\t\t\tresetUnresponsiveCheck();\n\t\t\tlet payload = message.data;\n\n\t\t\tconst binary = payload instanceof ArrayBuffer;\n\t\t\tlisteners.get(binary ? 'binary' : 'message')?.forEach((listener) => listener(payload));\n\t\t\ttry {\n\t\t\t\tpayload = binary ? msgpack.decode(new Uint8Array(payload)) : JSON.parse(payload);\n\t\t\t} catch (error) {\n\t\t\t\tlisteners.get('error')?.forEach((listener) => listener(error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlisteners.get('json')?.forEach((listener) => listener(payload));\n\t\t\tconst { event, data } = payload;\n\t\t\tif (event) listeners.get(event)?.forEach((listener) => listener(data));\n\t\t};\n\t\tws.onerror = (ev) => disconnect('error', ev);\n\t\tws.onclose = (ev) => disconnect('close', ev);\n\t}\n\n\tfunction unsubscribe(event: string, listener?: (data: any) => void) {\n\t\tlet eventListeners = listeners.get(event);\n\t\tif (!eventListeners) return;\n\n\t\tif (!eventListeners.size) {\n\t\t\tsendEvent('unsubscribe', event);\n\t\t}\n\t\tif (listener) {\n\t\t\teventListeners?.delete(listener);\n\t\t} else {\n\t\t\tlisteners.delete(event);\n\t\t}\n\t}\n\n\tfunction resetUnresponsiveCheck() {\n\t\tclearTimeout(unresponsiveTimeoutId);\n\t\tunresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), 2000);\n\t}\n\n\tfunction send(msg: unknown) {\n\t\tif (!ws || ws.readyState !== WebSocket.OPEN) return;\n\t\tif (event_use_json) {\n\t\t\tws.send(JSON.stringify(msg));\n\t\t} else {\n\t\t\tws.send(msgpack.encode(msg));\n\t\t}\n\t}\n\n\tfunction sendEvent(event: string, data: unknown) {\n\t\tsend({ event, data });\n\t}\n\n\treturn {\n\t\tsubscribe,\n\t\tsend,\n\t\tsendEvent,\n\t\tinit,\n\t\ton: <T>(event: string, listener: (data: T) => void): (() => void) => {\n\t\t\tlet eventListeners = listeners.get(event);\n\t\t\tif (!eventListeners) {\n\t\t\t\teventListeners = new Set();\n\t\t\t\tlisteners.set(event, eventListeners);\n\t\t\t\t\n\t\t\t\t// Only send subscription if WebSocket is open and it's not a socket event\n\t\t\t\tif (!socketEvents.includes(event as SocketEvent) && \n\t\t\t\t\tws && ws.readyState === WebSocket.OPEN) {\n\t\t\t\t\tsendEvent('subscribe', event);\n\t\t\t\t}\n\t\t\t}\n\t\t\teventListeners.add(listener as (data: any) => void);\n\n\t\t\treturn () => {\n\t\t\t\tunsubscribe(event, listener);\n\t\t\t};\n\t\t},\n\t\toff: (event: string, listener?: (data: any) => void) => {\n\t\t\tunsubscribe(event, listener);\n\t\t}\n\t};\n}\n\nexport const socket = createWebSocket();\n"
  },
  {
    "path": "interface/src/lib/stores/telemetry.ts",
    "content": "import { writable } from 'svelte/store';\nimport type { RSSI } from '../types/models';\nimport type { Battery } from '../types/models';\nimport type { OTAStatus } from '../types/models';\nimport type { Ethernet } from '../types/models';\n\nlet telemetry_data = {\n\trssi: {\n\t\trssi: 0,\n\t\tssid: '',\n\t\tdisconnected: true\n\t},\n\tbattery: {\n\t\tsoc: 100,\n\t\tcharging: false\n\t},\n\tota_status: {\n\t\tstatus: 'none',\n\t\tprogress: 0,\n\t\tbytes_written: 0,\n\t\ttotal_bytes: 0,\n\t\terror: ''\n\t},\n\tethernet: {\n\t\tconnected: false\n\t}\n};\n\nfunction createTelemetry() {\n\tconst { subscribe, set, update } = writable(telemetry_data);\n\n\treturn {\n\t\tsubscribe,\n\t\tsetRSSI: (data: RSSI) => {\n\t\t\tif (!isNaN(Number(data.rssi))) {\n\t\t\t\tupdate((telemetry_data) => ({\n\t\t\t\t\t...telemetry_data,\n\t\t\t\t\trssi: { rssi: Number(data.rssi), ssid: data.ssid, disconnected: false }\n\t\t\t\t}));\n\t\t\t} else {\n\t\t\t\tupdate((telemetry_data) => ({\n\t\t\t\t\t...telemetry_data,\n\t\t\t\t\trssi: { rssi: 0, ssid: data.ssid, disconnected: true }\n\t\t\t\t}));\n\t\t\t}\n\t\t},\n\t\tsetBattery: (data: Battery) => {\n\t\t\tupdate((telemetry_data) => ({\n\t\t\t\t...telemetry_data,\n\t\t\t\tbattery: { soc: data.soc, charging: data.charging }\n\t\t\t}));\n\t\t},\n\t\tsetOTAStatus: (data: OTAStatus) => {\n\t\t\tupdate((telemetry_data) => ({\n\t\t\t\t...telemetry_data,\n\t\t\t\tota_status: { \n\t\t\t\t\tstatus: data.status, \n\t\t\t\t\tprogress: data.progress, \n\t\t\t\t\tbytes_written: data.bytes_written ?? 0,\n\t\t\t\t\ttotal_bytes: data.total_bytes ?? 0,\n\t\t\t\t\terror: data.error \n\t\t\t\t}\n\t\t\t}));\n\t\t},\n\t\tsetEthernet: (data: Ethernet) => {\n\t\t\tupdate((telemetry_data) => ({\n\t\t\t\t...telemetry_data,\n\t\t\t\tethernet: { connected: data.connected }\n\t\t\t}));\n\t\t}\n\t};\n}\n\nexport const telemetry = createTelemetry();\n"
  },
  {
    "path": "interface/src/lib/stores/user.ts",
    "content": "import { writable } from 'svelte/store';\nimport { goto } from '$app/navigation';\nimport { jwtDecode } from 'jwt-decode';\n\nexport type userProfile = {\n\tusername: string;\n\tadmin: boolean;\n\tbearer_token: string;\n};\n\ntype decodedJWT = {\n\tusername: string;\n\tadmin: boolean;\n};\n\nlet empty = {\n\tusername: '',\n\tadmin: false,\n\tbearer_token: ''\n};\n\nfunction createStore() {\n\tconst { subscribe, set } = writable(empty);\n\n\t// retrieve store from sessionStorage / localStorage if available\n\tconst userdata = localStorage.getItem('user');\n\tif (userdata) {\n\t\tset(JSON.parse(userdata));\n\t}\n\n\treturn {\n\t\tsubscribe,\n\t\tinit: (access_token: string) => {\n\t\t\tconst decoded: decodedJWT = jwtDecode(access_token);\n\t\t\tconst userdata = {\n\t\t\t\tbearer_token: access_token,\n\t\t\t\tusername: decoded.username,\n\t\t\t\tadmin: decoded.admin\n\t\t\t};\n\t\t\tset(userdata);\n\t\t\t// persist store in sessionStorage / localStorage\n\t\t\tlocalStorage.setItem('user', JSON.stringify(userdata));\n\t\t},\n\t\tinvalidate: () => {\n\t\t\tconsole.log('Log out user');\n\t\t\tset(empty);\n\t\t\t// remove localStorage \"user\"\n\t\t\tlocalStorage.removeItem('user');\n\t\t\t// redirect to login page\n\t\t\tgoto('/');\n\t\t}\n\t};\n}\n\nexport const user = createStore();\n"
  },
  {
    "path": "interface/src/lib/types/models.ts",
    "content": "export type WifiStatus = {\n\tstatus: number;\n\tlocal_ip: string;\n\tmac_address: string;\n\trssi: number;\n\tssid: string;\n\tbssid: string;\n\tchannel: number;\n\tsubnet_mask: string;\n\tgateway_ip: string;\n\tdns_ip_1: string;\n\tdns_ip_2?: string;\n};\n\nexport type WifiSettings = {\n\thostname: string;\n\tconnection_mode: number;\n\twifi_networks: KnownNetworkItem[];\n};\n\nexport type KnownNetworkItem = {\n\tssid: string;\n\tpassword: string;\n\tstatic_ip_config: boolean;\n\tlocal_ip?: string;\n\tsubnet_mask?: string;\n\tgateway_ip?: string;\n\tdns_ip_1?: string;\n\tdns_ip_2?: string;\n};\n\nexport type NetworkItem = {\n\trssi: number;\n\tssid: string;\n\tbssid: string;\n\tchannel: number;\n\tencryption_type: number;\n};\n\nexport type ApStatus = {\n\tstatus: number;\n\tip_address: string;\n\tmac_address: string;\n\tstation_num: number;\n};\n\nexport type ApSettings = {\n\tprovision_mode: number;\n\tssid: string;\n\tpassword: string;\n\tchannel: number;\n\tssid_hidden: boolean;\n\tmax_clients: number;\n\tlocal_ip: string;\n\tgateway_ip: string;\n\tsubnet_mask: string;\n};\n\nexport type LightState = {\n\tled_on: boolean;\n};\n\nexport type BrokerSettings = {\n\tmqtt_path: string;\n\tname: string;\n\tunique_id: string;\n\tstatus_topic: string;\n};\n\nexport type NTPStatus = {\n\tstatus: number;\n\tutc_time: string;\n\tlocal_time: string;\n\tserver: string;\n\tuptime: number;\n};\n\nexport type NTPSettings = {\n\tenabled: boolean;\n\tserver: string;\n\ttz_label: string;\n\ttz_format: string;\n};\n\nexport type Analytics = {\n\tmax_alloc_heap: number;\n\tpsram_size: number;\n\tfree_psram: number;\n\tused_psram: number;\n\tfree_heap: number;\n\tused_heap: number;\n\ttotal_heap: number;\n\tmin_free_heap: number;\n\tcore_temp: number;\n\tfs_total: number;\n\tfs_used: number;\n\tuptime: number;\n};\n\nexport type RSSI = {\n\trssi: number;\n\tssid: string;\n};\n\nexport type Battery = {\n\tsoc: number;\n\tcharging: boolean;\n};\n\nexport type OTAStatus = {\n\tstatus: 'none' | 'preparing' | 'progress' | 'finished' | 'error';\n\tprogress: number;\n\tbytes_written?: number;\n\ttotal_bytes?: number;\n\terror: string;\n};\n\nexport type StaticSystemInformation = {\n\tesp_platform: string;\n\tfirmware_version: string;\n\tcpu_freq_mhz: number;\n\tcpu_type: string;\n\tcpu_rev: number;\n\tcpu_cores: number;\n\tsketch_size: number;\n\tfree_sketch_space: number;\n\tsdk_version: string;\n\tarduino_version: string;\n\tflash_chip_size: number;\n\tflash_chip_speed: number;\n\tcpu_reset_reason: string;\n};\n\nexport type SystemInformation = Analytics & StaticSystemInformation;\n\nexport type MQTTStatus = {\n\tenabled: boolean;\n\tconnected: boolean;\n\tclient_id: string;\n\tlast_error: string;\n};\n\nexport type MQTTSettings = {\n\tenabled: boolean;\n\turi: string;\n\tusername: string;\n\tpassword: string;\n\tclient_id: string;\n\tkeep_alive: number;\n\tclean_session: boolean;\n\tmessage_interval_ms: number;\n};\n\n\nexport type Ethernet = {\n\tconnected: boolean;\n};\n\nexport type EthernetStatus = {\n\tconnected: boolean;\n\tlocal_ip: string;\n\tmac_address: string;\n\tsubnet_mask: string;\n\tgateway_ip?: string;\n\tdns_ip_1?: string;\n\tdns_ip_2?: string;\n\tlink_speed?: number;\n};\n\nexport type EthernetSettings = {\n\thostname: string;\n\tstatic_ip_config: boolean;\n\tlocal_ip?: string;\n\tsubnet_mask?: string;\n\tgateway_ip?: string;\n\tdns_ip_1?: string;\n\tdns_ip_2?: string;\n};\n"
  },
  {
    "path": "interface/src/routes/+error.svelte",
    "content": "<script lang=\"ts\">\n\timport { goto } from '$app/navigation';\n\timport { page } from '$app/state';\n\n\tif (page.status == 404) {\n\t\tgoto('/');\n\t}\n</script>\n\n<div class=\"min-w-screen flex h-screen\">\n\t<div class=\"mx-auto flex w-8/12 max-w-lg flex-col justify-center self-center p-2 align-middle\">\n\t\t<div class=\"flex flex-wrap items-end justify-start\">\n\t\t\t<div class=\"text-secondary pr-2 text-7xl font-bold\">{page.status}</div>\n\t\t\t<div class=\"text-base-content text-5xl font-semibold\">{page.error?.message}</div>\n\t\t</div>\n\t\t<div class=\"divider\"></div>\n\t\t<p class=\"text-xl\">Oops! Something has gone wrong.</p>\n\t</div>\n</div>\n"
  },
  {
    "path": "interface/src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n\timport type { LayoutData } from './$types';\n\timport { onDestroy, onMount } from 'svelte';\n\timport { user } from '$lib/stores/user';\n\timport { telemetry } from '$lib/stores/telemetry';\n\timport { analytics } from '$lib/stores/analytics';\n\timport { batteryHistory } from '$lib/stores/battery';\n\timport { socket } from '$lib/stores/socket';\n\timport type { userProfile } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { Modals, modals } from 'svelte-modals';\n\timport Toast from '$lib/components/toasts/Toast.svelte';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport { fade } from 'svelte/transition';\n\timport '../app.css';\n\timport Menu from './menu.svelte';\n\timport Statusbar from './statusbar.svelte';\n\timport Login from './login.svelte';\n\timport type { Analytics } from '$lib/types/models';\n\timport type { RSSI } from '$lib/types/models';\n\timport type { Battery } from '$lib/types/models';\n\timport type { OTAStatus } from '$lib/types/models';\n\timport type { Ethernet } from '$lib/types/models';\n\n\tinterface Props {\n\t\tdata: LayoutData;\n\t\tchildren?: import('svelte').Snippet;\n\t}\n\n\tlet { data, children }: Props = $props();\n\n\tonMount(async () => {\n\t\tif ($user.bearer_token !== '') {\n\t\t\tawait validateUser($user);\n\t\t}\n\t\tif (!(page.data.features.security && $user.bearer_token === '')) {\n\t\t\tinitSocket();\n\t\t}\n\t});\n\n\tconst initSocket = () => {\n\t\tconst ws_token = page.data.features.security ? '?access_token=' + $user.bearer_token : '';\n\t\tconst ws_protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';\n\t\tsocket.init(\n\t\t\t`${ws_protocol}://${window.location.host}/ws/events${ws_token}`,\n\t\t\tpage.data.features.event_use_json\n\t\t);\n\t\taddEventListeners();\n\t};\n\n\tonDestroy(() => {\n\t\tremoveEventListeners();\n\t});\n\n\tconst addEventListeners = () => {\n\t\tsocket.on('open', handleOpen);\n\t\tsocket.on('close', handleClose);\n\t\tsocket.on('error', handleError);\n\t\tsocket.on('rssi', handleNetworkStatus);\n\t\tsocket.on('notification', handleNotification);\n\t\tif (page.data.features.analytics) socket.on('analytics', handleAnalytics);\n\t\tif (page.data.features.battery) socket.on('battery', handleBattery);\n\t\tif (page.data.features.download_firmware) socket.on('otastatus', handleOTA);\n\t\tif (page.data.features.ethernet) socket.on('ethernet', handleEthernet);\n\t};\n\n\tconst removeEventListeners = () => {\n\t\tsocket.off('analytics', handleAnalytics);\n\t\tsocket.off('open', handleOpen);\n\t\tsocket.off('close', handleClose);\n\t\tsocket.off('rssi', handleNetworkStatus);\n\t\tsocket.off('notification', handleNotification);\n\t\tsocket.off('battery', handleBattery);\n\t\tsocket.off('otastatus', handleOTA);\n\t\tsocket.off('ethernet', handleEthernet);\n\t};\n\n\tasync function validateUser(userdata: userProfile) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/verifyAuthorization', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: 'Bearer ' + userdata.bearer_token,\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (response.status !== 200) {\n\t\t\t\tuser.invalidate();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tconst handleOpen = () => {\n\t\tnotifications.success('Connection to device established', 5000);\n\t};\n\n\tconst handleClose = () => {\n\t\tnotifications.error('Connection to device lost', 5000);\n\t\ttelemetry.setRSSI({ rssi: 0, ssid: '' });\n\t};\n\n\tconst handleError = (data: any) => console.error(data);\n\n\tconst handleNotification = (data: any) => {\n\t\tswitch (data.type) {\n\t\t\tcase 'info':\n\t\t\t\tnotifications.info(data.message, 5000);\n\t\t\t\tbreak;\n\t\t\tcase 'warning':\n\t\t\t\tnotifications.warning(data.message, 5000);\n\t\t\t\tbreak;\n\t\t\tcase 'error':\n\t\t\t\tnotifications.error(data.message, 5000);\n\t\t\t\tbreak;\n\t\t\tcase 'success':\n\t\t\t\tnotifications.success(data.message, 5000);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t};\n\n\tconst handleAnalytics = (data: Analytics) => analytics.addData(data);\n\n\tconst handleNetworkStatus = (data: RSSI) => telemetry.setRSSI(data);\n\n\tconst handleBattery = (data: Battery) => {\n\t\ttelemetry.setBattery(data);\n\t\tbatteryHistory.addData(data);\n\t};\n\n\tconst handleOTA = (data: OTAStatus) => {\n\t\ttelemetry.setOTAStatus(data);\n\t};\n\n\tconst handleEthernet = (data: Ethernet) => {\n\t\ttelemetry.setEthernet(data);\n\t};\n\n\tlet menuOpen = $state(false);\n</script>\n\n<svelte:head>\n\t<title>{page.data.title}</title>\n</svelte:head>\n\n{#if page.data.features.security && $user.bearer_token === ''}\n\t<Login signIn={initSocket} />\n{:else}\n\t<div class=\"drawer lg:drawer-open\">\n\t\t<input id=\"main-menu\" type=\"checkbox\" class=\"drawer-toggle\" bind:checked={menuOpen} />\n\t\t<div class=\"drawer-content flex flex-col\">\n\t\t\t<!-- Status bar content here -->\n\t\t\t<Statusbar />\n\n\t\t\t<!-- Main page content here -->\n\t\t\t{@render children?.()}\n\t\t</div>\n\t\t<!-- Side Navigation -->\n\t\t<div class=\"drawer-side z-30 shadow-lg\">\n\t\t\t<label for=\"main-menu\" class=\"drawer-overlay\"></label>\n\t\t\t<Menu\n\t\t\t\tcloseMenu={() => {\n\t\t\t\t\tmenuOpen = false;\n\t\t\t\t}}\n\t\t\t/>\n\t\t</div>\n\t</div>\n{/if}\n\n<Modals>\n\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t{#snippet backdrop({ close })}\n\t\t<div\n\t\t\tclass=\"fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm\"\n\t\t\ttransition:fade|global\n\t\t\tonclick={() => close()}\n\t\t\trole=\"button\"\n\t\t\ttabindex=\"0\"\n\t\t\taria-label=\"Close modal\"\n\t\t></div>\n\t{/snippet}\n</Modals>\n\n<Toast />\n"
  },
  {
    "path": "interface/src/routes/+layout.ts",
    "content": "import type { LayoutLoad } from './$types';\n\n// This can be false if you're using a fallback (i.e. SPA mode)\nexport const prerender = false;\nexport const ssr = false;\n\nexport const load = (async ({ fetch }) => {\n\tconst result = await fetch('/rest/features');\n\tconst item = await result.json();\n\treturn {\n\t\tfeatures: item,\n\t\ttitle: 'ESP32-SvelteKit',\n\t\tgithub: 'theelims/ESP32-sveltekit',\n\t\tcopyright: '2025 theelims',\n\t\tappName: 'ESP32 SvelteKit'\n\t};\n}) satisfies LayoutLoad;\n"
  },
  {
    "path": "interface/src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport logo from '$lib/assets/logo.png';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div class=\"hero bg-base-100 h-screen\">\n\t<div class=\"card md:card-side bg-base-200 shadow-primary shadow-2xl\">\n\t\t<figure class=\"bg-base-200\"><img src={logo} alt=\"Logo\" class=\"h-auto w-64\" /></figure>\n\t\t<div class=\"card-body w-80\">\n\t\t\t<h2 class=\"card-title text-center text-2xl\">Welcome to ESP32-SvelteKit</h2>\n\t\t\t<p class=\"py-6 text-center\">\n\t\t\t\tA simple, secure and extensible framework for IoT projects for ESP32 platforms with\n\t\t\t\tresponsive <a\n\t\t\t\t\thref=\"https://kit.svelte.dev/\"\n\t\t\t\t\tclass=\"link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">SvelteKit</a\n\t\t\t\t>\n\t\t\t\tfront-end built with\n\t\t\t\t<a href=\"https://tailwindcss.com/\" class=\"link\" target=\"_blank\" rel=\"noopener noreferrer\"\n\t\t\t\t\t>TailwindCSS</a\n\t\t\t\t>\n\t\t\t\tand\n\t\t\t\t<a href=\"https://daisyui.com/\" class=\"link\" target=\"_blank\" rel=\"noopener noreferrer\"\n\t\t\t\t\t>DaisyUI</a\n\t\t\t\t>.\n\t\t\t</p>\n\t\t\t<a\n\t\t\t\tclass=\"btn btn-primary\"\n\t\t\t\thref=\"/demo\"\n\t\t\t\tonclick={() => notifications.success('You did it!', 1000)}>Start Demo</a\n\t\t\t>\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "interface/src/routes/connections/+page.ts",
    "content": "import type { PageLoad } from './$types';\nimport { goto } from '$app/navigation';\n\nexport const load = (async () => {\n\tgoto('/');\n\treturn;\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/connections/mqtt/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from '../$types';\n\timport MQTT from './MQTT.svelte';\n\timport MqttConfig from './MQTTConfig.svelte';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<MQTT />\n\t<MqttConfig />\n</div>\n"
  },
  {
    "path": "interface/src/routes/connections/mqtt/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n    return {\n        title: \"MQTT\"\n    };\n}) satisfies PageLoad;"
  },
  {
    "path": "interface/src/routes/connections/mqtt/MQTT.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount, onDestroy } from 'svelte';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport InputPassword from '$lib/components/InputPassword.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport Collapsible from '$lib/components/Collapsible.svelte';\n\timport MQTT from '~icons/tabler/topology-star-3';\n\timport Client from '~icons/tabler/robot';\n\timport type { MQTTSettings, MQTTStatus } from '$lib/types/models';\n\n\tlet mqttSettings: MQTTSettings = $state();\n\tlet mqttStatus: MQTTStatus = $state();\n\n\tlet formField: any = $state();\n\n\tasync function getMQTTStatus() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/mqttStatus', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tmqttStatus = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn mqttStatus;\n\t}\n\n\tasync function getMQTTSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/mqttSettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tmqttSettings = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn mqttSettings;\n\t}\n\n\tconst interval = setInterval(async () => {\n\t\tgetMQTTStatus();\n\t}, 5000);\n\n\tonDestroy(() => clearInterval(interval));\n\n\tonMount(() => {\n\t\tif (!page.data.features.security || $user.admin) {\n\t\t\tgetMQTTSettings();\n\t\t}\n\t});\n\n\tlet formErrors = $state({\n\t\thost: false,\n\t\tport: false,\n\t\tkeep_alive: false,\n\t\ttopic_length: false,\n\t\trate_limit: false\n\t});\n\n\tasync function postMQTTSettings(data: MQTTSettings) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/mqttSettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(data)\n\t\t\t});\n\t\t\tif (response.status == 200) {\n\t\t\t\tnotifications.success('MQTT settings updated.', 3000);\n\t\t\t\tmqttSettings = await response.json();\n\t\t\t} else {\n\t\t\t\tnotifications.error('User not authorized.', 3000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tfunction handleSubmitMQTT() {\n\t\tlet valid = true;\n\n\t\t// Validate Server URI\n\t\tconst regexExpURL =\n\t\t\t/^(mqtt|mqtts|ws|wss):\\/\\/((?:[a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+|(?:\\d{1,3}\\.){3}\\d{1,3})(?::(\\d{1,5}))?$/;\n\n\t\tif (!regexExpURL.test(mqttSettings.uri)) {\n\t\t\tvalid = false;\n\t\t\tformErrors.host = true;\n\t\t} else {\n\t\t\tformErrors.host = false;\n\t\t}\n\n\t\t// Validate if port is a number and within the right range\n\t\tlet keepalive = Number(mqttSettings.keep_alive);\n\t\tif (1 <= keepalive && keepalive <= 600) {\n\t\t\tformErrors.keep_alive = false;\n\t\t} else {\n\t\t\tformErrors.keep_alive = true;\n\t\t\tvalid = false;\n\t\t}\n\n\t\t// Validate it rate limit is a number and within the right range\n\t\tlet ratelimit = Number(mqttSettings.message_interval_ms);\n\t\tif (0 <= ratelimit && ratelimit <= 1000) {\n\t\t\tformErrors.rate_limit = false;\n\t\t} else {\n\t\t\tformErrors.rate_limit = true;\n\t\t\tvalid = false;\n\t\t}\n\n\t\t// Submit JSON to REST API\n\t\tif (valid) {\n\t\t\tpostMQTTSettings(mqttSettings);\n\t\t\t//alert('Form Valid');\n\t\t}\n\t}\n\n\tfunction preventDefault(fn) {\n\t\treturn function (event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn.call(this, event);\n\t\t};\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<MQTT class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>MQTT</span>\n\t{/snippet}\n\t<div class=\"w-full\">\n\t\t{#await getMQTTStatus()}\n\t\t\t<Spinner />\n\t\t{:then nothing}\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"mask mask-hexagon h-auto w-10 {mqttStatus.connected === true\n\t\t\t\t\t\t\t? 'bg-success'\n\t\t\t\t\t\t\t: 'bg-error'}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<MQTT\n\t\t\t\t\t\t\tclass=\"h-auto w-full scale-75 {mqttStatus.connected === true\n\t\t\t\t\t\t\t\t? 'text-success-content'\n\t\t\t\t\t\t\t\t: 'text-error-content'}\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Status</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{#if mqttStatus.connected}\n\t\t\t\t\t\t\t\tConnected\n\t\t\t\t\t\t\t{:else if !mqttStatus.enabled}\n\t\t\t\t\t\t\t\tMQTT Disabled\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t{mqttStatus.last_error}\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Client class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Client ID</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{mqttStatus.client_id}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{/await}\n\t</div>\n\n\t{#if !page.data.features.security || $user.admin}\n\t\t<Collapsible open={false} class=\"shadow-lg\" icon={null} opened={() => {}} closed={() => {}}>\n\t\t\t{#snippet title()}\n\t\t\t\t<span>Change MQTT Settings</span>\n\t\t\t{/snippet}\n\n\t\t\t<form\n\t\t\t\tonsubmit={preventDefault(handleSubmitMQTT)}\n\t\t\t\tnovalidate\n\t\t\t\tbind:this={formField}\n\t\t\t\tclass=\"fieldset\"\n\t\t\t>\n\t\t\t\t<div class=\"grid w-full grid-cols-1 content-center gap-x-4 gap-y-2 sm:grid-cols-2\">\n\t\t\t\t\t<!-- Enable -->\n\t\t\t\t\t<label class=\"label inline-flex cursor-pointer content-end justify-start gap-4 text-base\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tbind:checked={mqttSettings.enabled}\n\t\t\t\t\t\t\tclass=\"checkbox checkbox-primary\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\tEnable MQTT\n\t\t\t\t\t</label>\n\n\t\t\t\t\t<div class=\"hidden sm:block\"></div>\n\t\t\t\t\t<!-- URI -->\n\t\t\t\t\t<div class=\"sm:col-span-2\">\n\t\t\t\t\t\t<label class=\"label\" for=\"host\">URI</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.host\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\tbind:value={mqttSettings.uri}\n\t\t\t\t\t\t\tid=\"host\"\n\t\t\t\t\t\t\tmin=\"3\"\n\t\t\t\t\t\t\tmax=\"64\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<label class=\"label\" for=\"host\">\n\t\t\t\t\t\t\t<span class=\" text-error {formErrors.host ? '' : 'hidden'}\">Must be a valid URI</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Username -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"user\">Username </label>\n\t\t\t\t\t\t<input type=\"text\" class=\"input w-full\" bind:value={mqttSettings.username} id=\"user\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Password -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"pwd\">Password </label>\n\t\t\t\t\t\t<InputPassword bind:value={mqttSettings.password} id=\"pwd\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Client ID -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"clientid\">Client ID </label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input w-full\"\n\t\t\t\t\t\t\tbind:value={mqttSettings.client_id}\n\t\t\t\t\t\t\tid=\"clientid\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Keep Alive -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"keepalive\">Keep Alive </label>\n\t\t\t\t\t\t<label\n\t\t\t\t\t\t\tfor=\"keepalive\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.keep_alive\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\t\t\tmax=\"600\"\n\t\t\t\t\t\t\t\tclass=\"\"\n\t\t\t\t\t\t\t\tbind:value={mqttSettings.keep_alive}\n\t\t\t\t\t\t\t\tid=\"keepalive\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<span class=\"label\">Seconds</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<label for=\"keepalive\" class=\"\"\n\t\t\t\t\t\t\t><span class=\" text-error {formErrors.keep_alive ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t>Must be between 1 and 600 seconds</span\n\t\t\t\t\t\t\t></label\n\t\t\t\t\t\t>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Rate Limit -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"ratelimit\">Publish Message Interval</label>\n\t\t\t\t\t\t<label\n\t\t\t\t\t\t\tfor=\"ratelimit\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.rate_limit\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\t\t\tmax=\"1000\"\n\t\t\t\t\t\t\t\tclass=\"\"\n\t\t\t\t\t\t\t\tbind:value={mqttSettings.message_interval_ms}\n\t\t\t\t\t\t\t\tid=\"ratelimit\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<span class=\"label\">Milliseconds</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<label for=\"ratelimit\" class=\"\"\n\t\t\t\t\t\t\t><span class=\" text-error {formErrors.rate_limit ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t>Must be between 0 and 1000 milliseconds</span\n\t\t\t\t\t\t\t></label\n\t\t\t\t\t\t>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Clean Session -->\n\t\t\t\t\t<label\n\t\t\t\t\t\tclass=\"label inline-flex cursor-pointer content-end justify-start gap-4 text-base mt-2 sm:mt-4\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tbind:checked={mqttSettings.clean_session}\n\t\t\t\t\t\t\tclass=\"checkbox checkbox-primary\"\n\t\t\t\t\t\t/>Clean Session?\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"divider mb-2 mt-0\"></div>\n\t\t\t\t<div class=\"flex flex-wrap justify-end gap-2\">\n\t\t\t\t\t<button class=\"btn btn-primary\" type=\"submit\">Apply Settings</button>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t</Collapsible>\n\t{/if}\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/connections/mqtt/MQTTConfig.svelte",
    "content": "<script lang=\"ts\">\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport MQTT from '~icons/tabler/topology-star-3';\n\timport Info from '~icons/tabler/info-circle';\n\timport type { BrokerSettings } from '$lib/types/models';\n\n\tlet brokerSettings: BrokerSettings = $state();\n\n\tlet formField: any = $state();\n\n\tasync function getBrokerSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/brokerSettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tbrokerSettings = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tlet formErrors = $state({\n\t\tuid: false,\n\t\tpath: false,\n\t\tname: false,\n\t\tstatus_topic: false\n\t});\n\n\tasync function postBrokerSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/brokerSettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(brokerSettings)\n\t\t\t});\n\t\t\tif (response.status == 200) {\n\t\t\t\tnotifications.success('Broker settings updated.', 3000);\n\t\t\t\tbrokerSettings = await response.json();\n\t\t\t} else {\n\t\t\t\tnotifications.error('User not authorized.', 3000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tfunction handleSubmitBroker() {\n\t\tlet valid = true;\n\n\t\t// Validate unique ID\n\t\tif (brokerSettings.unique_id.length < 3 || brokerSettings.unique_id.length > 32) {\n\t\t\tvalid = false;\n\t\t\tformErrors.uid = true;\n\t\t} else {\n\t\t\tformErrors.uid = false;\n\t\t}\n\n\t\t// Validate name\n\t\tif (brokerSettings.name.length < 3 || brokerSettings.name.length > 32) {\n\t\t\tvalid = false;\n\t\t\tformErrors.name = true;\n\t\t} else {\n\t\t\tformErrors.name = false;\n\t\t}\n\t\t// Validate MQTT Path\n\t\tif (brokerSettings.mqtt_path.length > 64) {\n\t\t\tvalid = false;\n\t\t\tformErrors.path = true;\n\t\t} else {\n\t\t\tformErrors.path = false;\n\t\t}\n\n\t\t// Validate MQTT Status Topic\n\t\tif (brokerSettings.status_topic.length > 64) {\n\t\t\tvalid = false;\n\t\t\tformErrors.status_topic = true;\n\t\t} else {\n\t\t\tformErrors.status_topic = false;\n\t\t}\n\n\t\t// Submit JSON to REST API\n\t\tif (valid) {\n\t\t\tpostBrokerSettings();\n\t\t\t//alert('Form Valid');\n\t\t}\n\t}\n\n\tfunction preventDefault(fn) {\n\t\treturn function (event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn.call(this, event);\n\t\t};\n\t}\n</script>\n\n<SettingsCard collapsible={true} open={false}>\n\t{#snippet icon()}\n\t\t<MQTT class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>MQTT Broker Settings</span>\n\t{/snippet}\n\t<div class=\"w-full\">\n\t\t{#await getBrokerSettings()}\n\t\t\t<Spinner />\n\t\t{:then nothing}\n\t\t\t<form\n\t\t\t\tclass=\"fieldset\"\n\t\t\t\tonsubmit={preventDefault(handleSubmitBroker)}\n\t\t\t\tnovalidate\n\t\t\t\tbind:this={formField}\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"alert alert-info my-2 shadow-lg\">\n\t\t\t\t\t<Info class=\"h-6 w-6 shrink-0 stroke-current\" />\n\t\t\t\t\t<span\n\t\t\t\t\t\t>The LED is controllable via MQTT with the demo project designed to work with Home\n\t\t\t\t\t\tAssistant's auto discovery feature.</span\n\t\t\t\t\t>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"grid w-full grid-cols-1 content-center gap-x-4 gap-y-2 px-4\">\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"uid\">Unique ID</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.uid\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\tbind:value={brokerSettings.unique_id}\n\t\t\t\t\t\t\tid=\"uid\"\n\t\t\t\t\t\t\tmin=\"3\"\n\t\t\t\t\t\t\tmax=\"32\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<label class=\"label\" for=\"uid\">\n\t\t\t\t\t\t\t<span class=\"text-error {formErrors.uid ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t>Unique ID must be between 3 and 32 characters long</span\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"name\">Name</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.name\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\tbind:value={brokerSettings.name}\n\t\t\t\t\t\t\tid=\"name\"\n\t\t\t\t\t\t\tmin=\"3\"\n\t\t\t\t\t\t\tmax=\"32\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<label class=\"label\" for=\"name\">\n\t\t\t\t\t\t\t<span class=\"text-error {formErrors.name ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t>Name must be between 3 and 32 characters long</span\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"path\">MQTT Path</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.path\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\tbind:value={brokerSettings.mqtt_path}\n\t\t\t\t\t\t\tid=\"path\"\n\t\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\t\tmax=\"64\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<label class=\"label\" for=\"path\">\n\t\t\t\t\t\t\t<span class=\"text-error {formErrors.path ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t>MQTT path is limited to 64 characters</span\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"status_topic\">MQTT Status Topic</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.status_topic\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\tbind:value={brokerSettings.status_topic}\n\t\t\t\t\t\t\tid=\"status_topic\"\n\t\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\t\tmax=\"64\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<label class=\"label\" for=\"status_topic\">\n\t\t\t\t\t\t\t<span class=\"text-error {formErrors.status_topic ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t>MQTT status topic is limited to 64 characters</span\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"divider mb-2 mt-0\"></div>\n\t\t\t\t<div class=\"mx-4 flex flex-wrap justify-end gap-2\">\n\t\t\t\t\t<button class=\"btn btn-primary\" type=\"submit\">Apply Settings</button>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t{/await}\n\t</div>\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/connections/ntp/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from '../$types';\n\timport NTP from './NTP.svelte';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<NTP />\n</div>\n"
  },
  {
    "path": "interface/src/routes/connections/ntp/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn {\n\t\ttitle: 'NTP'\n\t};\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/connections/ntp/NTP.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount, onDestroy } from 'svelte';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport Collapsible from '$lib/components/Collapsible.svelte';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport { TIME_ZONES } from './timezones';\n\timport NTP from '~icons/tabler/clock-check';\n\timport Server from '~icons/tabler/server';\n\timport Clock from '~icons/tabler/clock';\n\timport UTC from '~icons/tabler/clock-pin';\n\timport Stopwatch from '~icons/tabler/24-hours';\n\timport type { NTPSettings, NTPStatus } from '$lib/types/models';\n\n\tlet ntpSettings: NTPSettings = $state();\n\tlet ntpStatus: NTPStatus = $state();\n\n\tasync function getNTPStatus() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/ntpStatus', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tntpStatus = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tasync function getNTPSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/ntpSettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tntpSettings = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst interval = setInterval(async () => {\n\t\tgetNTPStatus();\n\t}, 5000);\n\n\tonDestroy(() => clearInterval(interval));\n\n\tonMount(() => {\n\t\tif (!page.data.features.security || $user.admin) {\n\t\t\tgetNTPSettings();\n\t\t}\n\t});\n\n\tlet formField: any = $state();\n\n\tlet formErrors = $state({\n\t\tserver: false\n\t});\n\n\tasync function postNTPSettings(data: NTPSettings) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/ntpSettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(data)\n\t\t\t});\n\n\t\t\tif (response.status == 200) {\n\t\t\t\tnotifications.success('Security settings updated.', 3000);\n\t\t\t\tntpSettings = await response.json();\n\t\t\t} else {\n\t\t\t\tnotifications.error('User not authorized.', 3000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tfunction handleSubmitNTP() {\n\t\tlet valid = true;\n\n\t\t// Validate Server\n\t\t// RegEx for IPv4\n\t\tconst regexExpIPv4 =\n\t\t\t/\\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\\b/;\n\t\tconst regexExpURL =\n\t\t\t/[-a-zA-Z0-9@:%_\\+.~#?&//=]{2,256}\\.[a-z]{2,4}\\b(\\/[-a-zA-Z0-9@:%_\\+.~#?&//=]*)?/i;\n\n\t\tif (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {\n\t\t\tvalid = false;\n\t\t\tformErrors.server = true;\n\t\t} else {\n\t\t\tformErrors.server = false;\n\t\t}\n\n\t\tntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];\n\n\t\t// Submit JSON to REST API\n\t\tif (valid) {\n\t\t\tpostNTPSettings(ntpSettings);\n\t\t\t//alert('Form Valid');\n\t\t}\n\t}\n\n\tfunction convertSeconds(seconds: number) {\n\t\t// Calculate the number of seconds, minutes, hours, and days\n\t\tlet minutes = Math.floor(seconds / 60);\n\t\tlet hours = Math.floor(minutes / 60);\n\t\tlet days = Math.floor(hours / 24);\n\n\t\t// Calculate the remaining hours, minutes, and seconds\n\t\thours = hours % 24;\n\t\tminutes = minutes % 60;\n\t\tseconds = seconds % 60;\n\n\t\t// Create the formatted string\n\t\tlet result = '';\n\t\tif (days > 0) {\n\t\t\tresult += days + ' day' + (days > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (hours > 0) {\n\t\t\tresult += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (minutes > 0) {\n\t\t\tresult += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tresult += seconds + ' second' + (seconds > 1 ? 's' : '');\n\n\t\treturn result;\n\t}\n\n\tfunction preventDefault(fn) {\n\t\treturn function (event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn.call(this, event);\n\t\t};\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Clock class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Network Time</span>\n\t{/snippet}\n\t<div class=\"w-full\">\n\t\t{#await getNTPStatus()}\n\t\t\t<Spinner />\n\t\t{:then nothing}\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"mask mask-hexagon h-auto w-10 {ntpStatus.status === 1\n\t\t\t\t\t\t\t? 'bg-success'\n\t\t\t\t\t\t\t: 'bg-error'}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<NTP\n\t\t\t\t\t\t\tclass=\"h-auto w-full scale-75 {ntpStatus.status === 1\n\t\t\t\t\t\t\t\t? 'text-success-content'\n\t\t\t\t\t\t\t\t: 'text-error-content'}\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Status</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{ntpStatus.status === 1 ? 'Active' : 'Inactive'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Server class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">NTP Server</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{ntpStatus.server}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Clock class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Local Time</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{new Intl.DateTimeFormat('en-GB', {\n\t\t\t\t\t\t\t\tdateStyle: 'long',\n\t\t\t\t\t\t\t\ttimeStyle: 'long'\n\t\t\t\t\t\t\t}).format(new Date(ntpStatus.local_time))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<UTC class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">UTC Time</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{new Intl.DateTimeFormat('en-GB', {\n\t\t\t\t\t\t\t\tdateStyle: 'long',\n\t\t\t\t\t\t\t\ttimeStyle: 'long',\n\t\t\t\t\t\t\t\ttimeZone: 'UTC'\n\t\t\t\t\t\t\t}).format(new Date(ntpStatus.utc_time))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Stopwatch class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Uptime</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{convertSeconds(ntpStatus.uptime)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{/await}\n\t</div>\n\n\t{#if !page.data.features.security || $user.admin}\n\t\t<Collapsible open={false} class=\"shadow-lg\" icon={null} opened={() => {}} closed={() => {}}>\n\t\t\t{#snippet title()}\n\t\t\t\t<span>Change NTP Settings</span>\n\t\t\t{/snippet}\n\t\t\t<form\n\t\t\t\tclass=\"fieldset\"\n\t\t\t\tonsubmit={preventDefault(handleSubmitNTP)}\n\t\t\t\tnovalidate\n\t\t\t\tbind:this={formField}\n\t\t\t>\n\t\t\t\t<label class=\"label text-base inline-flex cursor-pointer content-end justify-start gap-4\">\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\tbind:checked={ntpSettings.enabled}\n\t\t\t\t\t\tclass=\"checkbox checkbox-primary\"\n\t\t\t\t\t/>Enable NTP\n\t\t\t\t</label>\n\n\t\t\t\t<label class=\"label\" for=\"server\">Server</label>\n\t\t\t\t<input\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tmin=\"3\"\n\t\t\t\t\tmax=\"64\"\n\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.server\n\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t: ''}\"\n\t\t\t\t\tbind:value={ntpSettings.server}\n\t\t\t\t\tid=\"server\"\n\t\t\t\t\trequired\n\t\t\t\t/>\n\t\t\t\t{#if formErrors.server}\n\t\t\t\t\t<p class=\"text-error text-sm\">Please enter a valid NTP server.</p>\n\t\t\t\t{/if}\n\n\t\t\t\t<label class=\"label\" for=\"tz\">Pick Time Zone</label>\n\t\t\t\t<select class=\"select w-full\" bind:value={ntpSettings.tz_label} id=\"tz\">\n\t\t\t\t\t{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}\n\t\t\t\t\t\t<option value={tz_label}>{tz_label}</option>\n\t\t\t\t\t{/each}\n\t\t\t\t</select>\n\n\t\t\t\t<div class=\"mt-4 place-self-end\">\n\t\t\t\t\t<button class=\"btn btn-primary\" type=\"submit\">Apply Settings</button>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t</Collapsible>\n\t{/if}\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/connections/ntp/timezones.ts",
    "content": "export type TimeZones = {\n    [name: string]: string\n  };\n  \nexport const TIME_ZONES: TimeZones = {\n    \"Africa/Abidjan\": \"GMT0\",\n    \"Africa/Accra\": \"GMT0\",\n    \"Africa/Addis_Ababa\": \"EAT-3\",\n    \"Africa/Algiers\": \"CET-1\",\n    \"Africa/Asmara\": \"EAT-3\",\n    \"Africa/Bamako\": \"GMT0\",\n    \"Africa/Bangui\": \"WAT-1\",\n    \"Africa/Banjul\": \"GMT0\",\n    \"Africa/Bissau\": \"GMT0\",\n    \"Africa/Blantyre\": \"CAT-2\",\n    \"Africa/Brazzaville\": \"WAT-1\",\n    \"Africa/Bujumbura\": \"CAT-2\",\n    \"Africa/Cairo\": \"EET-2\",\n    \"Africa/Casablanca\": \"UNK-1\",\n    \"Africa/Ceuta\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Africa/Conakry\": \"GMT0\",\n    \"Africa/Dakar\": \"GMT0\",\n    \"Africa/Dar_es_Salaam\": \"EAT-3\",\n    \"Africa/Djibouti\": \"EAT-3\",\n    \"Africa/Douala\": \"WAT-1\",\n    \"Africa/El_Aaiun\": \"UNK-1\",\n    \"Africa/Freetown\": \"GMT0\",\n    \"Africa/Gaborone\": \"CAT-2\",\n    \"Africa/Harare\": \"CAT-2\",\n    \"Africa/Johannesburg\": \"SAST-2\",\n    \"Africa/Juba\": \"EAT-3\",\n    \"Africa/Kampala\": \"EAT-3\",\n    \"Africa/Khartoum\": \"CAT-2\",\n    \"Africa/Kigali\": \"CAT-2\",\n    \"Africa/Kinshasa\": \"WAT-1\",\n    \"Africa/Lagos\": \"WAT-1\",\n    \"Africa/Libreville\": \"WAT-1\",\n    \"Africa/Lome\": \"GMT0\",\n    \"Africa/Luanda\": \"WAT-1\",\n    \"Africa/Lubumbashi\": \"CAT-2\",\n    \"Africa/Lusaka\": \"CAT-2\",\n    \"Africa/Malabo\": \"WAT-1\",\n    \"Africa/Maputo\": \"CAT-2\",\n    \"Africa/Maseru\": \"SAST-2\",\n    \"Africa/Mbabane\": \"SAST-2\",\n    \"Africa/Mogadishu\": \"EAT-3\",\n    \"Africa/Monrovia\": \"GMT0\",\n    \"Africa/Nairobi\": \"EAT-3\",\n    \"Africa/Ndjamena\": \"WAT-1\",\n    \"Africa/Niamey\": \"WAT-1\",\n    \"Africa/Nouakchott\": \"GMT0\",\n    \"Africa/Ouagadougou\": \"GMT0\",\n    \"Africa/Porto-Novo\": \"WAT-1\",\n    \"Africa/Sao_Tome\": \"GMT0\",\n    \"Africa/Tripoli\": \"EET-2\",\n    \"Africa/Tunis\": \"CET-1\",\n    \"Africa/Windhoek\": \"CAT-2\",\n    \"America/Adak\": \"HST10HDT,M3.2.0,M11.1.0\",\n    \"America/Anchorage\": \"AKST9AKDT,M3.2.0,M11.1.0\",\n    \"America/Anguilla\": \"AST4\",\n    \"America/Antigua\": \"AST4\",\n    \"America/Araguaina\": \"UNK3\",\n    \"America/Argentina/Buenos_Aires\": \"UNK3\",\n    \"America/Argentina/Catamarca\": \"UNK3\",\n    \"America/Argentina/Cordoba\": \"UNK3\",\n    \"America/Argentina/Jujuy\": \"UNK3\",\n    \"America/Argentina/La_Rioja\": \"UNK3\",\n    \"America/Argentina/Mendoza\": \"UNK3\",\n    \"America/Argentina/Rio_Gallegos\": \"UNK3\",\n    \"America/Argentina/Salta\": \"UNK3\",\n    \"America/Argentina/San_Juan\": \"UNK3\",\n    \"America/Argentina/San_Luis\": \"UNK3\",\n    \"America/Argentina/Tucuman\": \"UNK3\",\n    \"America/Argentina/Ushuaia\": \"UNK3\",\n    \"America/Aruba\": \"AST4\",\n    \"America/Asuncion\": \"UNK4UNK,M10.1.0/0,M3.4.0/0\",\n    \"America/Atikokan\": \"EST5\",\n    \"America/Bahia\": \"UNK3\",\n    \"America/Bahia_Banderas\": \"CST6CDT,M4.1.0,M10.5.0\",\n    \"America/Barbados\": \"AST4\",\n    \"America/Belem\": \"UNK3\",\n    \"America/Belize\": \"CST6\",\n    \"America/Blanc-Sablon\": \"AST4\",\n    \"America/Boa_Vista\": \"UNK4\",\n    \"America/Bogota\": \"UNK5\",\n    \"America/Boise\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"America/Cambridge_Bay\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"America/Campo_Grande\": \"UNK4\",\n    \"America/Cancun\": \"EST5\",\n    \"America/Caracas\": \"UNK4\",\n    \"America/Cayenne\": \"UNK3\",\n    \"America/Cayman\": \"EST5\",\n    \"America/Chicago\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Chihuahua\": \"MST7MDT,M4.1.0,M10.5.0\",\n    \"America/Costa_Rica\": \"CST6\",\n    \"America/Creston\": \"MST7\",\n    \"America/Cuiaba\": \"UNK4\",\n    \"America/Curacao\": \"AST4\",\n    \"America/Danmarkshavn\": \"GMT0\",\n    \"America/Dawson\": \"MST7\",\n    \"America/Dawson_Creek\": \"MST7\",\n    \"America/Denver\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"America/Detroit\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Dominica\": \"AST4\",\n    \"America/Edmonton\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"America/Eirunepe\": \"UNK5\",\n    \"America/El_Salvador\": \"CST6\",\n    \"America/Fort_Nelson\": \"MST7\",\n    \"America/Fortaleza\": \"UNK3\",\n    \"America/Glace_Bay\": \"AST4ADT,M3.2.0,M11.1.0\",\n    \"America/Godthab\": \"UNK3UNK,M3.5.0/-2,M10.5.0/-1\",\n    \"America/Goose_Bay\": \"AST4ADT,M3.2.0,M11.1.0\",\n    \"America/Grand_Turk\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Grenada\": \"AST4\",\n    \"America/Guadeloupe\": \"AST4\",\n    \"America/Guatemala\": \"CST6\",\n    \"America/Guayaquil\": \"UNK5\",\n    \"America/Guyana\": \"UNK4\",\n    \"America/Halifax\": \"AST4ADT,M3.2.0,M11.1.0\",\n    \"America/Havana\": \"CST5CDT,M3.2.0/0,M11.1.0/1\",\n    \"America/Hermosillo\": \"MST7\",\n    \"America/Indiana/Indianapolis\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Knox\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Marengo\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Petersburg\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Tell_City\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Vevay\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Vincennes\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Indiana/Winamac\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Inuvik\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"America/Iqaluit\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Jamaica\": \"EST5\",\n    \"America/Juneau\": \"AKST9AKDT,M3.2.0,M11.1.0\",\n    \"America/Kentucky/Louisville\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Kentucky/Monticello\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Kralendijk\": \"AST4\",\n    \"America/La_Paz\": \"UNK4\",\n    \"America/Lima\": \"UNK5\",\n    \"America/Los_Angeles\": \"PST8PDT,M3.2.0,M11.1.0\",\n    \"America/Lower_Princes\": \"AST4\",\n    \"America/Maceio\": \"UNK3\",\n    \"America/Managua\": \"CST6\",\n    \"America/Manaus\": \"UNK4\",\n    \"America/Marigot\": \"AST4\",\n    \"America/Martinique\": \"AST4\",\n    \"America/Matamoros\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Mazatlan\": \"MST7MDT,M4.1.0,M10.5.0\",\n    \"America/Menominee\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Merida\": \"CST6CDT,M4.1.0,M10.5.0\",\n    \"America/Metlakatla\": \"AKST9AKDT,M3.2.0,M11.1.0\",\n    \"America/Mexico_City\": \"CST6CDT,M4.1.0,M10.5.0\",\n    \"America/Miquelon\": \"UNK3UNK,M3.2.0,M11.1.0\",\n    \"America/Moncton\": \"AST4ADT,M3.2.0,M11.1.0\",\n    \"America/Monterrey\": \"CST6CDT,M4.1.0,M10.5.0\",\n    \"America/Montevideo\": \"UNK3\",\n    \"America/Montreal\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Montserrat\": \"AST4\",\n    \"America/Nassau\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/New_York\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Nipigon\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Nome\": \"AKST9AKDT,M3.2.0,M11.1.0\",\n    \"America/Noronha\": \"UNK2\",\n    \"America/North_Dakota/Beulah\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/North_Dakota/Center\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/North_Dakota/New_Salem\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Ojinaga\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"America/Panama\": \"EST5\",\n    \"America/Pangnirtung\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Paramaribo\": \"UNK3\",\n    \"America/Phoenix\": \"MST7\",\n    \"America/Port-au-Prince\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Port_of_Spain\": \"AST4\",\n    \"America/Porto_Velho\": \"UNK4\",\n    \"America/Puerto_Rico\": \"AST4\",\n    \"America/Punta_Arenas\": \"UNK3\",\n    \"America/Rainy_River\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Rankin_Inlet\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Recife\": \"UNK3\",\n    \"America/Regina\": \"CST6\",\n    \"America/Resolute\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Rio_Branco\": \"UNK5\",\n    \"America/Santarem\": \"UNK3\",\n    \"America/Santiago\": \"UNK4UNK,M9.1.6/24,M4.1.6/24\",\n    \"America/Santo_Domingo\": \"AST4\",\n    \"America/Sao_Paulo\": \"UNK3\",\n    \"America/Scoresbysund\": \"UNK1UNK,M3.5.0/0,M10.5.0/1\",\n    \"America/Sitka\": \"AKST9AKDT,M3.2.0,M11.1.0\",\n    \"America/St_Barthelemy\": \"AST4\",\n    \"America/St_Johns\": \"NST3:30NDT,M3.2.0,M11.1.0\",\n    \"America/St_Kitts\": \"AST4\",\n    \"America/St_Lucia\": \"AST4\",\n    \"America/St_Thomas\": \"AST4\",\n    \"America/St_Vincent\": \"AST4\",\n    \"America/Swift_Current\": \"CST6\",\n    \"America/Tegucigalpa\": \"CST6\",\n    \"America/Thule\": \"AST4ADT,M3.2.0,M11.1.0\",\n    \"America/Thunder_Bay\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Tijuana\": \"PST8PDT,M3.2.0,M11.1.0\",\n    \"America/Toronto\": \"EST5EDT,M3.2.0,M11.1.0\",\n    \"America/Tortola\": \"AST4\",\n    \"America/Vancouver\": \"PST8PDT,M3.2.0,M11.1.0\",\n    \"America/Whitehorse\": \"MST7\",\n    \"America/Winnipeg\": \"CST6CDT,M3.2.0,M11.1.0\",\n    \"America/Yakutat\": \"AKST9AKDT,M3.2.0,M11.1.0\",\n    \"America/Yellowknife\": \"MST7MDT,M3.2.0,M11.1.0\",\n    \"Antarctica/Casey\": \"UNK-8\",\n    \"Antarctica/Davis\": \"UNK-7\",\n    \"Antarctica/DumontDUrville\": \"UNK-10\",\n    \"Antarctica/Macquarie\": \"UNK-11\",\n    \"Antarctica/Mawson\": \"UNK-5\",\n    \"Antarctica/McMurdo\": \"NZST-12NZDT,M9.5.0,M4.1.0/3\",\n    \"Antarctica/Palmer\": \"UNK3\",\n    \"Antarctica/Rothera\": \"UNK3\",\n    \"Antarctica/Syowa\": \"UNK-3\",\n    \"Antarctica/Troll\": \"UNK0UNK-2,M3.5.0/1,M10.5.0/3\",\n    \"Antarctica/Vostok\": \"UNK-6\",\n    \"Arctic/Longyearbyen\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Asia/Aden\": \"UNK-3\",\n    \"Asia/Almaty\": \"UNK-6\",\n    \"Asia/Amman\": \"EET-2EEST,M3.5.4/24,M10.5.5/1\",\n    \"Asia/Anadyr\": \"UNK-12\",\n    \"Asia/Aqtau\": \"UNK-5\",\n    \"Asia/Aqtobe\": \"UNK-5\",\n    \"Asia/Ashgabat\": \"UNK-5\",\n    \"Asia/Atyrau\": \"UNK-5\",\n    \"Asia/Baghdad\": \"UNK-3\",\n    \"Asia/Bahrain\": \"UNK-3\",\n    \"Asia/Baku\": \"UNK-4\",\n    \"Asia/Bangkok\": \"UNK-7\",\n    \"Asia/Barnaul\": \"UNK-7\",\n    \"Asia/Beirut\": \"EET-2EEST,M3.5.0/0,M10.5.0/0\",\n    \"Asia/Bishkek\": \"UNK-6\",\n    \"Asia/Brunei\": \"UNK-8\",\n    \"Asia/Chita\": \"UNK-9\",\n    \"Asia/Choibalsan\": \"UNK-8\",\n    \"Asia/Colombo\": \"UNK-5:30\",\n    \"Asia/Damascus\": \"EET-2EEST,M3.5.5/0,M10.5.5/0\",\n    \"Asia/Dhaka\": \"UNK-6\",\n    \"Asia/Dili\": \"UNK-9\",\n    \"Asia/Dubai\": \"UNK-4\",\n    \"Asia/Dushanbe\": \"UNK-5\",\n    \"Asia/Famagusta\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Asia/Gaza\": \"EET-2EEST,M3.5.5/0,M10.5.6/1\",\n    \"Asia/Hebron\": \"EET-2EEST,M3.5.5/0,M10.5.6/1\",\n    \"Asia/Ho_Chi_Minh\": \"UNK-7\",\n    \"Asia/Hong_Kong\": \"HKT-8\",\n    \"Asia/Hovd\": \"UNK-7\",\n    \"Asia/Irkutsk\": \"UNK-8\",\n    \"Asia/Jakarta\": \"WIB-7\",\n    \"Asia/Jayapura\": \"WIT-9\",\n    \"Asia/Jerusalem\": \"IST-2IDT,M3.4.4/26,M10.5.0\",\n    \"Asia/Kabul\": \"UNK-4:30\",\n    \"Asia/Kamchatka\": \"UNK-12\",\n    \"Asia/Karachi\": \"PKT-5\",\n    \"Asia/Kathmandu\": \"UNK-5:45\",\n    \"Asia/Khandyga\": \"UNK-9\",\n    \"Asia/Kolkata\": \"IST-5:30\",\n    \"Asia/Krasnoyarsk\": \"UNK-7\",\n    \"Asia/Kuala_Lumpur\": \"UNK-8\",\n    \"Asia/Kuching\": \"UNK-8\",\n    \"Asia/Kuwait\": \"UNK-3\",\n    \"Asia/Macau\": \"CST-8\",\n    \"Asia/Magadan\": \"UNK-11\",\n    \"Asia/Makassar\": \"WITA-8\",\n    \"Asia/Manila\": \"PST-8\",\n    \"Asia/Muscat\": \"UNK-4\",\n    \"Asia/Nicosia\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Asia/Novokuznetsk\": \"UNK-7\",\n    \"Asia/Novosibirsk\": \"UNK-7\",\n    \"Asia/Omsk\": \"UNK-6\",\n    \"Asia/Oral\": \"UNK-5\",\n    \"Asia/Phnom_Penh\": \"UNK-7\",\n    \"Asia/Pontianak\": \"WIB-7\",\n    \"Asia/Pyongyang\": \"KST-9\",\n    \"Asia/Qatar\": \"UNK-3\",\n    \"Asia/Qyzylorda\": \"UNK-5\",\n    \"Asia/Riyadh\": \"UNK-3\",\n    \"Asia/Sakhalin\": \"UNK-11\",\n    \"Asia/Samarkand\": \"UNK-5\",\n    \"Asia/Seoul\": \"KST-9\",\n    \"Asia/Shanghai\": \"CST-8\",\n    \"Asia/Singapore\": \"UNK-8\",\n    \"Asia/Srednekolymsk\": \"UNK-11\",\n    \"Asia/Taipei\": \"CST-8\",\n    \"Asia/Tashkent\": \"UNK-5\",\n    \"Asia/Tbilisi\": \"UNK-4\",\n    \"Asia/Tehran\": \"UNK-3:30UNK,J79/24,J263/24\",\n    \"Asia/Thimphu\": \"UNK-6\",\n    \"Asia/Tokyo\": \"JST-9\",\n    \"Asia/Tomsk\": \"UNK-7\",\n    \"Asia/Ulaanbaatar\": \"UNK-8\",\n    \"Asia/Urumqi\": \"UNK-6\",\n    \"Asia/Ust-Nera\": \"UNK-10\",\n    \"Asia/Vientiane\": \"UNK-7\",\n    \"Asia/Vladivostok\": \"UNK-10\",\n    \"Asia/Yakutsk\": \"UNK-9\",\n    \"Asia/Yangon\": \"UNK-6:30\",\n    \"Asia/Yekaterinburg\": \"UNK-5\",\n    \"Asia/Yerevan\": \"UNK-4\",\n    \"Atlantic/Azores\": \"UNK1UNK,M3.5.0/0,M10.5.0/1\",\n    \"Atlantic/Bermuda\": \"AST4ADT,M3.2.0,M11.1.0\",\n    \"Atlantic/Canary\": \"WET0WEST,M3.5.0/1,M10.5.0\",\n    \"Atlantic/Cape_Verde\": \"UNK1\",\n    \"Atlantic/Faroe\": \"WET0WEST,M3.5.0/1,M10.5.0\",\n    \"Atlantic/Madeira\": \"WET0WEST,M3.5.0/1,M10.5.0\",\n    \"Atlantic/Reykjavik\": \"GMT0\",\n    \"Atlantic/South_Georgia\": \"UNK2\",\n    \"Atlantic/St_Helena\": \"GMT0\",\n    \"Atlantic/Stanley\": \"UNK3\",\n    \"Australia/Adelaide\": \"ACST-9:30ACDT,M10.1.0,M4.1.0/3\",\n    \"Australia/Brisbane\": \"AEST-10\",\n    \"Australia/Broken_Hill\": \"ACST-9:30ACDT,M10.1.0,M4.1.0/3\",\n    \"Australia/Currie\": \"AEST-10AEDT,M10.1.0,M4.1.0/3\",\n    \"Australia/Darwin\": \"ACST-9:30\",\n    \"Australia/Eucla\": \"UNK-8:45\",\n    \"Australia/Hobart\": \"AEST-10AEDT,M10.1.0,M4.1.0/3\",\n    \"Australia/Lindeman\": \"AEST-10\",\n    \"Australia/Lord_Howe\": \"UNK-10:30UNK-11,M10.1.0,M4.1.0\",\n    \"Australia/Melbourne\": \"AEST-10AEDT,M10.1.0,M4.1.0/3\",\n    \"Australia/Perth\": \"AWST-8\",\n    \"Australia/Sydney\": \"AEST-10AEDT,M10.1.0,M4.1.0/3\",\n    \"Etc/GMT\": \"GMT0\",\n    \"Etc/GMT+0\": \"GMT0\",\n    \"Etc/GMT+1\": \"UNK1\",\n    \"Etc/GMT+10\": \"UNK10\",\n    \"Etc/GMT+11\": \"UNK11\",\n    \"Etc/GMT+12\": \"UNK12\",\n    \"Etc/GMT+2\": \"UNK2\",\n    \"Etc/GMT+3\": \"UNK3\",\n    \"Etc/GMT+4\": \"UNK4\",\n    \"Etc/GMT+5\": \"UNK5\",\n    \"Etc/GMT+6\": \"UNK6\",\n    \"Etc/GMT+7\": \"UNK7\",\n    \"Etc/GMT+8\": \"UNK8\",\n    \"Etc/GMT+9\": \"UNK9\",\n    \"Etc/GMT-0\": \"GMT0\",\n    \"Etc/GMT-1\": \"UNK-1\",\n    \"Etc/GMT-10\": \"UNK-10\",\n    \"Etc/GMT-11\": \"UNK-11\",\n    \"Etc/GMT-12\": \"UNK-12\",\n    \"Etc/GMT-13\": \"UNK-13\",\n    \"Etc/GMT-14\": \"UNK-14\",\n    \"Etc/GMT-2\": \"UNK-2\",\n    \"Etc/GMT-3\": \"UNK-3\",\n    \"Etc/GMT-4\": \"UNK-4\",\n    \"Etc/GMT-5\": \"UNK-5\",\n    \"Etc/GMT-6\": \"UNK-6\",\n    \"Etc/GMT-7\": \"UNK-7\",\n    \"Etc/GMT-8\": \"UNK-8\",\n    \"Etc/GMT-9\": \"UNK-9\",\n    \"Etc/GMT0\": \"GMT0\",\n    \"Etc/Greenwich\": \"GMT0\",\n    \"Etc/UCT\": \"UTC0\",\n    \"Etc/UTC\": \"UTC0\",\n    \"Etc/Universal\": \"UTC0\",\n    \"Etc/Zulu\": \"UTC0\",\n    \"Europe/Amsterdam\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Andorra\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Astrakhan\": \"UNK-4\",\n    \"Europe/Athens\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Belgrade\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Berlin\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Bratislava\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Brussels\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Bucharest\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Budapest\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Busingen\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Chisinau\": \"EET-2EEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Copenhagen\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Dublin\": \"IST-1GMT0,M10.5.0,M3.5.0/1\",\n    \"Europe/Gibraltar\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Guernsey\": \"GMT0BST,M3.5.0/1,M10.5.0\",\n    \"Europe/Helsinki\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Isle_of_Man\": \"GMT0BST,M3.5.0/1,M10.5.0\",\n    \"Europe/Istanbul\": \"UNK-3\",\n    \"Europe/Jersey\": \"GMT0BST,M3.5.0/1,M10.5.0\",\n    \"Europe/Kaliningrad\": \"EET-2\",\n    \"Europe/Kiev\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Kirov\": \"UNK-3\",\n    \"Europe/Lisbon\": \"WET0WEST,M3.5.0/1,M10.5.0\",\n    \"Europe/Ljubljana\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/London\": \"GMT0BST,M3.5.0/1,M10.5.0\",\n    \"Europe/Luxembourg\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Madrid\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Malta\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Mariehamn\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Minsk\": \"UNK-3\",\n    \"Europe/Monaco\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Moscow\": \"MSK-3\",\n    \"Europe/Oslo\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Paris\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Podgorica\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Prague\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Riga\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Rome\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Samara\": \"UNK-4\",\n    \"Europe/San_Marino\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Sarajevo\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Saratov\": \"UNK-4\",\n    \"Europe/Simferopol\": \"MSK-3\",\n    \"Europe/Skopje\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Sofia\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Stockholm\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Tallinn\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Tirane\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Ulyanovsk\": \"UNK-4\",\n    \"Europe/Uzhgorod\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Vaduz\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Vatican\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Vienna\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Vilnius\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Volgograd\": \"UNK-4\",\n    \"Europe/Warsaw\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Zagreb\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Europe/Zaporozhye\": \"EET-2EEST,M3.5.0/3,M10.5.0/4\",\n    \"Europe/Zurich\": \"CET-1CEST,M3.5.0,M10.5.0/3\",\n    \"Indian/Antananarivo\": \"EAT-3\",\n    \"Indian/Chagos\": \"UNK-6\",\n    \"Indian/Christmas\": \"UNK-7\",\n    \"Indian/Cocos\": \"UNK-6:30\",\n    \"Indian/Comoro\": \"EAT-3\",\n    \"Indian/Kerguelen\": \"UNK-5\",\n    \"Indian/Mahe\": \"UNK-4\",\n    \"Indian/Maldives\": \"UNK-5\",\n    \"Indian/Mauritius\": \"UNK-4\",\n    \"Indian/Mayotte\": \"EAT-3\",\n    \"Indian/Reunion\": \"UNK-4\",\n    \"Pacific/Apia\": \"UNK-13UNK,M9.5.0/3,M4.1.0/4\",\n    \"Pacific/Auckland\": \"NZST-12NZDT,M9.5.0,M4.1.0/3\",\n    \"Pacific/Bougainville\": \"UNK-11\",\n    \"Pacific/Chatham\": \"UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45\",\n    \"Pacific/Chuuk\": \"UNK-10\",\n    \"Pacific/Easter\": \"UNK6UNK,M9.1.6/22,M4.1.6/22\",\n    \"Pacific/Efate\": \"UNK-11\",\n    \"Pacific/Enderbury\": \"UNK-13\",\n    \"Pacific/Fakaofo\": \"UNK-13\",\n    \"Pacific/Fiji\": \"UNK-12UNK,M11.2.0,M1.2.3/99\",\n    \"Pacific/Funafuti\": \"UNK-12\",\n    \"Pacific/Galapagos\": \"UNK6\",\n    \"Pacific/Gambier\": \"UNK9\",\n    \"Pacific/Guadalcanal\": \"UNK-11\",\n    \"Pacific/Guam\": \"ChST-10\",\n    \"Pacific/Honolulu\": \"HST10\",\n    \"Pacific/Kiritimati\": \"UNK-14\",\n    \"Pacific/Kosrae\": \"UNK-11\",\n    \"Pacific/Kwajalein\": \"UNK-12\",\n    \"Pacific/Majuro\": \"UNK-12\",\n    \"Pacific/Marquesas\": \"UNK9:30\",\n    \"Pacific/Midway\": \"SST11\",\n    \"Pacific/Nauru\": \"UNK-12\",\n    \"Pacific/Niue\": \"UNK11\",\n    \"Pacific/Norfolk\": \"UNK-11UNK,M10.1.0,M4.1.0/3\",\n    \"Pacific/Noumea\": \"UNK-11\",\n    \"Pacific/Pago_Pago\": \"SST11\",\n    \"Pacific/Palau\": \"UNK-9\",\n    \"Pacific/Pitcairn\": \"UNK8\",\n    \"Pacific/Pohnpei\": \"UNK-11\",\n    \"Pacific/Port_Moresby\": \"UNK-10\",\n    \"Pacific/Rarotonga\": \"UNK10\",\n    \"Pacific/Saipan\": \"ChST-10\",\n    \"Pacific/Tahiti\": \"UNK10\",\n    \"Pacific/Tarawa\": \"UNK-12\",\n    \"Pacific/Tongatapu\": \"UNK-13\",\n    \"Pacific/Wake\": \"UNK-12\",\n    \"Pacific/Wallis\": \"UNK-12\"\n  };\n"
  },
  {
    "path": "interface/src/routes/demo/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from '../$types';\n\timport Demo from './Demo.svelte';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<Demo />\n</div>\n"
  },
  {
    "path": "interface/src/routes/demo/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async ({ fetch }) => {\n\treturn {\n\t\ttitle: 'Demo App'\n\t};\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/demo/Demo.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount, onDestroy } from 'svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport Light from '~icons/tabler/bulb';\n\timport Info from '~icons/tabler/info-circle';\n\timport Save from '~icons/tabler/device-floppy';\n\timport Reload from '~icons/tabler/reload';\n\timport { socket } from '$lib/stores/socket';\n\timport type { LightState } from '$lib/types/models';\n\n\tlet lightState: LightState = $state({ led_on: false });\n\n\tlet lightOn = $state(false);\n\n\tasync function getLightstate() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/lightState', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst light = await response.json();\n\t\t\tlightOn = light.led_on;\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tonMount(() => {\n\t\tsocket.on<LightState>('led', (data) => {\n\t\t\tlightState = data;\n\t\t});\n\t\tgetLightstate();\n\t});\n\n\tonDestroy(() => socket.off('led'));\n\n\tasync function postLightstate() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/lightState', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ led_on: lightOn })\n\t\t\t});\n\t\t\tif (response.status == 200) {\n\t\t\t\tnotifications.success('Light state updated.', 3000);\n\t\t\t\tconst light = await response.json();\n\t\t\t\tlightOn = light.led_on;\n\t\t\t} else {\n\t\t\t\tnotifications.error('User not authorized.', 3000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Light class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Light State</span>\n\t{/snippet}\n\t<div class=\"w-full\">\n\t\t<h1 class=\"text-xl font-semibold\">REST Example</h1>\n\t\t<div class=\"alert alert-info my-2 shadow-lg\">\n\t\t\t<Info class=\"h-6 w-6 shrink-0 stroke-current\" />\n\t\t\t<span>The form below controls the LED via the RESTful service exposed by the ESP device.</span\n\t\t\t>\n\t\t</div>\n\t\t<div class=\"flex flex-row flex-wrap justify-between gap-x-2\">\n\t\t\t<div class=\"fieldset w-52\">\n\t\t\t\t<label class=\"label cursor-pointer\">\n\t\t\t\t\t<span class=\"mr-4 text-base\">Light State?</span>\n\t\t\t\t\t<input type=\"checkbox\" bind:checked={lightOn} class=\"checkbox checkbox-primary\" />\n\t\t\t\t</label>\n\t\t\t</div>\n\t\t\t<div class=\"grow\"></div>\n\t\t\t<button class=\"btn btn-primary inline-flex items-center\" onclick={postLightstate}\n\t\t\t\t><Save class=\"mr-2 h-5 w-5\" /><span>Save</span></button\n\t\t\t>\n\t\t\t<button class=\"btn btn-primary inline-flex items-center\" onclick={getLightstate}\n\t\t\t\t><Reload class=\"mr-2 h-5 w-5\" /><span>Reload</span></button\n\t\t\t>\n\t\t</div>\n\t\t<div class=\"divider\"></div>\n\t\t<h1 class=\"text-xl font-semibold\">Event Socket Example</h1>\n\t\t<div class=\"alert alert-info my-2 shadow-lg\">\n\t\t\t<Info class=\"h-6 w-6 shrink-0 stroke-current\" />\n\t\t\t<span\n\t\t\t\t>The switch below controls the LED via the event system which uses WebSocket under the hood.\n\t\t\t\tIt will automatically update whenever the LED state changes.</span\n\t\t\t>\n\t\t</div>\n\t\t<div class=\"fieldset w-52\">\n\t\t\t<label class=\"label cursor-pointer\">\n\t\t\t\t<span class=\"text-base\">Light State?</span>\n\t\t\t\t<input\n\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\tclass=\"toggle toggle-primary\"\n\t\t\t\t\tbind:checked={lightState.led_on}\n\t\t\t\t\tonchange={() => {\n\t\t\t\t\t\tsocket.sendEvent('led', lightState);\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</label>\n\t\t</div>\n\t</div>\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/ethernet/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport Ethernet from './Ethernet.svelte';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<Ethernet />\n</div>\n"
  },
  {
    "path": "interface/src/routes/ethernet/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn {\n\t\ttitle: 'Ethernet'\n\t};\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/ethernet/Ethernet.svelte",
    "content": "<script lang=\"ts\">\n\timport { onDestroy, onMount } from 'svelte';\n\timport { socket } from '$lib/stores/socket';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport Collapsible from '$lib/components/Collapsible.svelte';\n\timport type { EthernetSettings, EthernetStatus } from '$lib/types/models';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport Settings from '~icons/tabler/settings';\n\timport MAC from '~icons/tabler/dna-2';\n\timport Home from '~icons/tabler/home';\n\timport PlugConnected from '~icons/tabler/plug-connected';\n\timport DNS from '~icons/tabler/address-book';\n\timport Gateway from '~icons/tabler/torii';\n\timport Subnet from '~icons/tabler/grid-dots';\n\timport Cancel from '~icons/tabler/x';\n\timport Save from '~icons/tabler/device-floppy';\n\n\tinterface Props {\n\t\tethernetEditable?: EthernetSettings;\n\t}\n\n\tlet {\n\t\tethernetEditable: _ethernetEditable = {\n\t\t\thostname: '',\n\t\t\tstatic_ip_config: false,\n\t\t\tlocal_ip: undefined,\n\t\t\tsubnet_mask: undefined,\n\t\t\tgateway_ip: undefined,\n\t\t\tdns_ip_1: undefined,\n\t\t\tdns_ip_2: undefined\n\t\t} as EthernetSettings\n\t}: Props = $props();\n\n\tlet ethernetStatus: EthernetStatus = $state({\n\t\tconnected: false,\n\t\tlocal_ip: '',\n\t\tmac_address: '',\n\t\tsubnet_mask: '',\n\t\tgateway_ip: '',\n\t\tdns_ip_1: '',\n\t\tdns_ip_2: '',\n\t\tlink_speed: 0,\n\t});\n\n\tlet ethernetEditable = $state(_ethernetEditable);\n\n\t// Stringify to recognize changes\n\t// svelte-ignore state_referenced_locally\n\tlet strEthernetEditable: string = $state(JSON.stringify(_ethernetEditable, emptyStringToUndefinedReplacer));\n\t// Recognize changes in settings\n\tlet isSettingsDirty: boolean = $derived(JSON.stringify(ethernetEditable, emptyStringToUndefinedReplacer) !== strEthernetEditable);\n\n\t// Use this to directly access the form's DOM element\n\tlet formField: any = $state();\n\n\tlet formErrors = $state({\n\t\thostname: false,\n\t\tlocal_ip: false,\n\t\tgateway_ip: false,\n\t\tsubnet_mask: false,\n\t\tdns_1: false,\n\t\tdns_2: false\n\t});\n\n\tfunction emptyStringToUndefinedReplacer(key: string, value: any): any {\n\t\tif (value === '') {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn value; \n\t}\n\n\tfunction validateAndApplyNetworkSettings() {\n\t\tlet valid = true;\n\n\t\tif (ethernetEditable.hostname.length < 3 || ethernetEditable.hostname.length > 32) {\n\t\t\tvalid = false;\n\t\t\tformErrors.hostname = true;\n\t\t} else {\n\t\t\tformErrors.hostname = false;\n\t\t}\n\n\t\tif (ethernetEditable.static_ip_config) {\n\t\t\t// RegEx for IPv4\n\t\t\tconst regexExp =\n\t\t\t\t/\\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\\b/;\n\t\t\t// RegEx for IPv4 OR empty string\n\t\t\tconst regexExpAllowEmpty =\n\t\t\t\t/^(?:$|\\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\\b)$/;\n\n\t\t\t// Validate gateway IP\n\t\t\tif (!regexExpAllowEmpty.test(ethernetEditable.gateway_ip || '')) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.gateway_ip = true;\n\t\t\t} else {\n\t\t\t\tformErrors.gateway_ip = false;\n\t\t\t}\n\n\t\t\t// Validate Subnet Mask\n\t\t\tif (!regexExp.test(ethernetEditable.subnet_mask!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.subnet_mask = true;\n\t\t\t} else {\n\t\t\t\tformErrors.subnet_mask = false;\n\t\t\t}\n\n\t\t\t// Validate local IP\n\t\t\tif (!regexExp.test(ethernetEditable.local_ip!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.local_ip = true;\n\t\t\t} else {\n\t\t\t\tformErrors.local_ip = false;\n\t\t\t}\n\n\t\t\t// Validate DNS 1\n\t\t\tif (!regexExpAllowEmpty.test(ethernetEditable.dns_ip_1 || '')) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.dns_1 = true;\n\t\t\t} else {\n\t\t\t\tformErrors.dns_1 = false;\n\t\t\t}\n\n\t\t\t// Validate DNS 2\n\t\t\tif (!regexExpAllowEmpty.test(ethernetEditable.dns_ip_2 || '')) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.dns_2 = true;\n\t\t\t} else {\n\t\t\t\tformErrors.dns_2 = false;\n\t\t\t}\n\t\t} else {\n\t\t\tformErrors.local_ip = false;\n\t\t\tformErrors.subnet_mask = false;\n\t\t\tformErrors.gateway_ip = false;\n\t\t\tformErrors.dns_1 = false;\n\t\t\tformErrors.dns_2 = false;\n\t\t}\n\n\t\tif (valid) {\n\t\t\tpostEthernetSettings();\n\t\t}\n\t}\n\n\n\tasync function getEthernetStatus() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/ethernetStatus', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tethernetStatus = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tasync function getEthernetSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/ethernetSettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tethernetEditable = await response.json();\n\t\t\tstrEthernetEditable = JSON.stringify(ethernetEditable); // Store the recently loaded settings in a string variable\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tasync function postEthernetSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/ethernetSettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(ethernetEditable)\n\t\t\t});\n\t\t\tif (response.status == 200) {\n\t\t\t\tlet newEthernetEditable: EthernetSettings = await response.json();\n\t\t\t\tethernetEditable = newEthernetEditable;\n\t\t\t\tstrEthernetEditable = JSON.stringify(ethernetEditable); // Store the recently loaded settings in a string variable\n\t\t\t\tnotifications.success('Ethernet settings updated.', 3000);\n\t\t\t} else {\n\t\t\t\tnotifications.error('Failed to update Ethernet settings.', 5000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tnotifications.error('Error updating Ethernet settings.', 5000);\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tconst interval = setInterval(async () => {\n\t\tgetEthernetStatus();\n\t}, 5000);\n\n\tonDestroy(() => {\n\t\tclearInterval(interval);\n\t\tsocket.off('reconnect');\n\t});\n\n\tasync function getEthernetData() {\n\t\tawait getEthernetStatus();\n\t\tawait getEthernetSettings();\n\t}\n\n\tfunction preventDefault(fn: (event: Event) => void) {\n\t\treturn function (event: Event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn(event);\n\t\t};\n\t}\n\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<PlugConnected class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Ethernet Connection</span>\n\t{/snippet}\n\t{#await getEthernetData()}\n\t\t<Spinner />\n\t{:then nothing}\n\t\t<div class=\"w-full overflow-x-auto\">\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"mask mask-hexagon h-auto w-10 {ethernetStatus.connected\n\t\t\t\t\t\t\t? 'bg-success'\n\t\t\t\t\t\t\t: 'bg-error'}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<PlugConnected\n\t\t\t\t\t\t\tclass=\"h-auto w-full scale-75 {ethernetStatus.connected\n\t\t\t\t\t\t\t\t? 'text-success-content'\n\t\t\t\t\t\t\t\t: 'text-error-content'}\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Status</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{ethernetStatus.connected ? 'Connected' : 'Disconnected'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{#if ethernetStatus.connected}\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Home class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">IP Address</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{ethernetStatus.local_ip}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tclass=\"flex w-full flex-col space-y-1 pt-1\"\n\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t>\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<MAC class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">MAC Address</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{ethernetStatus.mac_address}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<MAC class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">Link Speed</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{ethernetStatus.link_speed} Mbps\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Gateway class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">Gateway IP</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{ethernetStatus.gateway_ip}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Subnet class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">Subnet Mask</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{ethernetStatus.subnet_mask}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<DNS class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">DNS</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{ethernetStatus.dns_ip_1}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</div>\n\n\t\t{#if !page.data.features.security || $user.admin}\n\t\t\t<Collapsible open={true} class=\"shadow-lg\" isDirty={isSettingsDirty}>\n\t\t\t\t{#snippet icon()}\n\t\t\t\t\t<Settings class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t\t\t\t{/snippet}\n\t\t\t\t{#snippet title()}\n\t\t\t\t\t<span>Settings</span>\n\t\t\t\t{/snippet}\n\t\n\t\t\t\t<form\n\t\t\t\t\tclass=\"fieldset\"\n\t\t\t\t\tonsubmit={preventDefault(validateAndApplyNetworkSettings)}\n\t\t\t\t\tnovalidate\n\t\t\t\t\tbind:this={formField}\n\t\t\t\t>\n\t\t\t\t<div>\n\t\t\t\t\t<label class=\"label\" for=\"hostname\">Host Name (mDNS)</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\tmin=\"3\"\n\t\t\t\t\t\tmax=\"32\"\n\t\t\t\t\t\tclass=\"input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.hostname\n\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\tbind:value={ethernetEditable.hostname}\n\t\t\t\t\t\tid=\"hostname\"\n\t\t\t\t\t\trequired\n\t\t\t\t\t/>\n\t\t\t\t\t{#if formErrors.hostname}\n\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t<label for=\"hostname\" class=\"label\">\n\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\tHost name must be between 3 and 32 characters long.\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"grid w-full grid-cols-1 content-center gap-4 px-4 p-4 sm:grid-cols-2\"\n\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t>\n\t\t\t\t\t<label\n\t\t\t\t\t\tclass=\"label inline-flex cursor-pointer content-end justify-start gap-4 sm:col-span-2\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tbind:checked={ethernetEditable.static_ip_config}\n\t\t\t\t\t\t\tclass=\"checkbox checkbox-primary\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>Use static IP config</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\n\t\t\t\t{#if ethernetEditable.static_ip_config}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"grid w-full grid-cols-1 content-center mt-4 gap-4 px-4 sm:grid-cols-2\"\n\t\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"localIP\">Local IP</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.local_ip\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={ethernetEditable.local_ip}\n\t\t\t\t\t\t\t\tid=\"localIP\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.local_ip}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"localIP\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tLocal IP must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"gateway\">Gateway IP</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.gateway_ip\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={ethernetEditable.gateway_ip}\n\t\t\t\t\t\t\t\tid=\"gateway\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.gateway_ip}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"gateway\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tGateway IP must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"subnet\">Subnet Mask</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.subnet_mask\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={ethernetEditable.subnet_mask}\n\t\t\t\t\t\t\t\tid=\"subnet\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.subnet_mask}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"subnet\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tSubnet Mask must be a valid IPv4 subnet mask.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"dns_1\">DNS 1</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.dns_1\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={ethernetEditable.dns_ip_1}\n\t\t\t\t\t\t\t\tid=\"dns_1\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.dns_1}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"dns_1\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tDNS 1 must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"dns_2\">DNS 2</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.dns_2\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={ethernetEditable.dns_ip_2}\n\t\t\t\t\t\t\t\tid=\"dns_2\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.dns_2}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"dns_2\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tDNS 2 must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\n\t\t\t\t\t<div class=\"divider mt-2 mb-0\"></div>\n\n\t\t\t\t\t<div class=\"flex flex-wrap justify-end mb-4\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tclass=\"btn btn-primary\"\n\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\tdisabled={!isSettingsDirty}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Save class=\"mr-2 h-5 w-5\" />\n\t\t\t\t\t\t\t<span>Apply Settings</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tclass=\"btn btn-neutral ml-2\"\n\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\tethernetEditable = JSON.parse(strEthernetEditable);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tdisabled={!isSettingsDirty}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Cancel class=\"mr-2 h-5 w-5\" />\n\t\t\t\t\t\t\t<span>Discard Changes</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t</form>\n\n\t\t\t</Collapsible>\n\t\t{/if}\n\t{/await}\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/login.svelte",
    "content": "<script lang=\"ts\">\n\timport logo from '$lib/assets/logo.png';\n\timport InputPassword from '$lib/components/InputPassword.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport { fade, fly } from 'svelte/transition';\n\timport Login from '~icons/tabler/login';\n\n\ttype SignInData = {\n\t\tpassword: string;\n\t\tusername: string;\n\t};\n\n\tlet { signIn } = $props();\n\n\tlet username = $state('');\n\tlet password = $state('');\n\n\tlet loginFailed = $state(false);\n\n\tlet token = { access_token: '' };\n\n\tasync function signInUser(data: SignInData) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/signIn', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(data)\n\t\t\t});\n\t\t\tif (response.status === 200) {\n\t\t\t\ttoken = await response.json();\n\t\t\t\tuser.init(token.access_token);\n\t\t\t\tlet username = $user.username;\n\t\t\t\tnotifications.success('User ' + username + ' signed in', 5000);\n\t\t\t\tsignIn();\n\t\t\t} else {\n\t\t\t\tusername = '';\n\t\t\t\tpassword = '';\n\t\t\t\tnotifications.error('Wrong Username or Password!', 5000);\n\t\t\t\tloginFailed = true;\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tloginFailed = false;\n\t\t\t\t}, 1500);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n</script>\n\n<div class=\"hero from-primary/30 to-secondary/30 min-h-screen bg-linear-to-br\">\n\t<div\n\t\tclass=\"card lg:card-side bg-base-100 shadow-primary face shadow-2xl {loginFailed\n\t\t\t? 'failure border-error border-2'\n\t\t\t: ''}\"\n\t\tin:fly={{ delay: 200, y: 100, duration: 500 }}\n\t\tout:fade={{ duration: 200 }}\n\t>\n\t\t<figure class=\"bg-base-200 p-4\">\n\t\t\t<div class=\"image-container\">\n\t\t\t\t<img src={logo} alt=\"Logo\" class=\"responsive-image\" />\n\t\t\t</div>\n\t\t</figure>\n\t\t<div class=\"card-body w-80\">\n\t\t\t<h2 class=\"card-title text-2xl\">Login</h2>\n\t\t\t<form class=\"fieldset w-full max-w-xs\">\n\t\t\t\t<label class=\"label\" for=\"user\">Username</label>\n\t\t\t\t<input type=\"text\" class=\"input w-full max-w-xs\" id=\"user\" bind:value={username} />\n\n\t\t\t\t<label class=\"label\" for=\"pwd\">Password </label>\n\t\t\t\t<InputPassword id=\"pwd\" bind:value={password} />\n\n\t\t\t\t<div class=\"card-actions mt-4 justify-end\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary inline-flex items-center\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tsignInUser({ username: username, password: password });\n\t\t\t\t\t\t}}><Login class=\"mr-2 h-5 w-5\" /><span>Login</span></button\n\t\t\t\t\t>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</div>\n\n<style>\n\t.image-container {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\t\n\t.responsive-image {\n\t\tmax-width: 100%;\n\t\tmax-height: 100%;\n\t\twidth: auto;\n\t\theight: auto;\n\t\tobject-fit: contain;\n\t}\n\t\n\t.failure {\n\t\tanimation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n\t\ttransform: translate3d(0, 0, 0);\n\t\tbackface-visibility: hidden;\n\t\tperspective: 1000px;\n\t}\n\t@keyframes shake {\n\t\t10%,\n\t\t90% {\n\t\t\ttransform: translatex(-1px);\n\t\t}\n\n\t\t20%,\n\t\t80% {\n\t\t\ttransform: translatex(2px);\n\t\t}\n\n\t\t30%,\n\t\t50%,\n\t\t70% {\n\t\t\ttransform: translatex(-4px);\n\t\t}\n\n\t\t40%,\n\t\t60% {\n\t\t\ttransform: translatex(4px);\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "interface/src/routes/menu.svelte",
    "content": "<script lang=\"ts\">\n\timport logo from '$lib/assets/logo.png';\n\timport Github from '~icons/tabler/brand-github';\n\timport Discord from '~icons/tabler/brand-discord';\n\timport Users from '~icons/tabler/users';\n\timport Settings from '~icons/tabler/settings';\n\timport Health from '~icons/tabler/stethoscope';\n\timport Update from '~icons/tabler/refresh-alert';\n\timport WiFi from '~icons/tabler/wifi';\n\timport Router from '~icons/tabler/router';\n\timport AP from '~icons/tabler/access-point';\n\timport Remote from '~icons/tabler/network';\n\timport Control from '~icons/tabler/adjustments';\n\timport Avatar from '~icons/tabler/user-circle';\n\timport Logout from '~icons/tabler/logout';\n\timport Copyright from '~icons/tabler/copyright';\n\timport MQTT from '~icons/tabler/topology-star-3';\n\timport NTP from '~icons/tabler/clock-check';\n\timport Metrics from '~icons/tabler/report-analytics';\n\timport Bug from '~icons/tabler/bug';\n\timport PlugConnected from '~icons/tabler/plug-connected';\n\timport { page } from '$app/state';\n\timport { user } from '$lib/stores/user';\n\n\tlet { closeMenu } = $props();\n\n\tconst github = { href: 'https://github.com/' + page.data.github, active: true };\n\n\tconst discord = { href: 'https://discord.gg/MTn9mVUG5n', active: true };\n\n\ttype menuItem = {\n\t\ttitle: string;\n\t\ticon: ConstructorOfATypedSvelteComponent;\n\t\thref?: string;\n\t\tfeature: boolean;\n\t\tactive?: boolean;\n\t\tsubmenu?: subMenuItem[];\n\t};\n\n\ttype subMenuItem = {\n\t\ttitle: string;\n\t\ticon: ConstructorOfATypedSvelteComponent;\n\t\thref: string;\n\t\tfeature: boolean;\n\t\tactive: boolean;\n\t};\n\n\tlet menuItems = $state([\n\t\t{\n\t\t\ttitle: 'Demo App',\n\t\t\ticon: Control,\n\t\t\thref: '/demo',\n\t\t\tfeature: true\n\t\t},\n\t\t{\n\t\t\ttitle: 'Connections',\n\t\t\ticon: Remote,\n\t\t\tfeature: page.data.features.mqtt || page.data.features.ntp,\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\ttitle: 'MQTT',\n\t\t\t\t\ticon: MQTT,\n\t\t\t\t\thref: '/connections/mqtt',\n\t\t\t\t\tfeature: page.data.features.mqtt\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: 'NTP',\n\t\t\t\t\ticon: NTP,\n\t\t\t\t\thref: '/connections/ntp',\n\t\t\t\t\tfeature: page.data.features.ntp\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\ttitle: 'WiFi',\n\t\t\ticon: WiFi,\n\t\t\tfeature: true,\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\ttitle: 'WiFi Station',\n\t\t\t\t\ticon: Router,\n\t\t\t\t\thref: '/wifi/sta',\n\t\t\t\t\tfeature: true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: 'Access Point',\n\t\t\t\t\ticon: AP,\n\t\t\t\t\thref: '/wifi/ap',\n\t\t\t\t\tfeature: true\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\ttitle: 'Ethernet',\n\t\t\ticon: PlugConnected,\n\t\t\thref: '/ethernet',\n\t\t\tfeature: page.data.features.ethernet\n\t\t},\n\t\t{\n\t\t\ttitle: 'Users',\n\t\t\ticon: Users,\n\t\t\thref: '/user',\n\t\t\tfeature: page.data.features.security && $user.admin\n\t\t},\n\t\t{\n\t\t\ttitle: 'System',\n\t\t\ticon: Settings,\n\t\t\tfeature: true,\n\t\t\tsubmenu: [\n\t\t\t\t{\n\t\t\t\t\ttitle: 'System Status',\n\t\t\t\t\ticon: Health,\n\t\t\t\t\thref: '/system/status',\n\t\t\t\t\tfeature: true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: 'System Metrics',\n\t\t\t\t\ticon: Metrics,\n\t\t\t\t\thref: '/system/metrics',\n\t\t\t\t\tfeature: page.data.features.analytics\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: 'Core Dump',\n\t\t\t\t\ticon: Bug,\n\t\t\t\t\thref: '/system/coredump',\n\t\t\t\t\tfeature: page.data.features.coredump\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: 'Firmware Update',\n\t\t\t\t\ticon: Update,\n\t\t\t\t\thref: '/system/update',\n\t\t\t\t\tfeature:\n\t\t\t\t\t\t(page.data.features.ota ||\n\t\t\t\t\t\t\tpage.data.features.upload_firmware ||\n\t\t\t\t\t\t\tpage.data.features.download_firmware) &&\n\t\t\t\t\t\t(!page.data.features.security || $user.admin)\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t] as menuItem[]);\n\n\tfunction setActiveMenuItem(targetTitle: string) {\n\t\tmenuItems.forEach((item) => {\n\t\t\titem.active = item.title === targetTitle;\n\t\t\titem.submenu?.forEach((subItem) => {\n\t\t\t\tsubItem.active = subItem.title === targetTitle;\n\t\t\t});\n\t\t});\n\t\tcloseMenu();\n\t}\n\n\t$effect(() => {\n\t\tsetActiveMenuItem(page.data.title);\n\t});\n</script>\n\n<div class=\"bg-base-200 text-base-content flex h-full w-80 flex-col p-4\">\n\t<!-- Sidebar content here -->\n\t<a\n\t\thref=\"/\"\n\t\tclass=\"rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]\"\n\t\tonclick={() => setActiveMenuItem('')}\n\t>\n\t\t<img src={logo} alt=\"Logo\" class=\"max-h-12 max-w-12 h-auto w-auto object-contain\" />\n\t\t<h1 class=\"px-4 text-2xl font-bold\">{page.data.appName}</h1>\n\t</a>\n\t<ul class=\"menu w-full rounded-box menu-vertical flex-nowrap overflow-y-auto\">\n\t\t{#each menuItems as menuItem, i (menuItem.title)}\n\t\t\t{#if menuItem.feature}\n\t\t\t\t<li>\n\t\t\t\t\t{#if menuItem.submenu}\n\t\t\t\t\t\t<details open={menuItem.submenu.some((subItem) => subItem.active)}>\n\t\t\t\t\t\t\t<summary class=\"text-lg font-bold\">\n\t\t\t\t\t\t\t\t<menuItem.icon class=\"h-6 w-6\" />\n\t\t\t\t\t\t\t\t{menuItem.title}\n\t\t\t\t\t\t\t</summary>\n\t\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t\t{#each menuItem.submenu as subMenuItem}\n\t\t\t\t\t\t\t\t\t{#if subMenuItem.feature}\n\t\t\t\t\t\t\t\t\t\t<li class=\"hover-bordered\">\n\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\thref={subMenuItem.href}\n\t\t\t\t\t\t\t\t\t\t\t\tclass:bg-base-100={subMenuItem.active}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"text-ml font-bold\"\n\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsetActiveMenuItem(subMenuItem.title);\n\t\t\t\t\t\t\t\t\t\t\t\t}}><subMenuItem.icon class=\"h-5 w-5\" />{subMenuItem.title}</a\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t</details>\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref={menuItem.href}\n\t\t\t\t\t\t\tclass:bg-base-100={menuItem.active}\n\t\t\t\t\t\t\tclass=\"text-lg font-bold\"\n\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\tsetActiveMenuItem(menuItem.title);\n\t\t\t\t\t\t\t}}><menuItem.icon class=\"h-6 w-6\" />{menuItem.title}</a\n\t\t\t\t\t\t>\n\t\t\t\t\t{/if}\n\t\t\t\t</li>\n\t\t\t{/if}\n\t\t{/each}\n\t</ul>\n\n\t<div class=\"flex-col\"></div>\n\t<div class=\"grow\"></div>\n\n\t{#if page.data.features.security}\n\t\t<div class=\"flex items-center\">\n\t\t\t<Avatar class=\"h-8 w-8\" />\n\t\t\t<span class=\"grow px-4 text-xl font-bold\">{$user.username}</span>\n\t\t\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t\t\t<!-- svelte-ignore a11y_no_static_element_interactions -->\n\t\t\t<div\n\t\t\t\tclass=\"btn btn-ghost\"\n\t\t\t\tonclick={() => {\n\t\t\t\t\tuser.invalidate();\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Logout class=\"h-8 w-8 rotate-180\" />\n\t\t\t</div>\n\t\t</div>\n\t{/if}\n\n\t<div class=\"divider my-0\"></div>\n\t<div class=\"flex items-center\">\n\t\t{#if github.active}\n\t\t\t<a href={github.href} class=\"btn btn-ghost\" target=\"_blank\" rel=\"noopener noreferrer\"\n\t\t\t\t><Github class=\"h-5 w-5\" /></a\n\t\t\t>\n\t\t{/if}\n\t\t{#if discord.active}\n\t\t\t<a href={discord.href} class=\"btn btn-ghost\" target=\"_blank\" rel=\"noopener noreferrer\"\n\t\t\t\t><Discord class=\"h-5 w-5\" /></a\n\t\t\t>\n\t\t{/if}\n\t\t<div class=\"inline-flex grow items-center justify-end text-sm\">\n\t\t\t<Copyright class=\"h-4 w-4\" /><span class=\"px-2\">{page.data.copyright}</span>\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "interface/src/routes/statusbar.svelte",
    "content": "<script lang=\"ts\">\n\timport { page } from '$app/state';\n\timport { telemetry } from '$lib/stores/telemetry';\n\timport { modals } from 'svelte-modals';\n\timport { user } from '$lib/stores/user';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport WiFiOff from '~icons/tabler/wifi-off';\n\timport Hamburger from '~icons/tabler/menu-2';\n\timport Power from '~icons/tabler/power';\n\timport Cancel from '~icons/tabler/x';\n\timport RssiIndicator from '$lib/components/RSSIIndicator.svelte';\n\timport BatteryIndicator from '$lib/components/BatteryIndicator.svelte';\n\timport UpdateIndicator from '$lib/components/UpdateIndicator.svelte';\n\timport PlugConnected from '~icons/tabler/plug-connected';\n\timport PlugConnectedX from '~icons/tabler/plug-connected-x';\n\n\tasync function postSleep() {\n\t\tconst response = await fetch('/rest/sleep', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction confirmSleep() {\n\t\tmodals.open(ConfirmDialog, {\n\t\t\ttitle: 'Confirm Power Down',\n\t\t\tmessage: 'Are you sure you want to switch off the device?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Switch Off', icon: Power }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tmodals.close();\n\t\t\t\tpostSleep();\n\t\t\t}\n\t\t});\n\t}\n</script>\n\n<div class=\"navbar bg-base-300 sticky top-0 z-10 h-12 min-h-fit drop-shadow-lg lg:h-16\">\n\t<div class=\"flex-1 flex items-center justify-left\">\n\t\t<!-- Page Hamburger Icon here -->\n\t\t<label for=\"main-menu\" class=\"btn btn-ghost btn-circle btn-sm drawer-button lg:hidden\"\n\t\t\t><Hamburger class=\"h-6 w-auto\" /></label\n\t\t>\n\t\t<span class=\"inline-block px-2 text-xl font-bold lg:text-2xl\">{page.data.title}</span>\n\t</div>\n\t<div class=\"indicator flex-none\">\n\t\t<UpdateIndicator />\n\t</div>\n\t<div class=\"flex-none\">\n\t\t{#if page.data.features.ethernet}\n\t\t\t{#if $telemetry.ethernet.connected}\n\t\t\t\t<PlugConnected class=\"inline-block h-7 w-7\" />\n\t\t\t{:else}\n\t\t\t\t<PlugConnectedX class=\"inline-block h-7 w-7\" />\n\t\t\t{/if}\n\t\t{/if}\n\t\t{#if $telemetry.rssi.disconnected}\n\t\t\t<WiFiOff class=\"inline-block h-7 w-7\" />\n\t\t{:else}\n\t\t\t<RssiIndicator\n\t\t\t\tshowDBm={false}\n\t\t\t\trssi_dbm={$telemetry.rssi.rssi}\n\t\t\t\tssid={$telemetry.rssi.ssid}\n\t\t\t\tclass=\"inline-block h-7 w-7\"\n\t\t\t/>\n\t\t{/if}\n\t</div>\n\n\t{#if page.data.features.battery}\n\t\t<div class=\"flex-none\">\n\t\t\t<BatteryIndicator\n\t\t\t\tcharging={$telemetry.battery.charging}\n\t\t\t\tsoc={$telemetry.battery.soc}\n\t\t\t\tclass=\"inline-block h-7 w-7\"\n\t\t\t/>\n\t\t</div>\n\t{/if}\n\n\t{#if page.data.features.sleep}\n\t\t<div class=\"flex-none\">\n\t\t\t<button class=\"btn btn-square btn-ghost h-9 w-10\" onclick={confirmSleep}>\n\t\t\t\t<Power class=\"text-error h-9 w-9\" />\n\t\t\t</button>\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "interface/src/routes/system/+page.ts",
    "content": "import type { PageLoad } from './$types';\nimport { goto } from '$app/navigation';\n\nexport const load = (async () => {\n\tgoto('/');\n\treturn;\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/system/coredump/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount } from 'svelte';\n\timport { page } from '$app/state';\n\timport { user } from '$lib/stores/user';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport CoreDump from '~icons/tabler/bug';\n\timport Info from '~icons/tabler/info-circle';\n\n\tlet coreDumpBlob: Blob | null = null;\n\tlet errorMessage: string | null = null;\n\n\tonMount(async () => {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/coreDump', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/octet-stream' // Expect binary data\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (response.ok) {\n\t\t\t\tcoreDumpBlob = await response.blob();\n\t\t\t} else {\n\t\t\t\terrorMessage = 'No core dump available.';\n\t\t\t}\n\t\t} catch (e) {\n\t\t\terrorMessage = 'No core dump available.';\n\t\t}\n\t});\n\n\tfunction downloadCoreDump() {\n\t\tif (coreDumpBlob) {\n\t\t\tconst url = URL.createObjectURL(coreDumpBlob);\n\t\t\tconst a = document.createElement('a');\n\t\t\ta.href = url;\n\t\t\ta.download = 'coredump.bin';\n\t\t\tdocument.body.appendChild(a);\n\t\t\ta.click();\n\t\t\tdocument.body.removeChild(a);\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t}\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<SettingsCard collapsible={false}>\n\t\t{#snippet icon()}\n\t\t\t<CoreDump class=\"lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full\" />\n\t\t{/snippet}\n\t\t{#snippet title()}\n\t\t\t<span>Core Dump</span>\n\t\t{/snippet}\n\t\t<div class=\"alert alert-info shadow-lg\">\n\t\t\t<Info class=\"h-6 w-6 shrink-0\" />\n\t\t\t<span\n\t\t\t\t>This page displays the last core dump of the device. The core dump is a snapshot of the\n\t\t\t\tmemory when the device crashed. This information is useful for debugging.</span\n\t\t\t>\n\t\t</div>\n\t\t{#if coreDumpBlob}\n\t\t\t<button class=\"btn btn-primary mt-4\" on:click={downloadCoreDump}>\n\t\t\t\tDownload Core Dump (coredump.bin)\n\t\t\t</button>\n\t\t{:else if errorMessage}\n\t\t\t<p class=\"text-error mt-4\">{errorMessage}</p>\n\t\t{:else}\n\t\t\t<p class=\"mt-4\">Loading core dump...</p>\n\t\t{/if}\n\t</SettingsCard>\n</div>\n"
  },
  {
    "path": "interface/src/routes/system/coredump/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn { title: 'Core Dump' };\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/system/metrics/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport SystemMetrics from './SystemMetrics.svelte';\n\timport BatteryMetrics from './BatteryMetrics.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { goto } from '$app/navigation';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n\n\tif (!page.data.features.analytics && !page.data.features.battery) {\n\t\tgoto('/');\n\t}\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t{#if page.data.features.analytics}\n\t\t<SystemMetrics />\n\t{/if}\n\t{#if page.data.features.battery}\n\t\t<BatteryMetrics />\n\t{/if}\n</div>\n"
  },
  {
    "path": "interface/src/routes/system/metrics/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn { title: 'System Metrics' };\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/system/metrics/BatteryMetrics.svelte",
    "content": "<script lang=\"ts\">\n\timport { onDestroy, onMount } from 'svelte';\n\timport { page } from '$app/state';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport { Chart, registerables } from 'chart.js';\n\timport * as LuxonAdapter from 'chartjs-adapter-luxon';\n\timport Battery from '~icons/tabler/battery-automotive';\n\timport { daisyColor } from '$lib/DaisyUiHelper';\n\timport { batteryHistory } from '$lib/stores/battery';\n\n\tChart.register(...registerables);\n\tChart.register(LuxonAdapter);\n\n\tlet heapChartElement: HTMLCanvasElement = $state();\n\tlet heapChart: Chart;\n\n\tonMount(() => {\n\t\theapChart = new Chart(heapChartElement, {\n\t\t\ttype: 'line',\n\t\t\tdata: {\n\t\t\t\tlabels: $batteryHistory.timestamp,\n\t\t\t\tdatasets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'SOC',\n\t\t\t\t\t\tborderColor: daisyColor('--color-primary'),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-primary', 50),\n\t\t\t\t\t\tborderWidth: 2,\n\t\t\t\t\t\tdata: $batteryHistory.soc,\n\t\t\t\t\t\tyAxisID: 'y1'\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Charging',\n\t\t\t\t\t\tborderColor: daisyColor('--color-secondary', 25),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-secondary', 25),\n\t\t\t\t\t\tborderWidth: 0,\n\t\t\t\t\t\tdata: $batteryHistory.charging,\n\t\t\t\t\t\tfill: true,\n\t\t\t\t\t\tstepped: true,\n\t\t\t\t\t\tyAxisID: 'y2'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\toptions: {\n\t\t\t\tmaintainAspectRatio: false,\n\t\t\t\tresponsive: true,\n\t\t\t\tplugins: {\n\t\t\t\t\tlegend: {\n\t\t\t\t\t\tdisplay: true\n\t\t\t\t\t},\n\t\t\t\t\ttooltip: {\n\t\t\t\t\t\tmode: 'index',\n\t\t\t\t\t\tintersect: false\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\telements: {\n\t\t\t\t\tpoint: {\n\t\t\t\t\t\tradius: 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tscales: {\n\t\t\t\t\tx: {\n\t\t\t\t\t\ttype: 'time',\n\t\t\t\t\t\tgrid: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content', 10)\n\t\t\t\t\t\t},\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdisplay: true\n\t\t\t\t\t},\n\t\t\t\t\ty1: {\n\t\t\t\t\t\ttype: 'linear',\n\t\t\t\t\t\ttitle: {\n\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\ttext: 'State of Charge [%]',\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content'),\n\t\t\t\t\t\t\tfont: {\n\t\t\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t\t\t\tweight: 'bold'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tposition: 'left',\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tsuggestedMax: 100,\n\t\t\t\t\t\tgrid: { color: daisyColor('--color-base-content', 10) },\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tborder: { color: daisyColor('--color-base-content', 10) }\n\t\t\t\t\t},\n\t\t\t\t\ty2: {\n\t\t\t\t\t\ttype: 'linear',\n\t\t\t\t\t\tposition: 'right',\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tmax: 1,\n\t\t\t\t\t\tdisplay: false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tsetInterval(() => {\n\t\t\tupdateData(), 5000;\n\t\t});\n\t});\n\n\tfunction updateData() {\n\t\theapChart.data.labels = $batteryHistory.timestamp;\n\t\theapChart.data.datasets[0].data = $batteryHistory.soc;\n\t\theapChart.data.datasets[1].data = $batteryHistory.charging;\n\t\theapChart.update('none');\n\t}\n\n\tfunction convertSeconds(seconds: number) {\n\t\t// Calculate the number of seconds, minutes, hours, and days\n\t\tlet minutes = Math.floor(seconds / 60);\n\t\tlet hours = Math.floor(minutes / 60);\n\t\tlet days = Math.floor(hours / 24);\n\n\t\t// Calculate the remaining hours, minutes, and seconds\n\t\thours = hours % 24;\n\t\tminutes = minutes % 60;\n\t\tseconds = seconds % 60;\n\n\t\t// Create the formatted string\n\t\tlet result = '';\n\t\tif (days > 0) {\n\t\t\tresult += days + ' day' + (days > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (hours > 0) {\n\t\t\tresult += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (minutes > 0) {\n\t\t\tresult += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tresult += seconds + ' second' + (seconds > 1 ? 's' : '');\n\n\t\treturn result;\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Battery class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Battery History</span>\n\t{/snippet}\n\n\t<div class=\"w-full overflow-x-auto\">\n\t\t<div\n\t\t\tclass=\"flex w-full flex-col space-y-1 h-60\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t<canvas bind:this={heapChartElement}></canvas>\n\t\t</div>\n\t</div>\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/system/metrics/SystemMetrics.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount } from 'svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport { Chart, registerables } from 'chart.js';\n\timport Metrics from '~icons/tabler/report-analytics';\n\timport { daisyColor } from '$lib/DaisyUiHelper';\n\timport { analytics } from '$lib/stores/analytics';\n\n\tChart.register(...registerables);\n\n\tlet heapChartElement: HTMLCanvasElement | undefined = $state();\n\tlet heapChart: Chart;\n\n\tlet psramChartElement: HTMLCanvasElement | undefined = $state();\n\tlet psramChart: Chart;\n\n\tlet filesystemChartElement: HTMLCanvasElement | undefined = $state();\n\tlet filesystemChart: Chart;\n\n\tlet temperatureChartElement: HTMLCanvasElement | undefined = $state();\n\tlet temperatureChart: Chart;\n\n\t// Check if PSRAM data is available\n\tlet hasPsramData = $derived(Math.max(...$analytics.psram_size) > 0);\n\n\tonMount(() => {\n\t\theapChart = new Chart(heapChartElement, {\n\t\t\ttype: 'line',\n\t\t\tdata: {\n\t\t\t\tlabels: $analytics.uptime,\n\t\t\t\tdatasets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Used',\n\t\t\t\t\t\tborderColor: daisyColor('--color-primary'),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-primary', 50),\n\t\t\t\t\t\tborderWidth: 2,\n\t\t\t\t\t\tdata: $analytics.used_heap,\n\t\t\t\t\t\tyAxisID: 'y'\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Max Alloc',\n\t\t\t\t\t\tborderColor: daisyColor('--color-secondary'),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-secondary', 50),\n\t\t\t\t\t\tborderWidth: 2,\n\t\t\t\t\t\tdata: $analytics.max_alloc_heap,\n\t\t\t\t\t\tyAxisID: 'y'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\toptions: {\n\t\t\t\tmaintainAspectRatio: false,\n\t\t\t\tresponsive: true,\n\t\t\t\tplugins: {\n\t\t\t\t\tlegend: {\n\t\t\t\t\t\tdisplay: true\n\t\t\t\t\t},\n\t\t\t\t\ttooltip: {\n\t\t\t\t\t\tmode: 'index',\n\t\t\t\t\t\tintersect: false\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\telements: {\n\t\t\t\t\tpoint: {\n\t\t\t\t\t\tradius: 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tscales: {\n\t\t\t\t\tx: {\n\t\t\t\t\t\tgrid: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content', 10)\n\t\t\t\t\t\t},\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdisplay: false\n\t\t\t\t\t},\n\t\t\t\t\ty: {\n\t\t\t\t\t\ttype: 'linear',\n\t\t\t\t\t\ttitle: {\n\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\ttext: 'Memory [KB]',\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content'),\n\t\t\t\t\t\t\tfont: {\n\t\t\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t\t\t\tweight: 'bold'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tposition: 'left',\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tmax: Math.round(Math.max(...$analytics.total_heap)),\n\t\t\t\t\t\tgrid: { color: daisyColor('--color-base-content', 10) },\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tborder: { color: daisyColor('--color-base-content', 10) }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\t\n\t\t// Only create PSRAM chart if PSRAM data is available\n\t\tif (hasPsramData) {\n\t\t\tpsramChart = new Chart(psramChartElement, {\n\t\t\ttype: 'line',\n\t\t\tdata: {\n\t\t\t\tlabels: $analytics.uptime,\n\t\t\t\tdatasets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Used',\n\t\t\t\t\t\tborderColor: daisyColor('--color-primary'),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-primary', 50),\n\t\t\t\t\t\tborderWidth: 2,\n\t\t\t\t\t\tdata: $analytics.used_psram,\n\t\t\t\t\t\tyAxisID: 'y'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\toptions: {\n\t\t\t\tmaintainAspectRatio: false,\n\t\t\t\tresponsive: true,\n\t\t\t\tplugins: {\n\t\t\t\t\tlegend: {\n\t\t\t\t\t\tdisplay: true\n\t\t\t\t\t},\n\t\t\t\t\ttooltip: {\n\t\t\t\t\t\tmode: 'index',\n\t\t\t\t\t\tintersect: false\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\telements: {\n\t\t\t\t\tpoint: {\n\t\t\t\t\t\tradius: 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tscales: {\n\t\t\t\t\tx: {\n\t\t\t\t\t\tgrid: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content', 10)\n\t\t\t\t\t\t},\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdisplay: false\n\t\t\t\t\t},\n\t\t\t\t\ty: {\n\t\t\t\t\t\ttype: 'linear',\n\t\t\t\t\t\ttitle: {\n\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\ttext: 'PSRAM [KB]',\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content'),\n\t\t\t\t\t\t\tfont: {\n\t\t\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t\t\t\tweight: 'bold'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tposition: 'left',\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tmax: Math.round(Math.max(...$analytics.psram_size)),\n\t\t\t\t\t\tgrid: { color: daisyColor('--color-base-content', 10) },\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tborder: { color: daisyColor('--color-base-content', 10) }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\t}\n\t\t\n\t\tfilesystemChart = new Chart(filesystemChartElement, {\n\t\t\ttype: 'line',\n\t\t\tdata: {\n\t\t\t\tlabels: $analytics.uptime,\n\t\t\t\tdatasets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Used',\n\t\t\t\t\t\tborderColor: daisyColor('--color-primary'),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-primary', 50),\n\t\t\t\t\t\tborderWidth: 2,\n\t\t\t\t\t\tdata: $analytics.fs_used,\n\t\t\t\t\t\tyAxisID: 'y'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\toptions: {\n\t\t\t\tmaintainAspectRatio: false,\n\t\t\t\tresponsive: true,\n\t\t\t\tplugins: {\n\t\t\t\t\tlegend: {\n\t\t\t\t\t\tdisplay: true\n\t\t\t\t\t},\n\t\t\t\t\ttooltip: {\n\t\t\t\t\t\tmode: 'index',\n\t\t\t\t\t\tintersect: false\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\telements: {\n\t\t\t\t\tpoint: {\n\t\t\t\t\t\tradius: 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tscales: {\n\t\t\t\t\tx: {\n\t\t\t\t\t\tgrid: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content', 10)\n\t\t\t\t\t\t},\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdisplay: false\n\t\t\t\t\t},\n\t\t\t\t\ty: {\n\t\t\t\t\t\ttype: 'linear',\n\t\t\t\t\t\ttitle: {\n\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\ttext: 'File System [KB]',\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content'),\n\t\t\t\t\t\t\tfont: {\n\t\t\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t\t\t\tweight: 'bold'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tposition: 'left',\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tmax: Math.round(Math.max(...$analytics.fs_total)),\n\t\t\t\t\t\tgrid: { color: daisyColor('--color-base-content', 10) },\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tborder: { color: daisyColor('--color-base-content', 10) }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\ttemperatureChart = new Chart(temperatureChartElement, {\n\t\t\ttype: 'line',\n\t\t\tdata: {\n\t\t\t\tlabels: $analytics.uptime,\n\t\t\t\tdatasets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Core Temperature',\n\t\t\t\t\t\tborderColor: daisyColor('--color-primary'),\n\t\t\t\t\t\tbackgroundColor: daisyColor('--color-primary', 50),\n\t\t\t\t\t\tborderWidth: 2,\n\t\t\t\t\t\tdata: $analytics.core_temp,\n\t\t\t\t\t\tyAxisID: 'y'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\toptions: {\n\t\t\t\tmaintainAspectRatio: false,\n\t\t\t\tresponsive: true,\n\t\t\t\tplugins: {\n\t\t\t\t\tlegend: {\n\t\t\t\t\t\tdisplay: true\n\t\t\t\t\t},\n\t\t\t\t\ttooltip: {\n\t\t\t\t\t\tmode: 'index',\n\t\t\t\t\t\tintersect: false\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\telements: {\n\t\t\t\t\tpoint: {\n\t\t\t\t\t\tradius: 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tscales: {\n\t\t\t\t\tx: {\n\t\t\t\t\t\tgrid: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content', 10)\n\t\t\t\t\t\t},\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdisplay: false\n\t\t\t\t\t},\n\t\t\t\t\ty: {\n\t\t\t\t\t\ttype: 'linear',\n\t\t\t\t\t\ttitle: {\n\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\ttext: 'Core Temperature [°C]',\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content'),\n\t\t\t\t\t\t\tfont: {\n\t\t\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t\t\t\tweight: 'bold'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tposition: 'left',\n\t\t\t\t\t\tsuggestedMin: 20,\n\t\t\t\t\t\tsuggestedMax: 100,\n\t\t\t\t\t\tgrid: { color: daisyColor('--color-base-content', 10) },\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcolor: daisyColor('--color-base-content')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tborder: { color: daisyColor('--color-base-content', 10) }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tsetInterval(() => {\n\t\t\tupdateData(), 2000;\n\t\t});\n\t});\n\n\tfunction updateData() {\n\t\theapChart.data.labels = $analytics.uptime;\n\t\theapChart.data.datasets[0].data = $analytics.used_heap;\n\t\theapChart.data.datasets[1].data = $analytics.max_alloc_heap;\n\t\theapChart.update('none');\n\t\theapChart.options.scales.y.max = Math.round(Math.max(...$analytics.total_heap));\n\n\t\tif (hasPsramData) {\n\t\t\tpsramChart.data.labels = $analytics.uptime;\n\t\t\tpsramChart.data.datasets[0].data = $analytics.used_psram;\n\t\t\tpsramChart.update('none');\n\t\t\tpsramChart.options.scales.y.max = Math.round(Math.max(...$analytics.psram_size));\n\t\t}\n\n\t\tfilesystemChart.data.labels = $analytics.uptime;\n\t\tfilesystemChart.data.datasets[0].data = $analytics.fs_used;\n\t\tfilesystemChart.update('none');\n\t\tfilesystemChart.options.scales.y.max = Math.round(Math.max(...$analytics.fs_total));\n\n\t\ttemperatureChart.data.labels = $analytics.uptime;\n\t\ttemperatureChart.data.datasets[0].data = $analytics.core_temp;\n\t\ttemperatureChart.update('none');\n\t}\n\n\tfunction convertSeconds(seconds: number) {\n\t\t// Calculate the number of seconds, minutes, hours, and days\n\t\tlet minutes = Math.floor(seconds / 60);\n\t\tlet hours = Math.floor(minutes / 60);\n\t\tlet days = Math.floor(hours / 24);\n\n\t\t// Calculate the remaining hours, minutes, and seconds\n\t\thours = hours % 24;\n\t\tminutes = minutes % 60;\n\t\tseconds = seconds % 60;\n\n\t\t// Create the formatted string\n\t\tlet result = '';\n\t\tif (days > 0) {\n\t\t\tresult += days + ' day' + (days > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (hours > 0) {\n\t\t\tresult += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (minutes > 0) {\n\t\t\tresult += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tresult += seconds + ' second' + (seconds > 1 ? 's' : '');\n\n\t\treturn result;\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Metrics class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>System Metrics</span>\n\t{/snippet}\n\n\t<div class=\"w-full overflow-x-auto\">\n\t\t<div\n\t\t\tclass=\"flex w-full flex-col space-y-1 h-60\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t<canvas bind:this={heapChartElement}></canvas>\n\t\t</div>\n\t</div>\n\t{#if hasPsramData}\n\t\t<div class=\"w-full overflow-x-auto\">\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1 h-60\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<canvas bind:this={psramChartElement}></canvas>\n\t\t\t</div>\n\t\t</div>\n\t{/if}\n\t<div class=\"w-full overflow-x-auto\">\n\t\t<div\n\t\t\tclass=\"flex w-full flex-col space-y-1 h-52\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t<canvas bind:this={filesystemChartElement}></canvas>\n\t\t</div>\n\t</div>\n\t<div class=\"w-full overflow-x-auto\">\n\t\t<div\n\t\t\tclass=\"flex w-full flex-col space-y-1 h-52\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t<canvas bind:this={temperatureChartElement}></canvas>\n\t\t</div>\n\t</div>\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/system/status/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport SystemStatus from './SystemStatus.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<SystemStatus />\n</div>\n"
  },
  {
    "path": "interface/src/routes/system/status/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn { title: 'System Status' };\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/system/status/SystemStatus.svelte",
    "content": "<script lang=\"ts\">\n\timport { onDestroy, onMount } from 'svelte';\n\timport { modals } from 'svelte-modals';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport CPU from '~icons/tabler/cpu';\n\timport CPP from '~icons/tabler/binary';\n\timport Power from '~icons/tabler/reload';\n\timport Sleep from '~icons/tabler/zzz';\n\timport FactoryReset from '~icons/tabler/refresh-dot';\n\timport Speed from '~icons/tabler/activity';\n\timport Flash from '~icons/tabler/device-sd-card';\n\timport Pyramid from '~icons/tabler/pyramid';\n\timport Sketch from '~icons/tabler/chart-pie';\n\timport Folder from '~icons/tabler/folder';\n\timport Heap from '~icons/tabler/box-model';\n\timport Cancel from '~icons/tabler/x';\n\timport Temperature from '~icons/tabler/temperature';\n\timport Health from '~icons/tabler/stethoscope';\n\timport Stopwatch from '~icons/tabler/24-hours';\n\timport SDK from '~icons/tabler/sdk';\n\timport type { SystemInformation, Analytics } from '$lib/types/models';\n\timport { socket } from '$lib/stores/socket';\n\n\tlet systemInformation: SystemInformation = $state();\n\n\tasync function getSystemStatus() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/systemStatus', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tsystemInformation = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.log('Error:', error);\n\t\t}\n\t\treturn systemInformation;\n\t}\n\n\tonMount(() => socket.on('analytics', handleSystemData));\n\n\tonDestroy(() => socket.off('analytics', handleSystemData));\n\n\tconst handleSystemData = (data: Analytics) =>\n\t\t(systemInformation = { ...systemInformation, ...data });\n\n\tasync function postRestart() {\n\t\tconst response = await fetch('/rest/restart', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction confirmRestart() {\n\t\tmodals.open(ConfirmDialog, {\n\t\t\ttitle: 'Confirm Restart',\n\t\t\tmessage: 'Are you sure you want to restart the device?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Restart', icon: Power }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tmodals.close();\n\t\t\t\tpostRestart();\n\t\t\t}\n\t\t});\n\t}\n\n\tasync function postFactoryReset() {\n\t\tconst response = await fetch('/rest/factoryReset', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction confirmReset() {\n\t\tmodals.open(ConfirmDialog, {\n\t\t\ttitle: 'Confirm Factory Reset',\n\t\t\tmessage: 'Are you sure you want to reset the device to its factory defaults?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Factory Reset', icon: FactoryReset }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tmodals.close();\n\t\t\t\tpostFactoryReset();\n\t\t\t}\n\t\t});\n\t}\n\n\tasync function postSleep() {\n\t\tconst response = await fetch('/rest/sleep', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction confirmSleep() {\n\t\tmodals.open(ConfirmDialog, {\n\t\t\ttitle: 'Confirm Going to Sleep',\n\t\t\tmessage: 'Are you sure you want to put the device into sleep?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Sleep', icon: Sleep }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tmodals.close();\n\t\t\t\tpostSleep();\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction convertSeconds(seconds: number) {\n\t\t// Calculate the number of seconds, minutes, hours, and days\n\t\tlet minutes = Math.floor(seconds / 60);\n\t\tlet hours = Math.floor(minutes / 60);\n\t\tlet days = Math.floor(hours / 24);\n\n\t\t// Calculate the remaining hours, minutes, and seconds\n\t\thours = hours % 24;\n\t\tminutes = minutes % 60;\n\t\tseconds = seconds % 60;\n\n\t\t// Create the formatted string\n\t\tlet result = '';\n\t\tif (days > 0) {\n\t\t\tresult += days + ' day' + (days > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (hours > 0) {\n\t\t\tresult += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tif (minutes > 0) {\n\t\t\tresult += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';\n\t\t}\n\t\tresult += seconds + ' second' + (seconds > 1 ? 's' : '');\n\n\t\treturn result;\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Health class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>System Status</span>\n\t{/snippet}\n\n\t<div class=\"w-full overflow-x-auto\">\n\t\t{#await getSystemStatus()}\n\t\t\t<Spinner />\n\t\t{:then nothing}\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Stopwatch class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Uptime</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{convertSeconds(systemInformation.uptime)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Heap class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Memory</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{(\n\t\t\t\t\t\t\t\t((systemInformation.total_heap - systemInformation.free_heap) /\n\t\t\t\t\t\t\t\t\tsystemInformation.total_heap) *\n\t\t\t\t\t\t\t\t100\n\t\t\t\t\t\t\t).toFixed(1)} % of {Math.round(systemInformation.total_heap / 1000).toLocaleString(\n\t\t\t\t\t\t\t\t'en-US'\n\t\t\t\t\t\t\t)} KB\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t>({Math.round(systemInformation.free_heap / 1000).toLocaleString('en-US')} KB free, Max\n\t\t\t\t\t\t\t\talloc {Math.round(systemInformation.max_alloc_heap / 1000).toLocaleString('en-US')} KB)</span\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{#if systemInformation.psram_size}\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t\t<Pyramid class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">PSRAM</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{((systemInformation.used_psram / systemInformation.psram_size) * 100).toFixed(1)} %\n\t\t\t\t\t\t\t\tof {Math.round(systemInformation.psram_size / 1000).toLocaleString('en-US')} KB\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t>({Math.round(systemInformation.free_psram / 1000).toLocaleString('en-US')} KB free)</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Folder class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">File System</div>\n\t\t\t\t\t\t<div class=\"flex flex-wrap justify-start gap-1 text-sm opacity-75\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t>{((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(1)} % of {Math.round(\n\t\t\t\t\t\t\t\t\tsystemInformation.fs_total / 1000\n\t\t\t\t\t\t\t\t).toLocaleString('en-US')} KB</span\n\t\t\t\t\t\t\t>\n\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t>({Math.round(\n\t\t\t\t\t\t\t\t\t(systemInformation.fs_total - systemInformation.fs_used) / 1000\n\t\t\t\t\t\t\t\t).toLocaleString('en-US')}\n\t\t\t\t\t\t\t\tKB free)</span\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Temperature class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Core Temperature</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{systemInformation.core_temp == 53.33\n\t\t\t\t\t\t\t\t? 'NaN'\n\t\t\t\t\t\t\t\t: systemInformation.core_temp.toFixed(2) + ' °C'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Power class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Reset Reason</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{systemInformation.cpu_reset_reason}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Sketch class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Sketch</div>\n\t\t\t\t\t\t<div class=\"flex flex-wrap justify-start gap-1 text-sm opacity-75\">\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t{(\n\t\t\t\t\t\t\t\t\t(systemInformation.sketch_size / systemInformation.free_sketch_space) *\n\t\t\t\t\t\t\t\t\t100\n\t\t\t\t\t\t\t\t).toFixed(1)} % of\n\t\t\t\t\t\t\t\t{Math.round(systemInformation.free_sketch_space / 1000).toLocaleString('en-US')} KB\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t({Math.round(\n\t\t\t\t\t\t\t\t\t(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000\n\t\t\t\t\t\t\t\t).toLocaleString('en-US')} KB free)\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<CPP class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Firmware Version</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{systemInformation.firmware_version}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<CPU class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Chip</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<SDK class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">SDK Version</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\tESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Speed class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">CPU Frequency</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2\n\t\t\t\t\t\t\t\t? 'Dual Core'\n\t\t\t\t\t\t\t\t: 'Single Core'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 flex-none\">\n\t\t\t\t\t\t<Flash class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Flash Chip</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{Math.round(systemInformation.flash_chip_size / 1000).toLocaleString('en-US')} KB / {(\n\t\t\t\t\t\t\t\tsystemInformation.flash_chip_speed / 1000000\n\t\t\t\t\t\t\t).toLocaleString('en-US')} MHz\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{/await}\n\t</div>\n\n\t<div class=\"mt-4 flex flex-wrap justify-end gap-2\">\n\t\t{#if page.data.features.sleep}\n\t\t\t<button class=\"btn btn-primary inline-flex items-center\" onclick={confirmSleep}\n\t\t\t\t><Sleep class=\"mr-2 h-5 w-5\" /><span>Sleep</span></button\n\t\t\t>\n\t\t{/if}\n\t\t{#if !page.data.features.security || $user.admin}\n\t\t\t<button class=\"btn btn-primary inline-flex items-center\" onclick={confirmRestart}\n\t\t\t\t><Power class=\"mr-2 h-5 w-5\" /><span>Restart</span></button\n\t\t\t>\n\t\t\t<button class=\"btn btn-secondary inline-flex items-center\" onclick={confirmReset}\n\t\t\t\t><FactoryReset class=\"mr-2 h-5 w-5\" /><span>Factory Reset</span></button\n\t\t\t>\n\t\t{/if}\n\t</div>\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/system/update/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport UploadFirmware from './UploadFirmware.svelte';\n\timport GithubFirmwareManager from './GithubFirmwareManager.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t{#if page.data.features.download_firmware && (!page.data.features.security || $user.admin)}\n\t\t<GithubFirmwareManager />\n\t{/if}\n\n\t{#if page.data.features.upload_firmware && (!page.data.features.security || $user.admin)}\n\t\t<UploadFirmware />\n\t{/if}\n</div>\n"
  },
  {
    "path": "interface/src/routes/system/update/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn { title: 'Firmware Update' };\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/system/update/GithubFirmwareManager.svelte",
    "content": "<script lang=\"ts\">\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { modals } from 'svelte-modals';\n\timport type { ModalComponent } from 'svelte-modals';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport Github from '~icons/tabler/brand-github';\n\timport CloudDown from '~icons/tabler/cloud-download';\n\timport Cancel from '~icons/tabler/x';\n\timport Prerelease from '~icons/tabler/test-pipe';\n\timport Error from '~icons/tabler/circle-x';\n\timport { compareVersions } from 'compare-versions';\n\timport FirmwareUpdateDialog from '$lib/components/FirmwareUpdateDialog.svelte';\n\timport { assets } from '$app/paths';\n\timport InfoDialog from '$lib/components/InfoDialog.svelte';\n\timport Check from '~icons/tabler/check';\n\timport { telemetry } from '$lib/stores/telemetry';\n\n\tasync function getGithubAPI() {\n\t\ttry {\n\t\t\tconst githubResponse = await fetch(\n\t\t\t\t'https://api.github.com/repos/' + page.data.github + '/releases',\n\t\t\t\t{\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\taccept: 'application/vnd.github+json',\n\t\t\t\t\t\t'X-GitHub-Api-Version': '2022-11-28'\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t);\n\t\t\tconst results = await githubResponse.json();\n\t\t\treturn results;\n\t\t} catch (error) {\n\t\t\tconsole.warn(error);\n\t\t}\n\t\treturn;\n\t}\n\n\tasync function postGithubDownload(url: string) {\n\t\ttry {\n\t\t\tconst apiResponse = await fetch('/rest/downloadUpdate', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ download_url: url })\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tfunction confirmGithubUpdate(assets: any) {\n\t\tlet url = '';\n\t\t// iterate over assets and find the correct one\n\t\tfor (let i = 0; i < assets.length; i++) {\n\t\t\t// check if the asset is of type *.bin\n\t\t\tif (\n\t\t\t\tassets[i].name.includes('.bin') &&\n\t\t\t\tassets[i].name.includes(page.data.features.firmware_built_target)\n\t\t\t) {\n\t\t\t\turl = assets[i].browser_download_url;\n\t\t\t}\n\t\t}\n\t\tif (url === '') {\n\t\t\tmodals.open(InfoDialog as unknown as ModalComponent<any>, {\n\t\t\t\ttitle: 'No matching firmware found',\n\t\t\t\tmessage:\n\t\t\t\t\t'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',\n\t\t\t\tdismiss: { label: 'OK', icon: Check },\n\t\t\t\tonDismiss: () => modals.close()\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tmodals.open(ConfirmDialog as unknown as ModalComponent<any>, {\n\t\t\ttitle: 'Confirm flashing new firmware to the device',\n\t\t\tmessage: 'Are you sure you want to overwrite the existing firmware with a new one?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Update', icon: CloudDown }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\t// Reset OTA status before starting new download\n\t\t\t\ttelemetry.setOTAStatus({ status: 'none', progress: 0, error: '' });\n\t\t\t\tpostGithubDownload(url);\n\t\t\t\tmodals.open(FirmwareUpdateDialog as unknown as ModalComponent<any>, {\n\t\t\t\t\ttitle: 'Downloading Firmware'\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Github class=\"lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Github Firmware Manager</span>\n\t{/snippet}\n\t{#await getGithubAPI()}\n\t\t<Spinner />\n\t{:then githubReleases}\n\t\t<div class=\"alert alert-info\">\n\t\t\t<div>\n\t\t\t\t<span class=\"font-bold\">Current Firmware Version:</span>\n\t\t\t\tv{page.data.features.firmware_version}\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"relative w-full overflow-visible\">\n\t\t\t<div class=\"overflow-x-auto\" transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t<table class=\"table w-full table-auto\">\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr class=\"font-bold\">\n\t\t\t\t\t\t\t<th align=\"left\">Release</th>\n\t\t\t\t\t\t\t<th align=\"center\" class=\"hidden sm:block\">Release Date</th>\n\t\t\t\t\t\t\t<th align=\"center\">Exp.</th>\n\t\t\t\t\t\t\t<th align=\"center\">Install</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t{#each githubReleases as release}\n\t\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\t\tclass={compareVersions(page.data.features.firmware_version, release.tag_name) === 0\n\t\t\t\t\t\t\t\t\t? 'bg-primary text-primary-content'\n\t\t\t\t\t\t\t\t\t: 'bg-base-100 h-14'}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<td align=\"left\" class=\"text-base font-semibold\">\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\thref={release.html_url}\n\t\t\t\t\t\t\t\t\t\tclass=\"link link-hover\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\">{release.name}</a\n\t\t\t\t\t\t\t\t\t></td\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<td align=\"center\" class=\"hidden min-h-full align-middle sm:block\">\n\t\t\t\t\t\t\t\t\t<div class=\"my-2\">\n\t\t\t\t\t\t\t\t\t\t{new Intl.DateTimeFormat('en-GB', {\n\t\t\t\t\t\t\t\t\t\t\tdateStyle: 'medium'\n\t\t\t\t\t\t\t\t\t\t}).format(new Date(release.published_at))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td align=\"center\">\n\t\t\t\t\t\t\t\t\t{#if release.prerelease}\n\t\t\t\t\t\t\t\t\t\t<Prerelease class=\"text-accent h-5 w-5\" />\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td align=\"center\">\n\t\t\t\t\t\t\t\t\t{#if compareVersions(page.data.features.firmware_version, release.tag_name) != 0}\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-ghost btn-circle btn-sm\"\n\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tconfirmGithubUpdate(release.assets);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<CloudDown class=\"text-secondary h-6 w-6\" />\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</div>\n\t{:catch error}\n\t\t<div class=\"alert alert-error shadow-lg\">\n\t\t\t<Error class=\"h-6 w-6 shrink-0\" />\n\t\t\t<span>Please connect to a network with internet access to perform a firmware update.</span>\n\t\t</div>\n\t{/await}\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/system/update/UploadFirmware.svelte",
    "content": "<script lang=\"ts\">\n\timport { modals } from 'svelte-modals';\n\timport type { ModalComponent } from 'svelte-modals';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport FirmwareUpdateDialog from '$lib/components/FirmwareUpdateDialog.svelte';\n\timport { telemetry } from '$lib/stores/telemetry';\n\timport OTA from '~icons/tabler/file-upload';\n\timport Warning from '~icons/tabler/alert-triangle';\n\timport Cancel from '~icons/tabler/x';\n\timport Check from '~icons/tabler/check';\n\timport AlertCircle from '~icons/tabler/alert-circle';\n\n\tlet files: FileList | undefined = $state();\n\tlet md5Hash: string = $state('');\n\tlet md5StatusMessage: string = $state('');\n\tlet md5StatusType: 'success' | 'error' | null = $state(null);\n\tlet fileInput: HTMLInputElement = $state()!;\n\tlet fileValidationError: string = $state('');\n\n\t// Clear file input when BIN upload finishes (success or error)\n\t$effect(() => {\n\t\tconst status = $telemetry.ota_status.status;\n\t\t\n\t\tif ((status === 'finished' || status === 'error') && fileInput) {\n\t\t\tfileInput.value = '';\n\t\t\tfiles = undefined;\n\t\t}\n\t\t\n\t\t// Clear MD5 status when firmware update succeeds\n\t\tif (status === 'finished') {\n\t\t\tmd5StatusMessage = '';\n\t\t\tmd5StatusType = null;\n\t\t\tmd5Hash = '';\n\t\t}\n\t});\n\n\tasync function uploadMD5() {\n\t\tif (!files || files.length === 0) return;\n\n\t\ttry {\n\t\t\tconst formData = new FormData();\n\t\t\tformData.append('file', files[0]);\n\t\t\tconst response = await fetch('/rest/uploadFirmware', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'\n\t\t\t\t},\n\t\t\t\tbody: formData\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// Handle error responses\n\t\t\t\tlet errorMsg = 'Upload failed.';\n\t\t\t\tswitch (response.status) {\n\t\t\t\t\tcase 403:\n\t\t\t\t\t\terrorMsg = 'Unauthorized - Admin access required';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 406:\n\t\t\t\t\t\terrorMsg = 'Unsupported file type';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 422:\n\t\t\t\t\t\terrorMsg = 'Invalid MD5 file (must be exactly 32 bytes)';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 500:\n\t\t\t\t\t\terrorMsg = 'Internal server error during upload';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 503:\n\t\t\t\t\t\terrorMsg = 'Wrong firmware for this device';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 507:\n\t\t\t\t\t\terrorMsg = 'Insufficient storage space';\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\terrorMsg = `MD5 upload failed with error code ${response.status}`;\n\t\t\t\t}\n\t\t\t\tmd5StatusMessage = errorMsg;\n\t\t\t\tmd5StatusType = 'error';\n\n\t\t\t\t// Clear file input on error\n\t\t\t\tif (fileInput) {\n\t\t\t\t\tfileInput.value = '';\n\t\t\t\t\tfiles = undefined;\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst result = await response.json();\n\n\t\t\t// Check if MD5 was uploaded and update state\n\t\t\tif (result.md5) {\n\t\t\t\tmd5Hash = result.md5;\n\t\t\t\tmd5StatusMessage = 'MD5 hash uploaded successfully.';\n\t\t\t\tmd5StatusType = 'success';\n\n\t\t\t\t// Clear file input for next upload\n\t\t\t\tif (fileInput) {\n\t\t\t\t\tfileInput.value = '';\n\t\t\t\t\tfiles = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t\tmd5StatusMessage = 'Network error during upload';\n\t\t\tmd5StatusType = 'error';\n\n\t\t\t// Clear file input on network error\n\t\t\tif (fileInput) {\n\t\t\t\tfileInput.value = '';\n\t\t\t\tfiles = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function uploadBIN() {\n\t\tif (!files || files.length === 0) return;\n\n\t\ttry {\n\t\t\t// Keep MD5 status visible during BIN upload\n\t\t\tconst formData = new FormData();\n\t\t\tformData.append('file', files[0]);\n\t\t\tconst response = await fetch('/rest/uploadFirmware', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'\n\t\t\t\t},\n\t\t\t\tbody: formData\n\t\t\t});\n\t\t\t\n\t\t\tif (!response.ok) {\n\t\t\t\t// All upload errors (including file size) are handled via WebSocket EVENT_OTA_UPDATE\n\t\t\t\t// The backend's handleError() method emits detailed error information\n\t\t\t\t// which updates telemetry and displays in FirmwareUpdateDialog\n\t\t\t\tconsole.error(`Firmware upload failed with HTTP ${response.status}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Success/progress comes via WebSocket events to telemetry\n\t\t} catch (error) {\n\t\t\t// Only set network error if no error status already exists from WebSocket\n\t\t\t// (Vite dev proxy may send RST after HTTP 503, causing catch after successful error handling)\n\t\t\tif ($telemetry.ota_status.status !== 'error') {\n\t\t\t\tconsole.error('Error:', error);\n\t\t\t\ttelemetry.setOTAStatus({ \n\t\t\t\t\tstatus: 'error', \n\t\t\t\t\tprogress: 0, \n\t\t\t\t\terror: 'Network error during firmware upload'\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tfunction handleFileChange() {\n\t\tif (!files || files.length === 0) return;\n\n\t\t// Clear file validation error when selecting a new file\n\t\tfileValidationError = '';\n\t\t\n\t\t// Clear MD5 error status, but keep success status\n\t\tif (md5StatusType === 'error') {\n\t\t\tmd5StatusMessage = '';\n\t\t\tmd5StatusType = null;\n\t\t}\n\n\t\tconst fileName = files[0].name;\n\t\tconst fileSize = files[0].size;\n\t\tconst fileExtension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();\n\n\t\tif (fileExtension === '.md5') {\n\t\t\t// Upload MD5 file directly without confirmation\n\t\t\tuploadMD5();\n\t\t} else if (fileExtension === '.bin') {\n\t\t\t// Show confirmation dialog for BIN files\n\t\t\t// File size validation is handled by backend\n\t\t\tconfirmBinUpload();\n\t\t} else {\n\t\t\t// Invalid file type\n\t\t\tfileValidationError = `Invalid file type \"${fileExtension}\". Please upload a .bin or .md5 file.`;\n\n\t\t\t// Clear the invalid file selection\n\t\t\tif (fileInput) {\n\t\t\t\tfileInput.value = '';\n\t\t\t\tfiles = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction confirmBinUpload() {\n\t\tmodals.open(ConfirmDialog as unknown as ModalComponent<any>, {\n\t\t\ttitle: 'Confirm Flashing the Device',\n\t\t\tmessage: 'Are you sure you want to overwrite the existing firmware with a new one?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Upload', icon: OTA }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tmodals.close();\n\t\t\t\t// Reset OTA status before starting new upload\n\t\t\t\ttelemetry.setOTAStatus({ status: 'none', progress: 0, error: '' });\n\t\t\t\tuploadBIN();\n\t\t\t\tmodals.open(FirmwareUpdateDialog as unknown as ModalComponent<any>, {\n\t\t\t\t\ttitle: 'Uploading Firmware'\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<OTA class=\"flex-shrink-0 mr-2 h-6 w-6 self-end rounded-full\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Upload Firmware</span>\n\t{/snippet}\n\t<div class=\"alert alert-warning shadow-lg\">\n\t\t<Warning class=\"h-6 w-6 shrink-0\" />\n\t\t<span\n\t\t\t>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload a\n\t\t\t(.md5) file first to verify the uploaded firmware.</span\n\t\t>\n\t</div>\n\n\t{#if md5StatusType}\n\t\t<div\n\t\t\tclass=\"alert {md5StatusType === 'success' ? 'alert-success' : 'alert-error'} shadow-lg mt-4\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t{#if md5StatusType === 'success'}\n\t\t\t\t<Check class=\"h-6 w-6 shrink-0\" />\n\t\t\t{:else}\n\t\t\t\t<AlertCircle class=\"h-6 w-6 shrink-0\" />\n\t\t\t{/if}\n\t\t\t<div class=\"flex flex-col\">\n\t\t\t\t<span class=\"font-semibold\">{md5StatusMessage}</span>\n\t\t\t\t{#if md5StatusType === 'success'}\n\t\t\t\t\t<span>Upload a .bin file to flash the firmware</span>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</div>\n\t{/if}\n\n\t{#if fileValidationError}\n\t\t<div\n\t\t\tclass=\"alert alert-error shadow-lg mt-4\"\n\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t>\n\t\t\t<AlertCircle class=\"h-6 w-6 shrink-0\" />\n\t\t\t<span class=\"font-semibold\">{fileValidationError}</span>\n\t\t</div>\n\t{/if}\n\n\t<input\n\t\ttype=\"file\"\n\t\tid=\"binFile\"\n\t\tbind:this={fileInput}\n\t\tclass=\"file-input file-input-secondary mt-4 w-full\"\n\t\tbind:files\n\t\taccept=\".bin,.md5\"\n\t\tonchange={handleFileChange}\n\t/>\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/user/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport { onMount } from 'svelte';\n\timport { goto } from '$app/navigation';\n\timport { modals } from 'svelte-modals';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport { user } from '$lib/stores/user';\n\timport type { userProfile } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport InputPassword from '$lib/components/InputPassword.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport EditUser from './EditUser.svelte';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport Delete from '~icons/tabler/trash';\n\timport AddUser from '~icons/tabler/user-plus';\n\timport Edit from '~icons/tabler/pencil';\n\timport Admin from '~icons/tabler/key';\n\timport Users from '~icons/tabler/users';\n\timport Warning from '~icons/tabler/alert-triangle';\n\timport Cancel from '~icons/tabler/x';\n\timport Check from '~icons/tabler/check';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n\n\ttype userSetting = {\n\t\tusername: string;\n\t\tpassword: string;\n\t\tadmin: boolean;\n\t};\n\n\ttype SecuritySettings = {\n\t\tjwt_secret: string;\n\t\tusers: userSetting[];\n\t};\n\n\tlet securitySettings: SecuritySettings = $state();\n\n\tasync function getSecuritySettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/securitySettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tsecuritySettings = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tasync function postSecuritySettings(data: SecuritySettings) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/securitySettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(data)\n\t\t\t});\n\n\t\t\tsecuritySettings = await response.json();\n\t\t\tif (response.status == 200) {\n\t\t\t\tif (await validateUser($user)) {\n\t\t\t\t\tnotifications.success('Security settings updated.', 3000);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnotifications.error('User not authorized.', 3000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn;\n\t}\n\n\tasync function validateUser(userdata: userProfile) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/verifyAuthorization', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: 'Bearer ' + userdata.bearer_token,\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tif (response.status !== 200) {\n\t\t\t\tuser.invalidate();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn true;\n\t}\n\n\tfunction confirmDelete(index: number) {\n\t\tmodals.open(ConfirmDialog, {\n\t\t\ttitle: 'Confirm Delete User',\n\t\t\tmessage:\n\t\t\t\t'Are you sure you want to delete the user \"' +\n\t\t\t\tsecuritySettings.users[index].username +\n\t\t\t\t'\"?',\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Abort', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Yes', icon: Check }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\tsecuritySettings.users.splice(index, 1);\n\t\t\t\tsecuritySettings = securitySettings;\n\t\t\t\tmodals.close();\n\t\t\t\tpostSecuritySettings(securitySettings);\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction handleEdit(index: number) {\n\t\tmodals.open(EditUser, {\n\t\t\ttitle: 'Edit User',\n\t\t\tuser: { ...securitySettings.users[index] }, // Shallow Copy\n\t\t\tonSaveUser: (editedUser: userSetting) => {\n\t\t\t\tsecuritySettings.users[index] = editedUser;\n\t\t\t\tmodals.close();\n\t\t\t\tpostSecuritySettings(securitySettings);\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction handleNewUser() {\n\t\tmodals.open(EditUser, {\n\t\t\ttitle: 'Add User',\n\t\t\tonSaveUser: (newUser: userSetting) => {\n\t\t\t\tsecuritySettings.users = [...securitySettings.users, newUser];\n\t\t\t\tmodals.close();\n\t\t\t\tpostSecuritySettings(securitySettings);\n\t\t\t}\n\t\t});\n\t\t//\n\t}\n</script>\n\n{#if $user.admin}\n\t<div\n\t\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n\t>\n\t\t<SettingsCard collapsible={false}>\n\t\t\t{#snippet icon()}\n\t\t\t\t<Users class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t\t\t{/snippet}\n\t\t\t{#snippet title()}\n\t\t\t\t<span>Manage Users</span>\n\t\t\t{/snippet}\n\t\t\t{#await getSecuritySettings()}\n\t\t\t\t<Spinner />\n\t\t\t{:then nothing}\n\t\t\t\t<div class=\"relative w-full overflow-visible\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary text-primary-content btn-md absolute -top-14 right-0\"\n\t\t\t\t\t\tonclick={handleNewUser}\n\t\t\t\t\t>\n\t\t\t\t\t\t<AddUser class=\"h-6 w-6\" /></button\n\t\t\t\t\t>\n\n\t\t\t\t\t<div class=\"overflow-x-auto\" transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t<table class=\"table w-full table-auto\">\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr class=\"font-bold\">\n\t\t\t\t\t\t\t\t\t<th align=\"left\">Username</th>\n\t\t\t\t\t\t\t\t\t<th align=\"center\">Admin</th>\n\t\t\t\t\t\t\t\t\t<th align=\"right\" class=\"pr-8\">Edit</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t{#each securitySettings.users as user, index}\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td align=\"left\">{user.username}</td>\n\t\t\t\t\t\t\t\t\t\t<td align=\"center\">\n\t\t\t\t\t\t\t\t\t\t\t{#if user.admin}\n\t\t\t\t\t\t\t\t\t\t\t\t<Admin class=\"text-secondary\" />\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t<td align=\"right\">\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"my-auto inline-flex flex-row space-x-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-ghost btn-circle btn-xs\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => handleEdit(index)}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Edit class=\"h-6 w-6\" /></button\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-ghost btn-circle btn-xs\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => confirmDelete(index)}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Delete class=\"text-error h-6 w-6\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"divider mb-0\"></div>\n\n\t\t\t\t<span class=\"pb-2 text-xl font-medium\">Security Settings</span>\n\t\t\t\t<div class=\"alert alert-warning shadow-lg\">\n\t\t\t\t\t<Warning class=\"h-6 w-6 shrink-0\" />\n\t\t\t\t\t<span\n\t\t\t\t\t\t>The JWT secret is used to sign authentication tokens. If you modify the JWT Secret, all\n\t\t\t\t\t\tusers will be signed out.</span\n\t\t\t\t\t>\n\t\t\t\t</div>\n\t\t\t\t<label class=\"label\" for=\"secret\">JWT Secret</label>\n\t\t\t\t<InputPassword bind:value={securitySettings.jwt_secret} id=\"secret\" />\n\t\t\t\t<div class=\"mt-6 flex justify-end\">\n\t\t\t\t\t<button class=\"btn btn-primary\" onclick={() => postSecuritySettings(securitySettings)}\n\t\t\t\t\t\t>Apply Settings</button\n\t\t\t\t\t>\n\t\t\t\t</div>\n\t\t\t{/await}\n\t\t</SettingsCard>\n\t</div>\n{:else}\n\t{goto('/')}\n{/if}\n"
  },
  {
    "path": "interface/src/routes/user/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n    return {\n        title: 'Users'\n    };\n}) satisfies PageLoad;"
  },
  {
    "path": "interface/src/routes/user/EditUser.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount, onDestroy } from 'svelte';\n\timport { modals } from 'svelte-modals';\n\timport { fly } from 'svelte/transition';\n\timport InputPassword from '$lib/components/InputPassword.svelte';\n\timport Cancel from '~icons/tabler/x';\n\timport Save from '~icons/tabler/device-floppy';\n\n\t// provided by <Modals />\n\n\tinterface Props {\n\t\tisOpen: boolean;\n\t\ttitle: string;\n\t\tonSaveUser: any;\n\t\tuser?: any;\n\t}\n\n\tlet {\n\t\tisOpen,\n\t\ttitle,\n\t\tonSaveUser,\n\t\tuser: _user = {\n\t\t\tusername: '',\n\t\t\tpassword: '',\n\t\t\tadmin: false\n\t\t}\n\t}: Props = $props();\n\n\t// Make passed object reactive to prevent Svelte warning 'binding_property_non_reactive'\n\t// https://github.com/sveltejs/svelte/issues/12320\n\tlet user = $state(_user);\n\n\tlet errorUsername = $state(false);\n\n\tlet usernameEditable = $state(false);\n\n\tonMount(() => {\n\t\tif (user.username == '') {\n\t\t\tusernameEditable = true;\n\t\t}\n\t});\n\n\tfunction handleSave() {\n\t\t// Validate if username is within range\n\t\tif (user.username.length < 3 || user.username.length > 32) {\n\t\t\terrorUsername = true;\n\t\t} else {\n\t\t\terrorUsername = false;\n\t\t\t// Callback on saving\n\t\t\tonSaveUser(user);\n\t\t}\n\t}\n\n\tfunction preventDefault(fn) {\n\t\treturn function (event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn.call(this, event);\n\t\t};\n\t}\n</script>\n\n{#if isOpen}\n\t<div\n\t\trole=\"dialog\"\n\t\tclass=\"pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-y-auto\"\n\t\ttransition:fly={{ y: 50 }}\n\t>\n\t\t<div\n\t\t\tclass=\"rounded-box bg-base-100 shadow-secondary/30 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg md:w-md\"\n\t\t>\n\t\t\t<h2 class=\"text-base-content text-start text-2xl font-bold\">{title}</h2>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<form\n\t\t\t\tclass=\"fieldset text-base-content mb-1 w-full\"\n\t\t\t\tonsubmit={preventDefault(handleSave)}\n\t\t\t\tnovalidate\n\t\t\t>\n\t\t\t\t<label class=\"label\" for=\"username\">Username</label>\n\t\t\t\t<input\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\tmin=\"3\"\n\t\t\t\t\tmax=\"32\"\n\t\t\t\t\tclass=\"input invalid:border-error w-full invalid:border-2\"\n\t\t\t\t\tbind:value={user.username}\n\t\t\t\t\tid=\"username\"\n\t\t\t\t\tdisabled={!usernameEditable}\n\t\t\t\t/>\n\t\t\t\t<label for=\"username\" class=\"label\"\n\t\t\t\t\t><span class=\"text-error {errorUsername ? '' : 'hidden'}\"\n\t\t\t\t\t\t>Username must be between 3 and 32 characters long</span\n\t\t\t\t\t></label\n\t\t\t\t>\n\t\t\t\t<label class=\"label\" for=\"pwd\">Password </label>\n\t\t\t\t<InputPassword bind:value={user.password} id=\"pwd\" />\n\t\t\t\t<label class=\"label my-auto cursor-pointer justify-start gap-4 mt-4\">\n\t\t\t\t\t<input type=\"checkbox\" bind:checked={user.admin} class=\"checkbox checkbox-primary\" />\n\t\t\t\t\t<span class=\"\">Is Admin?</span>\n\t\t\t\t</label>\n\t\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t\t<div class=\"flex justify-end gap-2\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-neutral text-neutral-content inline-flex items-center\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tmodals.close();\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Cancel class=\"mr-2 h-5 w-5\" /><span>Cancel</span></button\n\t\t\t\t\t>\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary text-primary-content inline-flex items-center\"\n\t\t\t\t\t\ttype=\"submit\"><Save class=\"mr-2 h-5 w-5\" /><span>Save</span></button\n\t\t\t\t\t>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/routes/wifi/+page.ts",
    "content": "import type { PageLoad } from './$types';\nimport { goto } from '$app/navigation';\n\nexport const load = (async () => {\n\tgoto('/');\n\treturn;\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/wifi/ap/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport Accesspoint from './Accesspoint.svelte';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<Accesspoint />\n</div>\n"
  },
  {
    "path": "interface/src/routes/wifi/ap/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn {\n\t\ttitle: 'Access Point'\n\t};\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/wifi/ap/Accesspoint.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount, onDestroy } from 'svelte';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport InputPassword from '$lib/components/InputPassword.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport Collapsible from '$lib/components/Collapsible.svelte';\n\timport AP from '~icons/tabler/access-point';\n\timport MAC from '~icons/tabler/dna-2';\n\timport Home from '~icons/tabler/home';\n\timport Devices from '~icons/tabler/devices';\n\timport type { ApSettings, ApStatus } from '$lib/types/models';\n\n\tlet apSettings: ApSettings = $state();\n\tlet apStatus: ApStatus = $state();\n\n\tlet formField: any = $state();\n\n\tasync function getAPStatus() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/apStatus', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tapStatus = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn apStatus;\n\t}\n\n\tasync function getAPSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/apSettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\tapSettings = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t\treturn apSettings;\n\t}\n\n\tconst interval = setInterval(async () => {\n\t\tgetAPStatus();\n\t}, 5000);\n\n\tonDestroy(() => clearInterval(interval));\n\n\tonMount(() => {\n\t\tif (!page.data.features.security || $user.admin) {\n\t\t\tgetAPSettings();\n\t\t}\n\t});\n\n\tlet provisionMode = [\n\t\t{\n\t\t\tid: 0,\n\t\t\ttext: `Always`\n\t\t},\n\t\t{\n\t\t\tid: 1,\n\t\t\ttext: `When WiFi Disconnected`\n\t\t},\n\t\t{\n\t\t\tid: 2,\n\t\t\ttext: `Never`\n\t\t}\n\t];\n\n\tlet apStatusDescription = [\n\t\t{ bg_color: 'bg-success', text_color: 'text-success-content', description: 'Active' },\n\t\t{ bg_color: 'bg-error', text_color: 'text-error-content', description: 'Inactive' },\n\t\t{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }\n\t];\n\n\tlet formErrors = $state({\n\t\tssid: false,\n\t\tchannel: false,\n\t\tmax_clients: false,\n\t\tlocal_ip: false,\n\t\tgateway_ip: false,\n\t\tsubnet_mask: false\n\t});\n\n\tasync function postAPSettings(data: ApSettings) {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/apSettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(data)\n\t\t\t});\n\t\t\tif (response.status == 200) {\n\t\t\t\tnotifications.success('Access Point settings updated.', 3000);\n\t\t\t\tapSettings = await response.json();\n\t\t\t} else {\n\t\t\t\tnotifications.error('User not authorized.', 3000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tfunction handleSubmitAP() {\n\t\tlet valid = true;\n\n\t\t// Validate SSID\n\t\tif (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {\n\t\t\tvalid = false;\n\t\t\tformErrors.ssid = true;\n\t\t} else {\n\t\t\tformErrors.ssid = false;\n\t\t}\n\n\t\t// Validate Channel\n\t\tlet channel = Number(apSettings.channel);\n\t\tif (1 > channel || channel > 13) {\n\t\t\tvalid = false;\n\t\t\tformErrors.channel = true;\n\t\t} else {\n\t\t\tformErrors.channel = false;\n\t\t}\n\n\t\t// Validate max_clients\n\t\tlet maxClients = Number(apSettings.max_clients);\n\t\tif (1 > maxClients || maxClients > 8) {\n\t\t\tvalid = false;\n\t\t\tformErrors.max_clients = true;\n\t\t} else {\n\t\t\tformErrors.max_clients = false;\n\t\t}\n\n\t\t// RegEx for IPv4\n\t\tconst regexExp =\n\t\t\t/\\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\\b/;\n\n\t\t// Validate gateway IP\n\t\tif (!regexExp.test(apSettings.gateway_ip)) {\n\t\t\tvalid = false;\n\t\t\tformErrors.gateway_ip = true;\n\t\t} else {\n\t\t\tformErrors.gateway_ip = false;\n\t\t}\n\n\t\t// Validate Subnet Mask\n\t\tif (!regexExp.test(apSettings.subnet_mask)) {\n\t\t\tvalid = false;\n\t\t\tformErrors.subnet_mask = true;\n\t\t} else {\n\t\t\tformErrors.subnet_mask = false;\n\t\t}\n\n\t\t// Validate local IP\n\t\tif (!regexExp.test(apSettings.local_ip)) {\n\t\t\tvalid = false;\n\t\t\tformErrors.local_ip = true;\n\t\t} else {\n\t\t\tformErrors.local_ip = false;\n\t\t}\n\n\t\t// Submit JSON to REST API\n\t\tif (valid) {\n\t\t\tpostAPSettings(apSettings);\n\t\t}\n\t}\n\n\tfunction preventDefault(fn) {\n\t\treturn function (event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn.call(this, event);\n\t\t};\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<AP class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>Access Point</span>\n\t{/snippet}\n\t<div class=\"w-full\">\n\t\t{#await getAPStatus()}\n\t\t\t<Spinner />\n\t\t{:then nothing}\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status].bg_color}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AP class=\"h-auto w-full scale-75 {apStatusDescription[apStatus.status].text_color}\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Status</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{apStatusDescription[apStatus.status].description}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Home class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">IP Address</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{apStatus.ip_address}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<MAC class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">MAC Address</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{apStatus.mac_address}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t<Devices class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">AP Clients</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{apStatus.station_num}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{/await}\n\t</div>\n\n\t{#if !page.data.features.security || $user.admin}\n\t\t<div class=\"bg-base-200 shadow-lg relative grid w-full max-w-2xl self-center overflow-hidden\">\n\t\t\t<div\n\t\t\t\tclass=\"min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium\"\n\t\t\t>\n\t\t\t\tChange AP Settings\n\t\t\t</div>\n\t\t\t{#await getAPSettings()}\n\t\t\t\t<Spinner />\n\t\t\t{:then nothing}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"flex flex-col gap-2 p-0\"\n\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t>\n\t\t\t\t\t<form\n\t\t\t\t\t\tclass=\"fieldset grid w-full grid-cols-1 content-center gap-x-4 gap-y-2 p-4 mb-4 sm:grid-cols-2\"\n\t\t\t\t\t\tonsubmit={preventDefault(handleSubmitAP)}\n\t\t\t\t\t\tnovalidate\n\t\t\t\t\t\tbind:this={formField}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"apmode\">Provide Access Point ... </label>\n\t\t\t\t\t\t\t<select class=\"select w-full\" id=\"apmode\" bind:value={apSettings.provision_mode}>\n\t\t\t\t\t\t\t\t{#each provisionMode as mode}\n\t\t\t\t\t\t\t\t\t<option value={mode.id}>\n\t\t\t\t\t\t\t\t\t\t{mode.text}\n\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"ssid\">SSID</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.ssid\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tbind:value={apSettings.ssid}\n\t\t\t\t\t\t\t\tid=\"ssid\"\n\t\t\t\t\t\t\t\tmin=\"2\"\n\t\t\t\t\t\t\t\tmax=\"32\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"ssid\">\n\t\t\t\t\t\t\t\t<span class=\"text-error {formErrors.ssid ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t\t>SSID must be between 2 and 32 characters long</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"pwd\">Password</label>\n\t\t\t\t\t\t\t<InputPassword bind:value={apSettings.password} id=\"pwd\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"channel\">Preferred Channel</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\t\t\tmax=\"13\"\n\t\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.channel\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tbind:value={apSettings.channel}\n\t\t\t\t\t\t\t\tid=\"channel\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"channel\">\n\t\t\t\t\t\t\t\t<span class=\"text-error {formErrors.channel ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t\t>Must be channel 1 to 13</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"clients\">Max Clients</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t\t\t\tmax=\"8\"\n\t\t\t\t\t\t\t\tclass=\"input w-full invalid:border-error invalid:border-2 {formErrors.max_clients\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tbind:value={apSettings.max_clients}\n\t\t\t\t\t\t\t\tid=\"clients\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"clients\">\n\t\t\t\t\t\t\t\t<span class=\"text-error {formErrors.max_clients ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t\t>Maximum 8 clients allowed</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"localIP\">Local IP</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input w-full {formErrors.local_ip ? 'border-error border-2' : ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={apSettings.local_ip}\n\t\t\t\t\t\t\t\tid=\"localIP\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"localIP\">\n\t\t\t\t\t\t\t\t<span class=\"text-error {formErrors.local_ip ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t\t>Must be a valid IPv4 address</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"gateway\">Gateway IP</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input w-full {formErrors.gateway_ip ? 'border-error border-2' : ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={apSettings.gateway_ip}\n\t\t\t\t\t\t\t\tid=\"gateway\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"gateway\">\n\t\t\t\t\t\t\t\t<span class=\"text-error {formErrors.gateway_ip ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t\t>Must be a valid IPv4 address</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"subnet\">Subnet Mask</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input w-full {formErrors.subnet_mask ? 'border-error border-2' : ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={apSettings.subnet_mask}\n\t\t\t\t\t\t\t\tid=\"subnet\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"subnet\">\n\t\t\t\t\t\t\t\t<span class=\"text-error {formErrors.subnet_mask ? '' : 'hidden'}\"\n\t\t\t\t\t\t\t\t\t>Must be a valid IPv4 address</span\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<label class=\"label my-auto cursor-pointer justify-start gap-4\">\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\t\tbind:checked={apSettings.ssid_hidden}\n\t\t\t\t\t\t\t\tclass=\"checkbox checkbox-primary\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<span class=\"\">Hide SSID</span>\n\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t<div class=\"place-self-end\">\n\t\t\t\t\t\t\t<button class=\"btn btn-primary\" type=\"submit\">Apply Settings</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</form>\n\t\t\t\t</div>\n\t\t\t{/await}\n\t\t</div>\n\t{/if}\n</SettingsCard>\n"
  },
  {
    "path": "interface/src/routes/wifi/sta/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport type { PageData } from './$types';\n\timport Wifi from './Wifi.svelte';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t}\n\n\tlet { data }: Props = $props();\n</script>\n\n<div\n\tclass=\"mx-0 my-1 flex flex-col space-y-4\n     sm:mx-8 sm:my-8\"\n>\n\t<Wifi />\n</div>\n"
  },
  {
    "path": "interface/src/routes/wifi/sta/+page.ts",
    "content": "import type { PageLoad } from './$types';\n\nexport const load = (async () => {\n\treturn {\n\t\ttitle: 'WiFi Station'\n\t};\n}) satisfies PageLoad;\n"
  },
  {
    "path": "interface/src/routes/wifi/sta/EditNetwork.svelte",
    "content": "<script lang=\"ts\">\n\timport { modals } from 'svelte-modals';\n\timport { fly } from 'svelte/transition';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport type { KnownNetworkItem } from '$lib/types/models';\n\timport InputPassword from '$lib/components/InputPassword.svelte';\n\timport Cancel from '~icons/tabler/x';\n\timport Set from '~icons/tabler/check';\n\n\tinterface Props {\n\t\tisOpen: boolean;\n\t\ttitle: string;\n\t\tnetworkEditable?: KnownNetworkItem;\n\t\tonSaveNetwork: any;\n\t}\n\n\tlet {\n\t\tisOpen,\n\t\ttitle,\n\t\tnetworkEditable: _networkEditable = {\n\t\t\tssid: '',\n\t\t\tpassword: '',\n\t\t\tstatic_ip_config: false,\n\t\t\tlocal_ip: undefined,\n\t\t\tsubnet_mask: undefined,\n\t\t\tgateway_ip: undefined,\n\t\t\tdns_ip_1: undefined,\n\t\t\tdns_ip_2: undefined\n\t\t} as KnownNetworkItem,\n\t\tonSaveNetwork\n\t}: Props = $props();\n\n\t// Make passed object reactive to prevent Svelte warning 'binding_property_non_reactive'\n\t// https://github.com/sveltejs/svelte/issues/12320\n\tlet networkEditable = $state(_networkEditable);\n\n\t// Create helper variable to achieve reactivity\n\tlet staticIPConfig = $state(networkEditable.static_ip_config);\n\n\t// Use this to directly access the form's DOM element\n\tlet formField: any = $state();\n\n\tlet formErrors = $state({\n\t\tssid: false,\n\t\tlocal_ip: false,\n\t\tgateway_ip: false,\n\t\tsubnet_mask: false,\n\t\tdns_1: false,\n\t\tdns_2: false\n\t});\n\n\tfunction validateNetworkSettings() {\n\t\tlet valid = true;\n\n\t\t// Validate SSID\n\t\tif (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {\n\t\t\tvalid = false;\n\t\t\tformErrors.ssid = true;\n\t\t} else {\n\t\t\tformErrors.ssid = false;\n\t\t}\n\n\t\tif (staticIPConfig) {\n\t\t\t// RegEx for IPv4\n\t\t\tconst regexExp =\n\t\t\t\t/\\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\\b/;\n\n\t\t\t// Validate gateway IP\n\t\t\tif (!regexExp.test(networkEditable.gateway_ip!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.gateway_ip = true;\n\t\t\t} else {\n\t\t\t\tformErrors.gateway_ip = false;\n\t\t\t}\n\n\t\t\t// Validate Subnet Mask\n\t\t\tif (!regexExp.test(networkEditable.subnet_mask!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.subnet_mask = true;\n\t\t\t} else {\n\t\t\t\tformErrors.subnet_mask = false;\n\t\t\t}\n\n\t\t\t// Validate local IP\n\t\t\tif (!regexExp.test(networkEditable.local_ip!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.local_ip = true;\n\t\t\t} else {\n\t\t\t\tformErrors.local_ip = false;\n\t\t\t}\n\n\t\t\t// Validate DNS 1\n\t\t\tif (!regexExp.test(networkEditable.dns_ip_1!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.dns_1 = true;\n\t\t\t} else {\n\t\t\t\tformErrors.dns_1 = false;\n\t\t\t}\n\n\t\t\t// Validate DNS 2\n\t\t\tif (!regexExp.test(networkEditable.dns_ip_2!)) {\n\t\t\t\tvalid = false;\n\t\t\t\tformErrors.dns_2 = true;\n\t\t\t} else {\n\t\t\t\tformErrors.dns_2 = false;\n\t\t\t}\n\t\t} else {\n\t\t\tformErrors.local_ip = false;\n\t\t\tformErrors.subnet_mask = false;\n\t\t\tformErrors.gateway_ip = false;\n\t\t\tformErrors.dns_1 = false;\n\t\t\tformErrors.dns_2 = false;\n\t\t}\n\n\t\tif (valid) {\n\t\t\tnetworkEditable.static_ip_config = staticIPConfig;\n\t\t\tonSaveNetwork(networkEditable);\n\t\t}\n\t}\n\n\tfunction preventDefault(fn: (event: Event) => void) {\n\t\treturn function (event: Event) {\n\t\t\tevent.preventDefault();\n\t\t\tfn(event);\n\t\t};\n\t}\n</script>\n\n{#if isOpen}\n\t<div\n\t\trole=\"dialog\"\n\t\tclass=\"pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-y-auto\"\n\t\ttransition:fly={{ y: 50 }}\n\t>\n\t\t<div\n\t\t\tclass=\"rounded-box bg-base-100 shadow-secondary/30 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg md:w-[28rem]\"\n\t\t>\n\t\t\t<h2 class=\"text-base-content text-start text-2xl font-bold\">{title}</h2>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<form\n\t\t\t\tclass=\"fieldset\"\n\t\t\t\tonsubmit={preventDefault(validateNetworkSettings)}\n\t\t\t\tnovalidate\n\t\t\t\tbind:this={formField}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"grid w-full grid-cols-1 content-center gap-4 px-4 sm:grid-cols-2\"\n\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"ssid\">SSID</label>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tclass=\"input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.ssid\n\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\tbind:value={networkEditable.ssid}\n\t\t\t\t\t\t\tid=\"ssid\"\n\t\t\t\t\t\t\tmin=\"3\"\n\t\t\t\t\t\t\tmax=\"32\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{#if formErrors.ssid}\n\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t<label for=\"ssid\" class=\"label\">\n\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\tSSID must be between 3 and 32 characters long.\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label class=\"label\" for=\"pwd\">Password</label>\n\t\t\t\t\t\t<InputPassword bind:value={networkEditable.password} id=\"pwd\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<label\n\t\t\t\t\t\tclass=\"label inline-flex cursor-pointer content-end justify-start gap-4 sm:col-span-2\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tbind:checked={staticIPConfig}\n\t\t\t\t\t\t\tclass=\"checkbox checkbox-primary\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>Use static IP config</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\n\t\t\t\t{#if staticIPConfig}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"grid w-full grid-cols-1 content-center mt-4 gap-4 px-4 sm:grid-cols-2\"\n\t\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"localIP\">Local IP</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.local_ip\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={networkEditable.local_ip}\n\t\t\t\t\t\t\t\tid=\"localIP\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.local_ip}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"localIP\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tLocal IP must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"gateway\">Gateway IP</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.gateway_ip\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={networkEditable.gateway_ip}\n\t\t\t\t\t\t\t\tid=\"gateway\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.gateway_ip}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"gateway\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tGateway IP must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"subnet\">Subnet Mask</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.subnet_mask\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={networkEditable.subnet_mask}\n\t\t\t\t\t\t\t\tid=\"subnet\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.subnet_mask}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"subnet\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tSubnet Mask must be a valid IPv4 subnet mask.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"dns_1\">DNS 1</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.dns_1\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={networkEditable.dns_ip_1}\n\t\t\t\t\t\t\t\tid=\"dns_1\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.dns_1}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"dns_1\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tDNS 1 must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"dns_2\">DNS 2</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered w-full {formErrors.dns_2\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tminlength=\"7\"\n\t\t\t\t\t\t\t\tmaxlength=\"15\"\n\t\t\t\t\t\t\t\tsize=\"15\"\n\t\t\t\t\t\t\t\tbind:value={networkEditable.dns_ip_2}\n\t\t\t\t\t\t\t\tid=\"dns_2\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrors.dns_2}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"dns_2\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tDNS 2 must be a valid IPv4 address.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\n\t\t\t\t<div class=\"divider my-2\"></div>\n\n\t\t\t\t<div class=\"flex justify-end gap-2\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-neutral text-neutral-content inline-flex items-center\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tmodals.close(1);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Cancel class=\"mr-2 h-5 w-5\" />\n\t\t\t\t\t\t<span>Cancel</span>\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary text-primary-content inline-flex items-center\"\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Set class=\"mr-2 h-5 w-5\" />\n\t\t\t\t\t\t<span>Set</span>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/routes/wifi/sta/Scan.svelte",
    "content": "<script lang=\"ts\">\n\timport { modals } from 'svelte-modals';\n\timport { focusTrap } from 'svelte-focus-trap';\n\timport { fly } from 'svelte/transition';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport Network from '~icons/tabler/router';\n\timport AP from '~icons/tabler/access-point';\n\timport Cancel from '~icons/tabler/x';\n\timport Reload from '~icons/tabler/reload';\n\timport { onMount, onDestroy } from 'svelte';\n\timport RssiIndicator from '$lib/components/RSSIIndicator.svelte';\n\timport type { NetworkItem } from '$lib/types/models';\n\n\t// provided by <Modals />\n\tinterface Props {\n\t\tisOpen: boolean;\n\t\tstoreNetwork: any;\n\t}\n\n\tconst { isOpen, storeNetwork }: Props = $props();\n\n\tconst encryptionType = [\n\t\t'Open',\n\t\t'WEP',\n\t\t'WPA PSK',\n\t\t'WPA2 PSK',\n\t\t'WPA WPA2 PSK',\n\t\t'WPA2 Enterprise',\n\t\t'WPA3 PSK',\n\t\t'WPA2 WPA3 PSK',\n\t\t'WAPI PSK'\n\t];\n\n\tlet listOfNetworks: NetworkItem[] = $state([]);\n\n\tlet scanActive = $state(false);\n\n\tlet pollingId: number;\n\n\tasync function scanNetworks() {\n\t\tscanActive = true;\n\t\tconst scan = await fetch('/rest/scanNetworks', {\n\t\t\tmethod: 'GET',\n\t\t\theaders: {\n\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t}\n\t\t});\n\t\tif ((await pollingResults()) == false) {\n\t\t\tpollingId = setInterval(() => pollingResults(), 1000);\n\t\t}\n\t\treturn;\n\t}\n\n\tasync function pollingResults() {\n\t\tconst response = await fetch('/rest/listNetworks', {\n\t\t\tmethod: 'GET',\n\t\t\theaders: {\n\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t}\n\t\t});\n\t\ttry {\n\t\t\tconst result = await response.json();\n\t\t\tlistOfNetworks = result.networks;\n\t\t\tif (listOfNetworks.length) {\n\t\t\t\tscanActive = false;\n\t\t\t\tclearInterval(pollingId);\n\t\t\t\tpollingId = 0;\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\tscanActive = false;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tonMount(() => {\n\t\tscanNetworks();\n\t});\n\n\tonDestroy(() => {\n\t\tif (pollingId) {\n\t\t\tclearInterval(pollingId);\n\t\t\tpollingId = 0;\n\t\t}\n\t});\n</script>\n\n{#if isOpen}\n\t<div\n\t\trole=\"dialog\"\n\t\tclass=\"pointer-events-none fixed inset-0 z-50 flex items-center justify-center\"\n\t\ttransition:fly={{ y: 50 }}\n\t\tuse:focusTrap\n\t>\n\t\t<div\n\t\t\tclass=\"bg-base-100 shadow-secondary/30 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg\"\n\t\t>\n\t\t\t<h2 class=\"text-base-content text-start text-2xl font-bold\">Scan Networks</h2>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<div class=\"overflow-y-auto\">\n\t\t\t\t{#if scanActive}<div class=\"bg-base-100 flex flex-col items-center justify-center p-6\">\n\t\t\t\t\t\t<AP class=\"text-secondary h-32 w-32 shrink animate-ping stroke-2\" />\n\t\t\t\t\t\t<p class=\"mt-8 text-2xl\">Scanning ...</p>\n\t\t\t\t\t</div>\n\t\t\t\t{:else}\n\t\t\t\t\t<ul class=\"menu w-full\">\n\t\t\t\t\t\t{#each listOfNetworks as network, i}\n\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\t\t\ttabindex={i}\n\t\t\t\t\t\t\t\t\tclass=\"bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]\"\n\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\tstoreNetwork(network.ssid);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10 shrink-0\">\n\t\t\t\t\t\t\t\t\t\t<Network class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<div class=\"font-bold\">{network.ssid}</div>\n\t\t\t\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t\t\t\tSecurity: {encryptionType[network.encryption_type]}, Channel: {network.channel}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"grow\"></div>\n\t\t\t\t\t\t\t\t\t<RssiIndicator\n\t\t\t\t\t\t\t\t\t\tshowDBm={true}\n\t\t\t\t\t\t\t\t\t\trssi_dbm={network.rssi}\n\t\t\t\t\t\t\t\t\t\tclass=\"text-base-content h-10 w-10\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</ul>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t\t<div class=\"divider my-2\"></div>\n\t\t\t<div class=\"flex flex-wrap justify-end gap-2\">\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn btn-primary inline-flex flex-none items-center\"\n\t\t\t\t\tdisabled={scanActive}\n\t\t\t\t\tonclick={scanNetworks}><Reload class=\"mr-2 h-5 w-5\" /><span>Scan again</span></button\n\t\t\t\t>\n\n\t\t\t\t<div class=\"grow\"></div>\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn btn-warning text-warning-content inline-flex flex-none items-center\"\n\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\tmodals.close();\n\t\t\t\t\t}}><Cancel class=\"mr-2 h-5 w-5\" /><span>Cancel</span></button\n\t\t\t\t>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{/if}\n"
  },
  {
    "path": "interface/src/routes/wifi/sta/Wifi.svelte",
    "content": "<script lang=\"ts\">\n\timport { onDestroy, onMount } from 'svelte';\n\timport { socket } from '$lib/stores/socket';\n\timport { modals } from 'svelte-modals';\n\timport { slide } from 'svelte/transition';\n\timport { cubicOut } from 'svelte/easing';\n\timport { user } from '$lib/stores/user';\n\timport { page } from '$app/state';\n\timport { notifications } from '$lib/components/toasts/notifications';\n\timport DraggableList from '$lib/components/DraggableList.svelte';\n\timport SettingsCard from '$lib/components/SettingsCard.svelte';\n\timport Collapsible from '$lib/components/Collapsible.svelte';\n\timport ConfirmDialog from '$lib/components/ConfirmDialog.svelte';\n\timport InfoDialog from '$lib/components/InfoDialog.svelte';\n\timport type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models';\n\timport ScanNetworks from './Scan.svelte';\n\timport EditNetwork from './EditNetwork.svelte';\n\timport Spinner from '$lib/components/Spinner.svelte';\n\timport AP from '~icons/tabler/access-point';\n\timport Router from '~icons/tabler/router';\n\timport Settings from '~icons/tabler/settings';\n\timport MAC from '~icons/tabler/dna-2';\n\timport Home from '~icons/tabler/home';\n\timport WiFi from '~icons/tabler/wifi';\n\timport SSID from '~icons/tabler/router';\n\timport Down from '~icons/tabler/chevron-down';\n\timport DNS from '~icons/tabler/address-book';\n\timport Gateway from '~icons/tabler/torii';\n\timport Subnet from '~icons/tabler/grid-dots';\n\timport Channel from '~icons/tabler/antenna';\n\timport Scan from '~icons/tabler/radar-2';\n\timport Add from '~icons/tabler/circle-plus';\n\timport Edit from '~icons/tabler/pencil';\n\timport Delete from '~icons/tabler/trash';\n\timport Grip from '~icons/tabler/grip-vertical';\n\timport Cancel from '~icons/tabler/x';\n\timport Check from '~icons/tabler/check';\n\timport Save from '~icons/tabler/device-floppy';\n\timport Info from '~icons/tabler/info-circle';\n\n\ttype WifiReconnectEvent = {\n\t\tdelay_ms: number;\n\t};\n\n\tlet wifiStatus: WifiStatus = $state({\n\t\tstatus: 0,\n\t\tssid: '',\n\t\tlocal_ip: '',\n\t\tip_address: '',\n\t\tmac_address: '',\n\t\trssi: 0,\n\t\tbssid: '',\n\t\tchannel: 0,\n\t\tgateway_ip: '',\n\t\tsubnet_mask: '',\n\t\tdns_ip_1: '',\n\t\tdns_ip_2: '',\n\t\tconnected: false\n\t});\n\n\tlet wifiSettings: WifiSettings = $state({\n\t\thostname: '',\n\t\tconnection_mode: 1,\n\t\twifi_networks: [] as KnownNetworkItem[]\n\t});\n\n\t// Stringify to recognize changes\n\t// svelte-ignore state_referenced_locally\n\tlet strWifiSettings: string = $state(JSON.stringify(wifiSettings));\n\t// Recognize changes in settings\n\tlet isSettingsDirty: boolean = $derived(JSON.stringify(wifiSettings) !== strWifiSettings);\n\n\tlet showWifiDetails = $state(false);\n\n\tlet formErrorhostname = $state(false);\n\n\tlet connectionMode = [\n\t\t{\n\t\t\tid: 0,\n\t\t\ttext: `Offline`\n\t\t},\n\t\t{\n\t\t\tid: 1,\n\t\t\ttext: `Signal Strength`\n\t\t},\n\t\t{\n\t\t\tid: 2,\n\t\t\ttext: `Priority (Sequence)`\n\t\t}\n\t];\n\n\tasync function getWifiStatus() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/wifiStatus', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\twifiStatus = await response.json();\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tasync function getWifiSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/wifiSettings', {\n\t\t\t\tmethod: 'GET',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t}\n\t\t\t});\n\t\t\twifiSettings = await response.json();\n\t\t\tstrWifiSettings = JSON.stringify(wifiSettings); // Store the recently loaded settings in a string variable\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tasync function postWiFiSettings() {\n\t\ttry {\n\t\t\tconst response = await fetch('/rest/wifiSettings', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(wifiSettings)\n\t\t\t});\n\t\t\tif (response.status == 200) {\n\t\t\t\tnotifications.success('Wi-Fi settings updated.', 3000);\n\t\t\t\twifiSettings = await response.json();\n\t\t\t\tstrWifiSettings = JSON.stringify(wifiSettings); // Store the recently loaded settings in a string variable\n\t\t\t} else {\n\t\t\t\tnotifications.error('Failed to update Wi-Fi settings.', 5000);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error:', error);\n\t\t}\n\t}\n\n\tconst interval = setInterval(async () => {\n\t\tgetWifiStatus();\n\t}, 5000);\n\n\tonMount(() => {\n\t\tsocket.on<WifiReconnectEvent>('reconnect', (data) => {\n\t\t\tnotifications.warning(\n\t\t\t\t`Reconnecting shortly as new WiFi settings will be applied in ${Math.round(data.delay_ms / 1000)} seconds.`,\n\t\t\t\t5000\n\t\t\t);\n\t\t});\n\t});\n\n\tonDestroy(() => {\n\t\tclearInterval(interval);\n\t\tsocket.off('reconnect');\n\t});\n\n\tfunction checkHostname() {\n\t\tif (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {\n\t\t\tformErrorhostname = true;\n\t\t} else {\n\t\t\tformErrorhostname = false;\n\t\t}\n\n\t\treturn !formErrorhostname;\n\t}\n\n\tfunction applyWifiSettings() {\n\t\tif (checkHostname()) {\n\t\t\tpostWiFiSettings();\n\t\t}\n\t}\n\n\tfunction scanForNetworks() {\n\t\tmodals.open(ScanNetworks, {\n\t\t\tstoreNetwork: (ssid: string) => {\n\t\t\t\tconsole.log('Storing network:', ssid);\n\t\t\t\tmodals.close();\n\t\t\t\thandleNewNetwork(ssid);\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction handleNewNetwork(ssid?: string) {\n\t\tmodals.open(EditNetwork, {\n\t\t\ttitle: 'Add network',\n\t\t\tnetworkEditable: {\n\t\t\t\tssid: ssid || '',\n\t\t\t\tpassword: '',\n\t\t\t\tstatic_ip_config: false,\n\t\t\t\tlocal_ip: undefined,\n\t\t\t\tsubnet_mask: undefined,\n\t\t\t\tgateway_ip: undefined,\n\t\t\t\tdns_ip_1: undefined,\n\t\t\t\tdns_ip_2: undefined\n\t\t\t},\n\t\t\tonSaveNetwork: async (newNetwork: KnownNetworkItem) => {\n\t\t\t\twifiSettings.wifi_networks = [...wifiSettings.wifi_networks, newNetwork];\n\t\t\t\tmodals.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction handleEdit(index: number) {\n\t\tmodals.open(EditNetwork, {\n\t\t\ttitle: 'Edit network',\n\t\t\tnetworkEditable: $state.snapshot(wifiSettings.wifi_networks[index]), // Deep copy\n\t\t\tonSaveNetwork: async (editedNetwork: KnownNetworkItem) => {\n\t\t\t\twifiSettings.wifi_networks[index] = editedNetwork;\n\t\t\t\tmodals.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction confirmDelete(index: number) {\n\t\tmodals.open(ConfirmDialog, {\n\t\t\ttitle: 'Delete Network?',\n\t\t\tmessage: `Are you sure you want to delete network \\'${wifiSettings.wifi_networks[index].ssid}\\'?`,\n\t\t\tlabels: {\n\t\t\t\tcancel: { label: 'Cancel', icon: Cancel },\n\t\t\t\tconfirm: { label: 'Delete', icon: Delete }\n\t\t\t},\n\t\t\tonConfirm: () => {\n\t\t\t\twifiSettings.wifi_networks.splice(index, 1);\n\t\t\t\tmodals.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction isNetworkListTooLong() {\n\t\tif (wifiSettings.wifi_networks.length >= 5) {\n\t\t\tmodals.open(InfoDialog, {\n\t\t\t\ttitle: 'Reached Maximum Networks',\n\t\t\t\tmessage:\n\t\t\t\t\t'You have reached the maximum number of networks. Please delete one to add another.',\n\t\t\t\tdismiss: { label: 'OK', icon: Check },\n\t\t\t\tonDismiss: () => {\n\t\t\t\t\tmodals.close();\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tfunction handleNetworkReorder(reorderedNetworks: KnownNetworkItem[]) {\n\t\twifiSettings.wifi_networks = reorderedNetworks;\n\t}\n\n\tasync function getWifiData() {\n\t\tawait getWifiStatus();\n\t\tawait getWifiSettings();\n\t}\n</script>\n\n<SettingsCard collapsible={false}>\n\t{#snippet icon()}\n\t\t<Router class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t{/snippet}\n\t{#snippet title()}\n\t\t<span>WiFi Connection</span>\n\t{/snippet}\n\t{#await getWifiData()}\n\t\t<Spinner />\n\t{:then nothing}\n\t\t<div class=\"w-full overflow-x-auto\">\n\t\t\t<div\n\t\t\t\tclass=\"flex w-full flex-col space-y-1\"\n\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t>\n\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"mask mask-hexagon h-auto w-10 {wifiStatus.status === 3\n\t\t\t\t\t\t\t? 'bg-success'\n\t\t\t\t\t\t\t: 'bg-error'}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AP\n\t\t\t\t\t\t\tclass=\"h-auto w-full scale-75 {wifiStatus.status === 3\n\t\t\t\t\t\t\t\t? 'text-success-content'\n\t\t\t\t\t\t\t\t: 'text-error-content'}\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"font-bold\">Status</div>\n\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t{wifiStatus.status === 3 ? 'Connected' : 'Inactive'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{#if wifiStatus.status === 3}\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<SSID class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">SSID</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.ssid}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Home class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">IP Address</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.local_ip}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<WiFi class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">RSSI</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.rssi} dBm\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"grow\"></div>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tclass=\"btn btn-circle btn-ghost btn-sm modal-button\"\n\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\tshowWifiDetails = !showWifiDetails;\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Down\n\t\t\t\t\t\t\t\tclass=\"text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {showWifiDetails\n\t\t\t\t\t\t\t\t\t? 'rotate-180'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t<!-- Folds open -->\n\t\t\t{#if showWifiDetails}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"flex w-full flex-col space-y-1 pt-1\"\n\t\t\t\t\ttransition:slide|local={{ duration: 300, easing: cubicOut }}\n\t\t\t\t>\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<MAC class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">MAC Address</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.mac_address}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Channel class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">Channel</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.channel}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Gateway class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">Gateway IP</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.gateway_ip}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<Subnet class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">Subnet Mask</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.subnet_mask}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2\">\n\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t<DNS class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div class=\"font-bold\">DNS</div>\n\t\t\t\t\t\t\t<div class=\"text-sm opacity-75\">\n\t\t\t\t\t\t\t\t{wifiStatus.dns_ip_1}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t</div>\n\n\t\t{#if !page.data.features.security || $user.admin}\n\t\t\t<Collapsible open={true} class=\"shadow-lg\" isDirty={isSettingsDirty}>\n\t\t\t\t{#snippet icon()}\n\t\t\t\t\t<Settings class=\"lex-shrink-0 mr-2 h-6 w-6 self-end\" />\n\t\t\t\t{/snippet}\n\t\t\t\t{#snippet title()}\n\t\t\t\t\t<span>Settings & Networks</span>\n\t\t\t\t{/snippet}\n\t\t\t\t<div class=\"fieldset\">\n\t\t\t\t\t<div class=\"grid w-full grid-cols-1 content-center gap-4 sm:grid-cols-2\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"hostname\">Host Name (mDNS)</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tmin=\"3\"\n\t\t\t\t\t\t\t\tmax=\"32\"\n\t\t\t\t\t\t\t\tclass=\"input input-bordered invalid:border-error w-full invalid:border-2 {formErrorhostname\n\t\t\t\t\t\t\t\t\t? 'border-error border-2'\n\t\t\t\t\t\t\t\t\t: ''}\"\n\t\t\t\t\t\t\t\tbind:value={wifiSettings.hostname}\n\t\t\t\t\t\t\t\tid=\"hostname\"\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{#if formErrorhostname}\n\t\t\t\t\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t\t\t\t<label for=\"hostname\" class=\"label\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-error\">\n\t\t\t\t\t\t\t\t\t\t\tHost name must be between 3 and 32 characters long.\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<label class=\"label\" for=\"apmode\">WiFi Connection Mode</label>\n\t\t\t\t\t\t\t<select class=\"select w-full\" id=\"apmode\" bind:value={wifiSettings.connection_mode}>\n\t\t\t\t\t\t\t\t{#each connectionMode as mode}\n\t\t\t\t\t\t\t\t\t<option value={mode.id}>\n\t\t\t\t\t\t\t\t\t\t{mode.text}\n\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"divider mt-2 mb-0\"></div>\n\n\t\t\t\t<div class=\"flex justify-end w-full gap-x-2\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary text-primary-content btn-md\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\thandleNewNetwork();\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Add class=\"h-6 w-6\" />\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary text-primary-content btn-md\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tif (!isNetworkListTooLong()) {\n\t\t\t\t\t\t\t\tscanForNetworks();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Scan class=\"h-6 w-6\" />\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\n\t\t\t\t<div transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t{#if wifiSettings.wifi_networks.length === 0}\n\t\t\t\t\t\t<div class=\"text-center text-base-content/50 mt-2\">\n\t\t\t\t\t\t\tNo WiFi networks configured yet.<br />\n\t\t\t\t\t\t\tScan for available networks or add one manually.\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<DraggableList\n\t\t\t\t\t\t\titems={wifiSettings.wifi_networks}\n\t\t\t\t\t\t\tonReorder={handleNetworkReorder}\n\t\t\t\t\t\t\tclass=\"space-y-2\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{#snippet children({ item: network, index }: { item: any; index: number })}\n\t\t\t\t\t\t\t\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclass=\"rounded-box bg-base-100 grid grid-cols-[auto_auto_minmax(6rem,1fr)_auto] items-center gap-3 p-2\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Grip class=\"h-6 w-6 text-base-content/30 cursor-grab\" />\n\t\t\t\t\t\t\t\t\t<div class=\"mask mask-hexagon bg-primary h-auto w-10\">\n\t\t\t\t\t\t\t\t\t\t<Router class=\"text-primary-content h-auto w-full scale-75\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"flex items-center gap-2 overflow-hidden\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"font-bold truncate\">{network.ssid}</div>\n\t\t\t\t\t\t\t\t\t\t{#if network.static_ip_config}\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"badge badge-sm badge-secondary opacity-75 flex-shrink-0 hidden sm:block\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\tStatic\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"badge badge-sm badge-outline badge-secondary opacity-75 flex-shrink-0 hidden sm:block\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\tDHCP\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"flex\">\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-ghost btn-sm\"\n\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst actualIndex = wifiSettings.wifi_networks.findIndex(\n\t\t\t\t\t\t\t\t\t\t\t\t\t(n) => n.ssid === network.ssid\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\thandleEdit(actualIndex);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Edit class=\"h-6 w-6\" /></button\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-ghost btn-sm\"\n\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst actualIndex = wifiSettings.wifi_networks.findIndex(\n\t\t\t\t\t\t\t\t\t\t\t\t\t(n) => n.ssid === network.ssid\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\tconfirmDelete(actualIndex);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Delete class=\"text-error h-6 w-6\" />\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/snippet}\n\t\t\t\t\t\t</DraggableList>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t{#if wifiSettings.connection_mode === 2 && wifiSettings.wifi_networks.length > 1}\n\t\t\t\t\t<div class=\"w-full\" transition:slide|local={{ duration: 300, easing: cubicOut }}>\n\t\t\t\t\t\t<div role=\"alert\" class=\"alert bg-base-300 mt-2\">\n\t\t\t\t\t\t\t<Info class=\"h-6 w-6\" />\n\t\t\t\t\t\t\t<div>Arrange the networks according to their priority (most important first).</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\n\t\t\t\t<div class=\"divider mt-2 mb-0\"></div>\n\n\t\t\t\t<div class=\"flex flex-wrap justify-end mb-4\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn btn-primary\"\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tdisabled={!isSettingsDirty}\n\t\t\t\t\t\tonclick={applyWifiSettings}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Save class=\"mr-2 h-5 w-5\" />\n\t\t\t\t\t\t<span>Apply Settings</span>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</Collapsible>\n\t\t{/if}\n\t{/await}\n</SettingsCard>\n"
  },
  {
    "path": "interface/static/manifest.json",
    "content": "{\n\t\"name\": \"ESP32 SvelteKit\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"/favicon.png\",\n\t\t\t\"sizes\": \"48x48 72x72 96x96 128x128 256x256\"\n\t\t}\n\t],\n\t\"start_url\": \"/\",\n\t\"display\": \"fullscreen\",\n\t\"orientation\": \"any\"\n}\n"
  },
  {
    "path": "interface/svelte.config.js",
    "content": "import adapter from '@sveltejs/adapter-static';\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\t// Consult https://kit.svelte.dev/docs/integrations#preprocessors\n\t// for more information about preprocessors\n\tpreprocess: vitePreprocess(),\n\n\tkit: {\n\t\tadapter: adapter({\n\t\t\tpages: 'build',\n\t\t\tassets: 'build',\n\t\t\tfallback: 'index.html',\n\t\t\tprecompress: false,\n\t\t\tstrict: true\n\t\t}),\n\t\talias: {\n\t\t\t$src: './src'\n\t\t},\n        output: {\n            bundleStrategy: 'single'\n        }\n\t}\n};\n\nexport default config;\n"
  },
  {
    "path": "interface/tsconfig.json",
    "content": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true\n\t}\n\t// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias\n\t//\n\t// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes\n\t// from the referenced tsconfig.json - TypeScript does not merge them in\n}\n"
  },
  {
    "path": "interface/vite-plugin-littlefs.ts",
    "content": "import type { UserConfig, Plugin } from 'vite';\n\nexport default function viteLittleFS(): Plugin[] {\n\treturn [\n\t\t{\n\t\t\tname: 'vite-plugin-littlefs',\n\t\t\tenforce: 'post',\n\t\t\tapply: 'build',\n\n\t\t\tasync config(config, _configEnv) {\n\t\t\t\tconst { assetFileNames, chunkFileNames, entryFileNames } =\n\t\t\t\t\tconfig.build?.rollupOptions?.output;\n\n        // Handle Server-build + Client Assets\n        config.build.rollupOptions.output = {\n          ...config.build?.rollupOptions?.output,\n          assetFileNames: assetFileNames.replace('.[hash]', '')\n        }\n\n        // Handle Client-build\n        if (config.build?.rollupOptions?.output.chunkFileNames.includes('hash')) {\n\n          config.build.rollupOptions.output = {\n            ...config.build?.rollupOptions?.output,\n            chunkFileNames: chunkFileNames.replace('.[hash]', ''),\n            entryFileNames: entryFileNames.replace('.[hash]', ''),\n          }\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "interface/vite.config.ts",
    "content": "import { sveltekit } from '@sveltejs/kit/vite';\nimport type { UserConfig } from 'vite';\nimport Icons from 'unplugin-icons/vite';\nimport viteLittleFS from './vite-plugin-littlefs';\nimport tailwindcss from '@tailwindcss/vite';\n\nconst config: UserConfig = {\n\tplugins: [\n\t\tsveltekit(),\n\t\tIcons({\n\t\t\tcompiler: 'svelte'\n\t\t}),\n\t\ttailwindcss(),\n\t\t// Shorten file names for LittleFS 32 char limit\n\t\tviteLittleFS()\n\t],\n\tserver: {\n\t\tproxy: {\n\t\t\t// Proxying REST: http://localhost:5173/rest/bar -> http://192.168.1.83/rest/bar\n\t\t\t'/rest': {\n\t\t\t\ttarget: 'http://192.168.1.111',\n\t\t\t\tchangeOrigin: true\n\t\t\t},\n\t\t\t// Proxying websockets ws://localhost:5173/ws -> ws://192.168.1.83/ws\n\t\t\t'/ws': {\n\t\t\t\ttarget: 'ws://192.168.1.111',\n\t\t\t\tchangeOrigin: true,\n\t\t\t\tws: true\n\t\t\t}\n\t\t}\n\t},\n\tbuild: {\n\t\tminify: 'terser',\n\t\tsourcemap: false,\n\t\trollupOptions: {\n\t\t\toutput: {\n\t\t\t\tmanualChunks(id) {\n\t\t\t\t\tif (id.includes('node_modules')) return 'vendor';\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tcssCodeSplit: true\n\t}\n};\n\nexport default config;\n"
  },
  {
    "path": "lib/PsychicHttp/.gitignore",
    "content": "**.vscode\n**.pio\n**.DS_Store\n.pioenvs\n.clang_complete\n.gcc-flags.json\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n# Precompiled Headers\n*.gch\n*.pch\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n# Fortran module files\n*.mod\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n# Executables\n*.exe\n*.out\n*.app\n# Visual Studio/VisualMicro stuff\nVisual\\ Micro\n*.sdf\n*.opensdf\n*.suo\n.pioenvs\n.piolibdeps\n.pio\n.vscode/c_cpp_properties.json\n.vscode/launch.json\n.vscode/settings.json\n.vscode/.browse.c_cpp.db*\n.vscode/ipch\n/psychic-http-loadtest.log\n/psychic-websocket-loadtest.log\nexamples/platformio/lib/PsychicHttp\nbenchmark/.~lock.comparison.ods#\nbenchmark/psychic-http-loadtest.log\n.$request flow.drawio.bkp\n.$request flow.drawio.dtmp\nbenchmark/package-lock.json\nbenchmark/node_modules\nsrc/secret.h\nbenchmark/psychichttp/src/_secret.h\nbenchmark/psychichttps/src/_secret.h\nexamples/platformio/src/_secret.h\nexamples/arduino/src/_secret.h\nsrc/cookie.txt\nexamples/websockets/lib/PsychicHttp\nexamples/websockets/src/_secret.h\n/build\n"
  },
  {
    "path": "lib/PsychicHttp/CHANGELOG.md",
    "content": "# v1.2.1\n\n* Fix bug with missing include preventing the HTTPS server from compiling.\n\n# v1.2\n\n* Added TemplatePrinter from https://github.com/Chris--A/PsychicHttp/tree/templatePrint\n* Support using as ESP IDF component\n* Optional using https server in ESP IDF\n* Fixed bug with headers\n* Add ESP IDF example + CI script\n* Added Arduino Captive Portal example and OTAUpdate from @06GitHub\n* HTTPS fix for ESP-IDF v5.0.2+ from @06GitHub\n* lots of bugfixes from @mathieucarbou\n\nThanks to @Chris--A, @06GitHub, and @dzungpv for your contributions.\n\n# v1.1\n\n* Changed the internal structure to support request handlers on endpoints and generic requests that do not match an endpoint\n    * websockets, uploads, etc should now create an appropriate handler and attach to an endpoint with the server.on() syntax\n* Added PsychicClient to abstract away some of the internals of ESP-IDF sockets + add convenience\n    * onOpen and onClose callbacks have changed as a result\n* Added support for EventSource / SSE\n* Added support for multipart file uploads\n* changed getParam() to return a PsychicWebParameter in line with ESPAsyncWebserver\n* Renamed various classes / files:\n    * PsychicHttpFileResponse -> PsychicFileResponse\n    * PsychicHttpServerEndpoint -> PsychicEndpoint\n    * PsychicHttpServerRequest -> PsychicRequest\n    * PsychicHttpServerResponse -> PsychicResponse\n    * PsychicHttpWebsocket.h -> PsychicWebSocket.h\n    * Websocket => WebSocket\n* Quite a few bugfixes from the community. Thank you @glennsky, @gb88, @KastanEr, @kstam, and @zekageri"
  },
  {
    "path": "lib/PsychicHttp/LICENSE",
    "content": "Copyright (c) 2024 Jeremy Poulter, Zachary Smith, and Mathieu Carbou\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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."
  },
  {
    "path": "lib/PsychicHttp/README.md",
    "content": "# PsychicHttp - HTTP on your ESP 🧙🔮\n\nPsychicHttp is a webserver library for ESP32 + Arduino framework which uses the [ESP-IDF HTTP Server](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/esp_http_server.html) library under the hood.  It is written in a similar style to the [Arduino WebServer](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer), [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer), and [ArduinoMongoose](https://github.com/jeremypoulter/ArduinoMongoose) libraries to make writing code simple and porting from those other libraries straightforward.\n\n# Features\n\n* Asynchronous approach (server runs in its own FreeRTOS thread)\n* Handles all HTTP methods with lots of convenience functions:\n    * GET/POST parameters\n    * get/set headers\n    * get/set cookies\n    * basic key/value session data storage\n    * authentication (basic and digest mode)\n* HTTPS / SSL support\n* Static fileserving (SPIFFS, LittleFS, etc.)\n* Chunked response serving for large files\n* File uploads (Basic + Multipart)\n* Websocket support with onOpen, onFrame, and onClose callbacks\n* EventSource / SSE support with onOpen, and onClose callbacks\n* Request filters, including Client vs AP mode (ON_STA_FILTER / ON_AP_FILTER)\n* TemplatePrinter class for dynamic variables at runtime\n\n## Differences from ESPAsyncWebserver\n\n* No templating system (anyone actually use this?)\n* No url rewriting (but you can use request->redirect)\n\n# Usage\n\n## Installation\n\n### Platformio\n\n[PlatformIO](http://platformio.org) is an open source ecosystem for IoT development.\n\n Add \"PsychicHttp\" to project using [Project Configuration File `platformio.ini`](http://docs.platformio.org/page/projectconf.html) and [lib_deps](http://docs.platformio.org/page/projectconf/section_env_library.html#lib-deps) option:\n\n```ini\n[env:myboard]\nplatform = espressif...\nboard = ...\nframework = arduino\n\n# using the latest stable version\nlib_deps = hoeken/PsychicHttp\n\n# or using GIT Url (the latest development version)\nlib_deps = https://github.com/hoeken/PsychicHttp\n```\n\n### Installation - Arduino\n\nOpen *Tools -> Manage Libraries...* and search for PsychicHttp.\n\n# Principles of Operation\n\n## Things to Note\n\n* PsychicHttp is a fully asynchronous server and as such does not run on the loop thread.\n* You should not use yield or delay or any function that uses them inside the callbacks.\n* The server is smart enough to know when to close the connection and free resources.\n* You can not send more than one response to a single request.\n\n## PsychicHttp\n\n* Listens for connections.\n* Wraps the incoming request into PsychicRequest.\n* Keeps track of clients + calls optional callbacks on client open and close.\n* Find the appropriate handler (if any) for a request and pass it on.\n\n## Request Life Cycle\n\n* TCP connection is received by the server.\n* HTTP request is wrapped inside ```PsychicRequest``` object + TCP Connection wrapped inside PsychicConnection object.\n* When the request head is received, the server goes through all ```PsychicEndpoints``` and finds one that matches the url + method.\n    * ```handler->filter()``` and ```handler->canHandle()``` are called on the handler to verify the handler should process the request.\n    * ```handler->needsAuthentication()``` is called and sends an authorization response if required.\n    * ```handler->handleRequest()``` is called to actually process the HTTP request.\n* If the handler cannot process the request, the server will loop through any global handlers and call that handler if it passes filter(), canHandle(), and needsAuthentication().\n* If no global handlers are called, the server.defaultEndpoint handler will be called.\n* Each handler is responsible for processing the request and sending a response.\n* When the response is sent, the client is closed and freed from the memory.\n    * Unless its a special handler like websockets or eventsource.\n\n![Flowchart of Request Lifecycle](/assets/request-flow.svg)\n\n### Handlers\n\n* ```PsychicHandler``` is used for processing and responding to specific HTTP requests.\n* ```PsychicHandler``` instances can be attached to any endpoint or as global handlers.\n* Setting a ```Filter``` to the ```PsychicHandler``` controls when to apply the handler, decision can be based on\n  request method, url, request host/port/target host, the request client's localIP or remoteIP.\n* Two filter callbacks are provided: ```ON_AP_FILTER``` to execute the rewrite when request is made to the AP interface,\n  ```ON_STA_FILTER``` to execute the rewrite when request is made to the STA interface.\n* The ```canHandle``` method is used for handler specific control on whether the requests can be handled. Decision can be based on request method, request url, request host/port/target host.\n* Depending on how the handler is implemented, it may provide callbacks for adding your own custom processing code to the handler.\n* Global ```Handlers``` are evaluated in the order they are attached to the server. The ```canHandle``` is called only\n  if the ```Filter``` that was set to the ```Handler``` return true.\n* The first global ```Handler``` that can handle the request is selected, no further processing of handlers is called.\n\n![Flowchart of Request Lifecycle](/assets/handler-callbacks.svg)\n\n### Responses and how do they work\n\n* The ```PsychicResponse``` objects are used to send the response data back to the client.\n* Typically the response should be fully generated and sent from the callback.\n* It may be possible to generate the response outside the callback, but it will be difficult.\n   * The exceptions are websockets + eventsource where the response is sent, but the connection is maintained and new data can be sent/received outside the handler.\n\n# Porting From ESPAsyncWebserver\n\nIf you have existing code using ESPAsyncWebserver, you will feel right at home with PsychicHttp.  Even if internally it is much different, the external interface is very similar.  Some things are mostly cosmetic, like different class names and callback definitions.  A few things might require a bit more in-depth approach.  If you're porting your code and run into issues that aren't covered here, please post and issue.\n\n## Globals Stuff\n\n* Change your #include to ```#include <PsychicHttp.h>```\n* Change your server instance: ```PsychicHttpServer server;```\n* Define websocket handler if you have one: ```PsychicWebSocketHandler websocketHandler;```\n* Define eventsource if you have one: ```PsychicEventSource eventSource;```\n\n## setup() Stuff\n\n* no more server.begin(), call server.listen(80), before you add your handlers\n* server has a configurable limit on .on() endpoints. change it with ```server.config.max_uri_handlers = 20;``` as needed.\n* check your callback function definitions:\n   * AsyncWebServerRequest -> PsychicRequest\n   * no more onBody() event\n      * for small bodies (server.maxRequestBodySize, default 16k) it will be automatically loaded and accessed by request->body()\n      * for large bodies, use an upload handler and onUpload()   \n   * websocket callbacks are much different (and simpler!)\n   * websocket / eventsource handlers get attached to url in server.on(\"/url\", &handler) instead of passing url to handler constructor.\n   * eventsource callbacks are onOpen and onClose now.\n* HTTP_ANY is not supported by ESP-IDF, so we can't use it either.\n* NO server.onFileUpload(onUpload); (you could attach an UploadHandler to the default endpoint i guess?)\n* NO server.onRequestBody(onBody); (same)\n\n## Requests / Responses\n\n* request->send is now request->reply()\n* if you create a response, call response->send() directly, not request->send(reply)\n* request->headers() is not supported by ESP-IDF, you have to just check for the header you need.\n* No AsyncCallbackJsonWebHandler (for now... can add if needed)\n* No request->beginResponse().  Instanciate a PsychicResponse instead: ```PsychicResponse response(request);```\n* No PROGMEM suppport (its not relevant to ESP32: https://esp32.com/viewtopic.php?t=20595)\n* No Stream response support just yet\n\n# Usage\n\n## Create the Server\n\nHere is an example of the typical server setup:\n\n```cpp\n#include <PsychicHttp.h>\nPsychicHttpServer server;\n\nvoid setup()\n{\n   //optional low level setup server config stuff here.\n   //server.config is an ESP-IDF httpd_config struct\n   //see: https://docs.espressif.com/projects/esp-idf/en/v4.4.6/esp32/api-reference/protocols/esp_http_server.html#_CPPv412httpd_config\n   //increase maximum number of uri endpoint handlers (.on() calls)\n   server.config.max_uri_handlers = 20; \n\n   //connect to wifi\n\n   //start the server listening on port 80 (standard HTTP port)\n   server.listen(80);\n\n   //call server methods to attach endpoints and handlers\n   server.on(...);\n   server.serveStatic(...);\n   server.attachHandler(...);\n}\n```\n\n## Add Handlers\n\nOne major difference from ESPAsyncWebserver is that handlers can be attached to a specific url (endpoint) or as a global handler.  The reason for this, is that attaching to a specific URL is more efficient and makes for cleaner code.\n\n### Endpoint Handlers\n\nAn endpoint is basically just the URL path (eg. /path/to/file) without any query string.  The ```server.on(...)``` function is a convenience function for creating endpoints and attaching a handler to them.  There are two main styles: attaching a basic ```WebRequest``` handler and attaching an external handler.\n\n```cpp\n//creates a basic PsychicWebHandler that calls the request_callback callback\nserver.on(\"/url\", HTTP_GET, request_callback);\n\n//same as above, but defaults to HTTP_GET\nserver.on(\"/url\", request_callback);\n\n//attaches a websocket handler to /ws\nPsychicWebSocketHandler websocketHandler;\nserver.on(\"/ws\", &websocketHandler);\n```\n\nThe ```server.on(...)``` returns a pointer to the endpoint, which can be used to call various functions like ```setHandler()```, ```setFilter()```, and ```setAuthentication()```.\n\n```cpp\n//respond to /url only from requests to the AP\nserver.on(\"/url\", HTTP_GET, request_callback)->setFilter(ON_AP_FILTER);\n\n//require authentication on /url\nserver.on(\"/url\", HTTP_GET, request_callback)->setAuthentication(\"user\", \"pass\");\n\n//attach websocket handler to /ws\nPsychicWebSocketHandler websocketHandler;\nserver.on(\"/ws\")->attachHandler(&websocketHandler);\n```\n\n### Basic Requests\n\nThe ```PsychicWebHandler``` class is for handling standard web requests.  It provides a single callback: ```onRequest()```.  This callback is called when the handler receives a valid HTTP request.\n\nOne major difference from ESPAsyncWebserver is that this callback needs to return an esp_err_t variable to let the server know the result of processing the request.  The ```response->reply()``` and ```request->send()``` functions will return this.  It is a good habit to return the result of these functions as sending the response will close the connection.\n\nThe function definition for the onRequest callback is:\n\n```cpp\nesp_err_t function_name(PsychicRequest *request);\n```\n\nHere is a simple example that sends back the client's IP on the URL /ip\n\n```cpp\nserver.on(\"/ip\", [](PsychicRequest *request)\n{\n   String output = \"Your IP is: \" + request->client()->remoteIP().toString();\n   return request->reply(output.c_str());\n});\n```\n\n### Uploads\n\nThe ```PsychicUploadHandler``` class is for handling uploads, both large POST bodies and multipart encoded forms.  It provides two callbacks: ```onUpload()``` and ```onRequest()```.\n\n```onUpload(...)``` is called when there is new data.  This function may be called multiple times so that you can process the data in chunks. The function definition for the onUpload callback is:\n\n```cpp\nesp_err_t function_name(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final);\n```\n\n* request is a pointer to the Request object\n* filename is the name of the uploaded file\n* index is the overall byte position of the current data\n* data is a pointer to the data buffer\n* len is the length of the data buffer\n* final is a flag to tell if its the last chunk of data\n\n```onRequest(...)``` is called after the successful handling of the upload.  Its definition and usage is the same as the basic request example as above.\n\n#### Basic Upload (file is the entire POST body)\n\nIt's worth noting that there is no standard way of passing in a filename for this method, so the handler attempts to guess the filename with the following methods:\n\n* Checking the Content-Disposition header\n* Checking the _filename query parameter (eg. /upload?filename=filename.txt becomes filename.txt)\n* Checking the url and taking the last part as filename (eg. /upload/filename.txt becomes filename.txt).  You must set a wildcard url for this to work as in the example below.\n\n```cpp\n//handle a very basic upload as post body\n PsychicUploadHandler *uploadHandler = new PsychicUploadHandler();\n uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) {\n   File file;\n   String path = \"/www/\" + filename;\n\n   Serial.printf(\"Writing %d/%d bytes to: %s\\n\", (int)index+(int)len, request->contentLength(), path.c_str());\n\n   if (last)\n     Serial.printf(\"%s is finished. Total bytes: %d\\n\", path.c_str(), (int)index+(int)len);\n\n   //our first call?\n   if (!index)\n     file = LittleFS.open(path, FILE_WRITE);\n   else\n     file = LittleFS.open(path, FILE_APPEND);\n   \n   if(!file) {\n     Serial.println(\"Failed to open file\");\n     return ESP_FAIL;\n   }\n\n   if(!file.write(data, len)) {\n     Serial.println(\"Write failed\");\n     return ESP_FAIL;\n   }\n\n   return ESP_OK;\n });\n\n //gets called after upload has been handled\n uploadHandler->onRequest([](PsychicRequest *request)\n {\n   String url = \"/\" + request->getFilename();\n   String output = \"<a href=\\\"\" + url + \"\\\">\" + url + \"</a>\";\n\n   return request->reply(output.c_str());\n });\n\n //wildcard basic file upload - POST to /upload/filename.ext\n server.on(\"/upload/*\", HTTP_POST, uploadHandler);\n```\n\n#### Multipart Upload\n\nVery similar to the basic upload, with 2 key differences:\n\n* multipart requests don't know the total size of the file until after it has been fully processed.  You can get a rough idea with request->contentLength(), but that is the length of the entire multipart encoded request.\n* you can access form variables, including multipart file infor (name + size) in the onRequest handler using request->getParam()\n\n```cpp\n //a little bit more complicated multipart form\n PsychicUploadHandler *multipartHandler = new PsychicUploadHandler();\n multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) {\n   File file;\n   String path = \"/www/\" + filename;\n\n   //some progress over serial.\n   Serial.printf(\"Writing %d bytes to: %s\\n\", (int)len, path.c_str());\n   if (last)\n     Serial.printf(\"%s is finished. Total bytes: %d\\n\", path.c_str(), (int)index+(int)len);\n\n   //our first call?\n   if (!index)\n     file = LittleFS.open(path, FILE_WRITE);\n   else\n     file = LittleFS.open(path, FILE_APPEND);\n   \n   if(!file) {\n     Serial.println(\"Failed to open file\");\n     return ESP_FAIL;\n   }\n\n   if(!file.write(data, len)) {\n     Serial.println(\"Write failed\");\n     return ESP_FAIL;\n   }\n\n   return ESP_OK;\n });\n\n //gets called after upload has been handled\n multipartHandler->onRequest([](PsychicRequest *request)\n {\n   PsychicWebParameter *file = request->getParam(\"file_upload\");\n\n   String url = \"/\" + file->value();\n   String output;\n\n   output += \"<a href=\\\"\" + url + \"\\\">\" + url + \"</a><br/>\\n\";\n   output += \"Bytes: \" + String(file->size()) + \"<br/>\\n\";\n   output += \"Param 1: \" + request->getParam(\"param1\")->value() + \"<br/>\\n\";\n   output += \"Param 2: \" + request->getParam(\"param2\")->value() + \"<br/>\\n\";\n   \n   return request->reply(output.c_str());\n });\n\n //upload to /multipart url\n server.on(\"/multipart\", HTTP_POST, multipartHandler);\n```\n\n### Static File Serving\n\nThe ```PsychicStaticFileHandler``` is a special handler that does not provide any callbacks.  It is used to serve a file or files from a specific directory in a filesystem to a directory on the webserver.  The syntax is exactly the same as ESPAsyncWebserver. Anything that is derived from the ```FS``` class should work (eg. SPIFFS, LittleFS, SD, etc)\n\nA couple important notes:\n\n* If it finds a file with an extra .gz extension, it will serve it as gzip encoded (eg: /targetfile.ext -> {targetfile.ext}.gz)\n* If the file is larger than FILE_CHUNK_SIZE (default 8kb) then it will send it as a chunked response.\n* It will detect most basic filetypes and automatically set the appropriate Content-Type\n\nThe ```server.serveStatic()``` function handles creating the handler and assigning it to the server:\n\n```cpp\n//serve static files from LittleFS/www on / only to clients on same wifi network\n//this is where our /index.html file lives\nserver.serveStatic(\"/\", LittleFS, \"/www/\")->setFilter(ON_STA_FILTER);\n\n//serve static files from LittleFS/www-ap on / only to clients on SoftAP\n//this is where our /index.html file lives\nserver.serveStatic(\"/\", LittleFS, \"/www-ap/\")->setFilter(ON_AP_FILTER);\n\n//serve static files from LittleFS/img on /img\n//it's more efficient to serve everything from a single www directory, but this is also possible.\nserver.serveStatic(\"/img\", LittleFS, \"/img/\");\n\n//you can also serve single files\nserver.serveStatic(\"/myfile.txt\", LittleFS, \"/custom.txt\");\n```\n\nYou could also theoretically use the file response directly:\n\n```cpp\nserver.on(\"/ip\", [](PsychicRequest *request)\n{\n   String filename = \"/path/to/file\";\n   PsychicFileResponse response(request, LittleFS, filename);\n\n   return response.send();\n});\nPsychicFileResponse(PsychicRequest *request, FS &fs, const String& path)\n```\n\n### Websockets\n\nThe ```PsychicWebSocketHandler``` class is for handling WebSocket connections.  It provides 3 callbacks:\n\n```onOpen(...)``` is called when a new WebSocket client connects.\n```onFrame(...)``` is called when a new WebSocket frame has arrived.\n```onClose(...)``` is called when a new WebSocket client disconnects.\n\nHere are the callback definitions:\n\n```cpp\nvoid open_function(PsychicWebSocketClient *client);\nesp_err_t frame_function(PsychicWebSocketRequest *request, httpd_ws_frame *frame);\nvoid close_function(PsychicWebSocketClient *client);\n```\n\nWebSockets were the main reason for starting PsychicHttp, so they are well tested.  They are also much simplified from the ESPAsyncWebserver style.  You do not need to worry about error handling, partial frame assembly, PONG messages, etc.  The onFrame() function is called when a complete frame has been received, and can handle frames up to the entire available heap size.\n\nHere is a basic example of using WebSockets:\n\n```cpp\n //create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed.\n PsychicWebSocketHandler websocketHandler();\n\n websocketHandler.onOpen([](PsychicWebSocketClient *client) {\n   Serial.printf(\"[socket] connection #%u connected from %s\\n\", client->socket(), client->remoteIP().toString());\n   client->sendMessage(\"Hello!\");\n });\n\n websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {\n     Serial.printf(\"[socket] #%d sent: %s\\n\", request->client()->socket(), (char *)frame->payload);\n     return request->reply(frame);\n });\n\n websocketHandler.onClose([](PsychicWebSocketClient *client) {\n   Serial.printf(\"[socket] connection #%u closed from %s\\n\", client->socket(), client->remoteIP().toString());\n });\n\n //attach the handler to /ws.  You can then connect to ws://ip.address/ws\n server.on(\"/ws\", &websocketHandler);\n```\n\nThe onFrame() callback has 2 parameters:\n\n* ```PsychicWebSocketRequest *request``` a special request with helper functions for replying in websocket format.\n* ```httpd_ws_frame *frame``` ESP-IDF websocket struct.  The important struct members we care about are:\n   * ```uint8_t *payload; /*!< Pre-allocated data buffer */```\n   * ```size_t len; /*!< Length of the WebSocket data */```\n \nFor sending data on the websocket connection, there are 3 methods:\n\n* ```request->reply()``` - only available in the onFrame() callback context.\n* ```webSocketHandler.sendAll()``` - can be used anywhere to send websocket messages to all connected clients.\n* ```client->send()``` - can be used anywhere* to send a websocket message to a specific client\n\nAll of the above functions either accept simple ```char *``` string of you can construct your own httpd_ws_frame.\n\n*Special Note:*  Do not hold on to the ```PsychicWebSocketClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects.  Instead, store the ```int client->socket()```.  Then when you want to send a message, use this code:\n\n```cpp\n//make sure our client is still connected.\nPsychicWebSocketClient *client = websocketHandler.getClient(socket);\nif (client != NULL)\n  client->send(\"Your Message\")\n```\n\n### EventSource / SSE\n\nThe ```PsychicEventSource``` class is for handling EventSource / SSE connections.  It provides 2 callbacks:\n\n```onOpen(...)``` is called when a new EventSource client connects.\n```onClose(...)``` is called when a new EventSource client disconnects.\n\nHere are the callback definitions:\n\n```cpp\nvoid open_function(PsychicEventSourceClient *client);\nvoid close_function(PsychicEventSourceClient *client);\n```\n\nHere is a basic example of using PsychicEventSource:\n\n```cpp\n //create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed.\n PsychicEventSource eventSource;\n\n eventSource.onOpen([](PsychicEventSourceClient *client) {\n   Serial.printf(\"[eventsource] connection #%u connected from %s\\n\", client->socket(), client->remoteIP().toString());\n   client->send(\"Hello user!\", NULL, millis(), 1000);\n });\n\n eventSource.onClose([](PsychicEventSourceClient *client) {\n   Serial.printf(\"[eventsource] connection #%u closed from %s\\n\", client->socket(), client->remoteIP().toString());\n });\n\n //attach the handler to /events\n server.on(\"/events\", &eventSource);\n```\n\nFor sending data on the EventSource connection, there are 2 methods:\n\n* ```eventSource.send()``` - can be used anywhere to send events to all connected clients.\n* ```client->send()``` - can be used anywhere* to send events to a specific client\n\nAll of the above functions accept a simple ```char *``` message, and optionally: ```char *``` event name, id, and reconnect time.\n\n*Special Note:*  Do not hold on to the ```PsychicEventSourceClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects.  Instead, store the ```int client->socket()```.  Then when you want to send a message, use this code:\n\n```cpp\n//make sure our client is still connected.\nPsychicEventSourceClient *client = eventSource.getClient(socket);\nif (client != NULL)\n  client->send(\"Your Event\")\n```\n\n### HTTPS / SSL\n\nPsychicHttp supports HTTPS / SSL out of the box, however there are some limitations (see performance below).  Enabling it also increases the code size by about 100kb.  To use HTTPS, you need to modify your setup like so:\n\n```cpp\n#include <PsychicHttp.h>\n#include <PsychicHttpsServer.h>\nPsychicHttpsServer server;\nserver.listen(443, server_cert, server_key);\n```\n\n```server_cert``` and ```server_key``` are both ```const char *``` parameters which contain the server certificate and private key, respectively.\n\nTo generate your own key and self signed certificate, you can use the command below:\n\n```\nopenssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -sha256 -days 365\n```\n\nIncluding the ```PsychicHttpsServer.h``` also defines ```PSY_ENABLE_SSL``` which you can use in your code to allow enabling / disabling calls in your code based on if the HTTPS server is available:\n\n```cpp\n//our main server object\n#ifdef PSY_ENABLE_SSL\n  PsychicHttpsServer server;\n#else\n  PsychicHttpServer server;\n#endif\n```\n\nLast, but not least, you can create a separate HTTP server on port 80 that redirects all requests to the HTTPS server:\n\n```cpp\n//this creates a 2nd server listening on port 80 and redirects all requests HTTPS\nPsychicHttpServer *redirectServer = new PsychicHttpServer();\nredirectServer->config.ctrl_port = 20420; // just a random port different from the default one\nredirectServer->listen(80);\nredirectServer->onNotFound([](PsychicRequest *request) {\n   String url = \"https://\" + request->host() + request->url();\n   return request->redirect(url.c_str());\n});\n```\n\n# TemplatePrinter\n\n**This is not specific to PsychicHttp, and it works with any `Print` object. You could for example, template data out to `File`, `Serial`, etc...**.\n\nThe template engine is a `Print` interface and can be printed to directly, however,  if you are just templating a few short strings, I'd probably just use `response.printf()` instead. **Its benefit will be seen when templating large inputs such as files.**\n\nOne benefit may be **templating a **JSON** file avoiding the need to use ArduinoJson.**\n\nBefore closing the underlying `Print`/`Stream` that this writes to, it must be flushed as small amounts of data can be buffered. A convenience method to take care of this is shows in `example 3`.\n\nThe header file is not currently added to `PsychicHttp.h` and users will have to add it manually:\n\n```C++\n#include <TemplatePrinter.h>\n```\n \n## Template parameter definition:\n\n- Must start and end with a preset delimiter, the default is `%`\n- Can only contain `a-z`, `A-Z`, `0-9`, and `_`\n- Maximum length of 63 characters (buffer is 64 including `null`).\n- A parameter must not be zero length (not including delimiters).\n- Spaces or any other character do not match as a parameter, and will be output as is.\n- Valid examples\n  - `%MY_PARAM%`\n  - `%SOME1%`\n- **Invalid** examples\n  - `%MY PARAM%`\n  - `%SOME1 %`\n  - `%UNFINISHED`\n  - `%%`\n\n## Template processing\nA function or lambda is used to receive the parameter replacement.\n\n```C++\nbool templateHandler(Print &output, const char *param){\n  //...\n}\n\n[](Print &output, const char *param){\n  //...\n}\n```\n\nParameters:\n- `Print &output` - the underlying `Print`, print the results of templating to this.\n- `const char *param` - a string containing the current parameter.\n\nThe handler must return a `bool`.\n- `true`: the parameter was handled, continue as normal.\n- `false`: the input detected as a parameter is not, print literal.\n\nSee output in **example 1** regarding the effects of returning `true` or `false`.\n\n## Template input handler\nThis is not needed unless using the static convenience function `TemplatePrinter::start()`. See **example 3**.\n\n```C++\nbool inputHandler(TemplatePrinter &printer){\n  //...\n}\n\n[](TemplatePrinter &printer){\n  //...\n}\n```\n\nParameters:\n- `TemplatePrinter &printer` - The template engine, print your template text to this for processing.\n\n\n## Example 1 - Simple use with `PsychicStreamResponse`:\nThis example highlights its most basic usage.\n\n```C++\n\n//  Function to handle parameter requests.\n\nbool templateHandler(Print &output, const char *param){\n\n  if(strcmp(param, \"FREE_HEAP\") == 0){\n    output.print((double)ESP.getFreeHeap() / 1024.0, 2);\n\n  }else if(strcmp(param, \"MIN_FREE_HEAP\") == 0){\n    output.print((double)ESP.getMinFreeHeap() / 1024.0, 2);\n\n  }else if(strcmp(param, \"MAX_ALLOC_HEAP\") == 0){\n    output.print((double)ESP.getMaxAllocHeap() / 1024.0, 2);\n    \n  }else if(strcmp(param, \"HEAP_SIZE\") == 0){\n    output.print((double)ESP.getHeapSize() / 1024.0, 2);\n  }else{\n    return false;\n  }\n  output.print(\"Kb\");\n  return true;\n}\n\n//  Example serving a request\nserver.on(\"/template\", [](PsychicRequest *request) {\n  PsychicStreamResponse response(request, \"text/plain\");\n\n  response.beginSend();\n  \n  TemplatePrinter printer(response, templateHandler);\n\n  printer.println(\"My ESP has %FREE_HEAP% left. Its lifetime minimum heap is %MIN_FREE_HEAP%.\");\n  printer.println(\"The maximum allocation size is %MAX_ALLOC_HEAP%, and its total size is %HEAP_SIZE%.\");\n  printer.println(\"This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%.\");\n  printer.println(\"This line finished with %UNFIN\");\n  printer.flush();\n\n  return response.endSend();\n});   \n```\n\nThe output for example looks like:\n```\nMy ESP has 170.92Kb left. Its lifetime minimum heap is 169.83Kb.\nThe maximum allocation size is 107.99Kb, and its total size is 284.19Kb.\nThis is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%.\nThis line finished with %UNFIN\n```\n\n## Example 2 - Templating a file\n\n```C++\nserver.on(\"/home\", [](PsychicRequest *request) {\n  PsychicStreamResponse response(request, \"text/html\");\n  File file = SD.open(\"/www/index.html\");\n\n  response.beginSend();\n\n  TemplatePrinter printer(response, templateHandler);\n\n  printer.copyFrom(file);\n  printer.flush();\n  file.close();\n\n  return response.endSend();\n}); \n```\n\n## Example 3 - Using the `TemplatePrinter::start` method.\nThis static method allows an RAII approach, allowing you to template a stream, etc... without needing a `flush()`. The function call is laid out as:\n\n```C++\nTemplatePrinter::start(host_stream, template_handler, input_handler);\n```\n\n\\*these examples use the `templateHandler` function defined in example 1.\n\n### Serve a file like example 2\n```C++\nserver.on(\"/home\", [](PsychicRequest *request) {\n  PsychicStreamResponse response(request, \"text/html\");\n  File file = SD.open(\"/www/index.html\");\n\n  response.beginSend();\n  TemplatePrinter::start(response, templateHandler, [&file](TemplatePrinter &printer){\n    printer.copyFrom(file);\n  });\n  file.close();\n\n  return response.endSend();\n});\n```\n\n### Template a string like example 1\n```C++\nserver.on(\"/template2\", [](PsychicRequest *request) {\n\n  PsychicStreamResponse response(request, \"text/plain\");\n\n  response.beginSend();\n\n  TemplatePrinter::start(response, templateHandler, [](TemplatePrinter &printer){\n    printer.println(\"My ESP has %FREE_HEAP% left. Its lifetime minimum heap is %MIN_FREE_HEAP%.\");\n    printer.println(\"The maximum allocation size is %MAX_ALLOC_HEAP%, and its total size is %HEAP_SIZE%.\");\n    printer.println(\"This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%.\");\n  });\n\n  return response.endSend();\n});\n```\n\n# Performance\n\nIn order to really see the differences between libraries, I created some basic benchmark firmwares for PsychicHttp, ESPAsyncWebserver, and ArduinoMongoose.  I then ran the loadtest-http.sh and loadtest-websocket.sh scripts against each firmware to get some real numbers on the performance of each server library.  All of the code and results are available in the /benchmark folder.  If you want to see the collated data and graphs, there is a [LibreOffice spreadsheet](/benchmark/comparison.ods).\n\n![Performance graph](/benchmark/performance.png)\n![Latency graph](/benchmark/latency.png)\n\n## HTTPS / SSL\n\nYes, PsychicHttp supports SSL out of the box, but there are a few caveats:\n\n* Due to memory limitations, it can only handle 2 connections at a time. Each SSL connection takes about 45k ram, and a blank PsychicHttp sketch has about 150k ram free.\n* Speed and latency are still pretty good (see graph above) but the SSH handshake seems to take 1500ms.  With websockets or browser its not an issue since the connection is kept alive, but if you are loading requests in another way it will be a bit slow\n* Unless you want to expose your ESP to the internet, you are limited to self signed keys and the annoying browser security warnings that come with them.\n\n## Analysis\n\nThe results clearly show some of the reasons for writing PsychicHttp: ESPAsyncWebserver crashes under heavy load on each test, across the board in a 60s test.  That means in normal usage, you're just rolling the dice with how long it will go until it crashes.  Every other number is moot, IMHO.\n\nArduinoMongoose doesn't crash under heavy load, but it does bog down with extremely high latency (15s) for web requests and appears to not even respond at the highest loadings as the loadtest script crashes instead.  The code itself doesnt crash, so bonus points there.  After the high load, it does go back to serving normally.  One area ArduinoMongoose does shine, is in websockets where its performance is almost 2x the performance of PsychicHttp.  Both in requests per second and latency.  Clearly an area of improvement for PsychicHttp.\n\nPsychicHttp has good performance across the board.  No crashes and continously responds during each test.  It is a clear winner in requests per second when serving files from memory, dynamic JSON, and has consistent performance when serving files from LittleFS. The only real downside is the lower performance of the websockets with a single connection handling 38rps, and maxing out at 120rps across multiple connections.\n\n## Takeaways\n\nWith all due respect to @me-no-dev who has done some amazing work in the open source community, I cannot recommend anyone use the ESPAsyncWebserver for anything other than simple projects that don't need to be reliable.  Even then, PsychicHttp has taken the arcane api of the ESP-IDF web server library and made it nice and friendly to use with a very similar API to ESPAsyncWebserver.  Also, ESPAsyncWebserver is more or less abandoned, with 150 open issues, 77 pending pull requests, and the last commit in over 2 years.\n\nArduinoMongoose is a good alternative, although the latency issues when it gets fully loaded can be very annoying. I believe it is also cross platform to other microcontrollers as well, but I haven't tested that. The other issue here is that it is based on an old version of a modified Mongoose library that will be difficult to update as it is a major revision behind and several security updates behind as well.  Big thanks to @jeremypoulter though as PsychicHttp is a fork of ArduinoMongoose so it's built on strong bones.\n\n# Roadmap\n\n## v1.2: ESPAsyncWebserver Parity\n\n\nChange:\nModify the request handling to bring initail url matching and filtering into PsychicHttpServer itself.\n\nBenefits: \n* Fix a bug with filter() where endpoint is matched, but filter fails and it doesn't continue matching further endpoints (checks are in different codebases)\n* HTTP_ANY support\n* unlimited endpoints\n  * we would use a List to store endpoints\n  * dont have to pre-declare config.max_uri_handlers;\n* much more flexibility for future\n\nIssues\n* it would log a warning on every request as if its a 404. (httpd_uri.c:298)\n* req->user_ctx is not passed in. (httpd_uri.c:309)\n    * but... user_ctx is something we could store in the psychicendpoint data\n  * Websocket support assumes an endpoint with matching url / method (httpd_uri.c:312)\n    * we could copy and bring this code into our own internal request processor\n  * would need to manually maintain more code (~100 lines?) and be more prone to esp-idf http_server updates causing problems.\n\nHow to implement\n* set config.max_uri_handlers = 1;\n* possibly do not register any uri_handlers (looks like it would be fastest way to exit httpd_find_uri_handler (httpd_uri.c:94))\n  * looks like 404 is set by default, so should work.\n* modify PsychicEndpoint to store the stuff we would pass to http_server\n* create a new function handleRequest() before PsychicHttpServer::defaultNotFoundHandler to process incoming requests.\n  * bring in code from PsychicHttpServer::notFoundHandler\n  * add new code to loop over endpoints to call match and filter\n* bring code from esp-idf library\n\n* templating system\n* regex url matching\n* rewrite urls?\n* What else are we missing?\n\n\n## Longterm Wants\n\n* investigate websocket performance gap\n* support for esp-idf framework\n* support for arduino 3.0 framework\n* Enable worker based multithreading with esp-idf v5.x\n* 100-continue support?\n     \nIf anyone wants to take a crack at implementing any of the above features I am more than happy to accept pull requests.\n"
  },
  {
    "path": "lib/PsychicHttp/RELEASE.md",
    "content": "* Update CHANGELOG\n* Bump version in library.json\n* Bump version in library.properties\n* Make new release + tag\n\t* this will get pulled in automatically by Arduino Library Indexer\n* run ```pio pkg publish``` to publish to Platform.io"
  },
  {
    "path": "lib/PsychicHttp/library.json",
    "content": "{\n  \"name\": \"PsychicHttp\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Arduino style wrapper around ESP-IDF HTTP library. HTTP server with SSL + websockets. Works on esp32 and probably esp8266\",\n  \"keywords\": \"network,http,https,tcp,ssl,tls,websocket,espasyncwebserver\",\n  \"repository\":\n  {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/hoeken/PsychicHttp\"\n  },\n  \"authors\":\n  [\n    {\n      \"name\": \"Zach Hoeken\",\n      \"email\": \"hoeken@gmail.com\",\n      \"maintainer\": true\n    }\n  ],\n  \"license\" : \"MIT\",\n  \"examples\": [\n    {\n      \"name\": \"platformio\",\n      \"base\": \"examples/platformio\",\n      \"files\": [\n        \"src/main.cpp\"\n      ]\n    }\n  ],\n  \"frameworks\": \"arduino\",\n  \"platforms\": \"espressif32\",\n  \"dependencies\": [\n    {\n      \"owner\": \"bblanchon\",\n      \"name\": \"ArduinoJson\",\n      \"version\": \"^7.0.4\"\n    },\n    {\n      \"owner\": \"plageoj\",\n      \"name\" : \"UrlEncode\",\n      \"version\" : \"^1.0.1\"\n    }\n  ],\n  \"export\": {\n    \"include\": [\n      \"examples/platformio\",\n      \"src\",\n      \"library.json\",\n      \"library.properties\",\n      \"LICENSE\",\n      \"README.md\"\n    ]\n  }\n}\n"
  },
  {
    "path": "lib/PsychicHttp/request flow.drawio",
    "content": "<mxfile host=\"Electron\" modified=\"2023-12-09T15:02:09.427Z\" agent=\"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.8.10 Chrome/106.0.5249.199 Electron/21.3.5 Safari/537.36\" etag=\"cOtONeHkqmkM2fyBsENB\" version=\"20.8.10\" type=\"device\" pages=\"2\"><diagram id=\"C5RBs43oDa-KdzZeNtuy\" name=\"Request Flow\">7Vxbd5s4EP41Pmf3wTkgAcaPza3Zbq/bbNM+yiAbNhi5ICd2f/0KIxmQFIypsZ1jpw+1hBCS5pv5ZgaJHryaLt4maBZ8ID6OesDwFz143QPANIDD/stqlnnNwIJ5xSQJfd6oqPga/sLiTl47D32cVhpSQiIazqqVHolj7NFKHUoS8lxtNiZR9akzNMFKxVcPRWrtQ+jTIK91waCov8PhJBBPNp1hfmWKRGM+kzRAPnkuVcGbHrxKCKH5r+niCkfZ4ol1efhr+RC9f3TevvuS/kT/Xv59//FbP+/sdptb1lNIcExbd+0/B97S/uJ9MrxvX76P0/f9+Gff5MvwhKI5XzA+WboUK5iQeezjrBezBy+fg5DirzPkZVefGWZYXUCnEb+8XiSDFcYkphwRbL3ZRXZbGE9Y0c6uhlF0RSKSrB4Dx3b2j99Vqs//srtpQh5x6Yqz+mNXGq4PX8cnnFC8KKGDr9dbTKaYJkvWRFwVIObYN21efi6QZA95XVBCkevwSsTRO1n3XUiI/eBC0gvsevkufbie3t/Yd9blDfz0bvHQ70NzG4EZVeH8hyldcoGgOSWsiiQ0IBMSo+g9ITPeriQ3Myvj2H+TqSIrjyLiPeZVt2E29NUzWIm3d9eCEtqmoCJCIxxdIu9xshqoEGhMYpx15TP15XMpBndT1LKHsYX8nnV2YYvij/VAWOF6USktRWkR0tJtrPSDDy/7XdyUFcQ9KUUJFXPnI9wObSmZJx6uUUEuP/agCaZ1kueQyhaoFrwJjhANn6r2TwdEfutnErKJrEEPbFgBPRyCahf5jPhdZYOzoSPbkDrKp6x0tNKL9XwaqYpWAODkNaU9UrXtoIpUbTtn10BtajHrRl2Cwd39/WdW8w/+Occp/T2+KwOAzfFyEqE0XRuPWulK/Ocj7I49Lc95Lh6Nu+Q5t6qpQGhqiedMoOE5awc0p52CpQjtB3MiT0x92xLdjtXeaaj2ptlQ7znq+sYFgIZbQV7f5N53WxITTch4nOJ6foKGdVB+shWMfyQKxDej5FUrQTdQ3YhA4c8b+wObVTWxljHYK9gcBWw3sT/jY/yAqBdkELpVTWxApqN52iLwu0XTMMpme4ejJ0xDD2noEkXhJGYFj8kfJ3o0rYNGpyjdr9DLaEINI8dj4Glp1HdGjt1puDiUPGdnoNKooQsXu6JRU40WP6dLLwi9OxT7EVvxs/MjOz+2uUfnRxvpaaR2it5Pi3B9/x6TXoCamH5XLpMNoC25TPtjMcuQWAxILNY2N2ANm9EhwydalpqtGCzVTErQrmSSLRfUj0um6Wp79iMfQVturoVLSd9XjJyxHnAiBqDLEbPUzoSuoOqgaWaH41Ga/ScM+Zm7f4sFZP9sr9ytR4WauLjGYzSPMrgWztvJ87cle12udWE3ZHC7K9mp+YvTi+0KMq5QccHMOyHjjXlzoUdHGBRam4LCpnQKZTo1O4su9Wus5jLOHuur8lib5vZbeKyOpST5Duix2m1VTNJVW3Z9d+Sx2rIqH6XHqqaTPuJnVnGV7+IISax1SA/jnBwyJSS/WXEbJhdAZ26luuMjxQmb0QWJP81wnAkRRdGImcFjkZ9vY9e3dPJzwQh2ugMEwqrpAoODJ4eGivxUx7KgRS9b9dCriqbKw4K2zBJpFRS2iba69O0205bm3VQt6jfSVnlnj0as9m8y1xpWktEWsNqalVw5ISN11LHjB9QgtUQBzJREJH3hxcLJc4Fra1IMe+UCoIapQZ5OYmSwEp2af1qTxQvXBXnUef8nwx6Sb+ZCt2lqQnZTdydzNVQ7Xf4AmrCnVlOOlD9Mw2ob1khGadgwEb8rAhG7GktwvA3jMA2OxXyMXQ/rCWPk2pa9PSjbE8YQNCSMzpxPCGoIg0cPPaC8o0hn2AvHzIoAwzvTw4v0MBCm+HDyVd25kyAH7cZstyE3CKU4Fm4wq6hybPNiWP5rSRTWVt3ujja0smmA0y6Tzes3HK9ub2ibHHV7vapNIZcVq+5kROcHHuTMrt0yGJc7GkCwV6VQXSnGdRQnf/yZ0fI5Bte4VPY+87H6E3iK0DwU55s6znJ7UW4OPLTcVE+YcUnAph16iJ5FVyO6g6ucmvVK8qNAbwoJhiTOZHgc0sMmi1IGOukNnQFE+4xRtNKzzD1K70TzV03PjtYh/kgjlIHV0uFSAmi5o44dLleBYp4N4ScLzwZEY/4HB2du9QVqgtMZiVN8kTLLcZaaRmpu00MR3fnJanRzULu/r+1adfZ8o90fHrfdbxtoy9sdlY52tRdLvPYRzg//FMJL45LbD7bcuzXY8d4tvSKpkcvpKpKhKlLtEY1j1ST56HJTTbLEkQaxed6W7PeONMmxqs8RX815aVxyewjr28vnfKT2HWnSkb0tOagmNd6Of9ya5LTXJAmBg272B8uaIcqvW5PUlIw4p0zprFf6VotxGzFlkrWMebW0qldVl5m/Vin717xKOcSW+cihh6I3/MI09P3VGxed21/VXuVzZ8q30PiIre10r7nbDiTh6T7kAsUOj7I2we3ddlYsvn2XA6H4giC8+R8=</diagram><diagram name=\"Standard Handlers\" id=\"NiY0WBS-fA4Nieu9lBZ8\">7Vtbc5s4FP41ntk+tGMQAvyYOLdNp9NOvTttn3ZkkDGNQA7It/z6FUayBcK3xNhMA34I5+iCdL5P5xwJ0gH9aHGfoMn4C/Ux6Zhdf9EBNx3TNLqmzf9kmmWucSyQK4Ik9EWljWIQvmDZUminoY/TQkVGKWHhpKj0aBxjjxV0KEnovFhtREnxqRMUYE0x8BDRtT9Cn41zrWs6G/0DDoOxfLJh9/KSCMnKYibpGPl0rqjAbQf0E0pZfhct+phkxpN2ue69/J493t0Yzy/92Xf/3v3nKviYd3Z3TJP1FBIcs9N2beZdzxCZCnuJubKlNGBCp7GPs06MDriej0OGBxPkZaVzThmuG7OIiOK1jbpcGNGYCUJwc/NC3iyMAy7CrDQkpE8JTVaPASOY/UQrRZ9fWWuW0CeslNiri5dEdIaGq+FmQ0hwGr6oMmWIKTInOFZl7IeqKHioaA60vkBphhOGFwr3BBr3mEaYJUteRZTyueVNxMoCUBBtvuEpsIVurHDUcoQSibURrPve4M9vBAWOoENPo8MPPHxAsU9w8jZiKFwwuE2uA4LSVPAkR1auTp1GRaL4CLsjr5IQnouHozoRc7sFxCwANcQMswqxugCTA1AQE3Clq2HxUZndPiJkiLynVIOQz5uVFnDBqDGNcQkBoUIkDGIuetzEnBvgOrNiyN3ulSiIQt/PHlNJjA11yn4CVjoRMWKrNmANWFyKbsVS7Dk6sOAEwBqfH8jEnz7+9yv4m4HH5+gruhe+XMWVxt/x8xSn7K8PjVmKELu+VbUUXXMIVr65LsQsGcDlUrQOXIpmXYgZGmIaTDj2r7KUJls3mdFDr4hMcVlwiyXLn6rwKxM+QSneLNTCm6WUFiH7KYMbv1dacWnTKBNkm+NgSuk08fD+OMJQEmC2w2YCC+wXcjgddAVUWIGp1CWYIBbOiplfFdDiCd9oyKercKrk3iEsdpHPW7RSU61yR3axI2Bbn3rqVew2N5PW7YqFayO8nph6jseD+oB6T5i1oX1baLfNM4b2SthARQT4OsFx6/4r3L/dvbT7tyrguktQhFu8qvA6NHOuDS89caZxn9C0xasSL/vSeNnvM73aFdH3pldWs9IroxhiofHa9Ao4uzuqOaFymkLF42i1ly6gWXTpljIy60R0sctp/Ra6cADRUqk2ySqkh/NbDnjruMr1jUJ9fpOP4KTcdf9Q7sJ3wl2zodx1zsBd/XT63wmhyG93sdt2sevIeLFdrORJe5B5YKYNL3+Q+U5PMnf5nL3xR/K8KQGodJQJwYmOMi3rNUeZbw1HcviHhiMIzxCODLPCteURqfVsVZ7t4md0hn5IdyHPdpzN93sfs1nep3sq71Pe6dvnSX+P9jfn2LoZ+pHl7YzzZ5DHsKY4nOZkv845P8+ohkw/tWxf4mwNEM6h79zqCxD68Uj7VmAHYBd/K2Dq28s/JKLbjY7ozqs/jQBbKFRzRHfgcRHdAWeI6GZjdtqnZq/7TthbTmybwt63HcdycfNZe159888B4PZ/</diagram></mxfile>"
  },
  {
    "path": "lib/PsychicHttp/src/ChunkPrinter.cpp",
    "content": "\n#include \"ChunkPrinter.h\"\n\nChunkPrinter::ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len) :\n  _response(response),\n  _buffer(buffer),\n  _length(len),\n  _pos(0)\n{}\n\nChunkPrinter::~ChunkPrinter()\n{\n  flush();\n}\n\nsize_t ChunkPrinter::write(uint8_t c)\n{\n  esp_err_t err;\n  \n  //if we're full, send a chunk\n  if (_pos == _length)\n  {\n    _pos = 0;\n    err = _response->sendChunk(_buffer, _length);\n\t\n    if (err != ESP_OK)\n      return 0;\n  }    \n\n  _buffer[_pos] = c;\n  _pos++;\n  return 1;\n}\n\nsize_t ChunkPrinter::write(const uint8_t *buffer, size_t size)\n{\n  size_t written = 0;\n  \n  while (written < size)\n  {\n    size_t space = _length - _pos;\n    size_t blockSize = std::min(space, size - written);\n    \n    memcpy(_buffer + _pos, buffer + written, blockSize);\n    _pos += blockSize;\n    \n    if (_pos == _length)\n    {\n      _pos = 0;\n\n      if (_response->sendChunk(_buffer, _length) != ESP_OK)\n        return written;\n    }\n    written += blockSize; //Update if sent correctly.\n  }\n  return written;\n}\n\nvoid ChunkPrinter::flush()\n{\n  if (_pos)\n  {\n    _response->sendChunk(_buffer, _pos);\n    _pos = 0;\n  }\n}\n\nsize_t ChunkPrinter::copyFrom(Stream &stream)\n{\n  size_t count = 0;\n\n  while (stream.available()){\n    \n    if (_pos == _length)\n    {\n      _response->sendChunk(_buffer, _length);\n      _pos = 0;\n    }\n    \n    size_t readBytes = stream.readBytes(_buffer + _pos, _length - _pos);\n    _pos += readBytes;\n    count += readBytes;\n  }\n  return count;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/ChunkPrinter.h",
    "content": "#ifndef ChunkPrinter_h\n#define ChunkPrinter_h\n\n#include \"PsychicResponse.h\"\n#include <Print.h>\n\nclass ChunkPrinter : public Print\n{\n  private:\n    PsychicResponse *_response;\n    uint8_t *_buffer;\n    size_t _length;\n    size_t _pos;\n\n  public:\n    ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len);\n    ~ChunkPrinter();\n  \n    size_t write(uint8_t c) override;\n    size_t write(const uint8_t *buffer, size_t size) override;\n\n    size_t copyFrom(Stream &stream);\n\n    void flush() override;\n};\n\n#endif"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicClient.cpp",
    "content": "#include \"PsychicClient.h\"\n#include \"PsychicHttpServer.h\"\n#include <lwip/sockets.h>\n\nPsychicClient::PsychicClient(httpd_handle_t server, int socket) :\n  _server(server),\n  _socket(socket),\n  _friend(NULL),\n  isNew(false)\n{}\n\nPsychicClient::~PsychicClient() {\n}\n\nhttpd_handle_t PsychicClient::server() {\n  return _server;\n}\n\nint PsychicClient::socket() {\n  return _socket;\n}\n\n// I'm not sure this is entirely safe to call.  I was having issues with race conditions when highly loaded using this.\nesp_err_t PsychicClient::close()\n{\n  esp_err_t err = httpd_sess_trigger_close(_server, _socket);\n  //PsychicHttpServer::closeCallback(_server, _socket); // call this immediately so the client is taken off the list.\n\n  return err;\n}\n\nIPAddress PsychicClient::localIP()\n{\n  IPAddress address(0,0,0,0);\n\n  char ipstr[INET6_ADDRSTRLEN];\n  struct sockaddr_in6 addr;   // esp_http_server uses IPv6 addressing\n  socklen_t addr_size = sizeof(addr);\n\n  if (getsockname(_socket, (struct sockaddr *)&addr, &addr_size) < 0) {\n    ESP_LOGE(PH_TAG, \"Error getting client IP\");\n    return address;\n  }\n\n  // Convert to IPv4 string\n  inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));\n  //ESP_LOGD(PH_TAG, \"Client Local IP => %s\", ipstr);\n  address.fromString(ipstr);\n\n  return address;\n}\n\nIPAddress PsychicClient::remoteIP()\n{\n  IPAddress address(0,0,0,0);\n\n  char ipstr[INET6_ADDRSTRLEN];\n  struct sockaddr_in6 addr;   // esp_http_server uses IPv6 addressing\n  socklen_t addr_size = sizeof(addr);\n\n  if (getpeername(_socket, (struct sockaddr *)&addr, &addr_size) < 0) {\n    ESP_LOGE(PH_TAG, \"Error getting client IP\");\n    return address;\n  }\n\n  // Convert to IPv4 string\n  inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));\n  //ESP_LOGD(PH_TAG, \"Client Remote IP => %s\", ipstr);\n  address.fromString(ipstr);\n\n  return address;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicClient.h",
    "content": "#ifndef PsychicClient_h\n#define PsychicClient_h\n\n#include \"PsychicCore.h\"\n\n/*\n* PsychicClient :: Generic wrapper around the ESP-IDF socket\n*/\n\nclass PsychicClient {\n  protected:\n    httpd_handle_t _server;\n    int _socket;\n\n  public:\n    PsychicClient(httpd_handle_t server, int socket);\n    ~PsychicClient();\n\n    //no idea if this is the right way to do it or not, but lets see.\n    //pointer to our derived class (eg. PsychicWebSocketConnection)\n    void *_friend;\n\n    bool isNew = false;\n\n    bool operator==(PsychicClient& rhs) const { return _socket == rhs.socket(); }\n\n    httpd_handle_t server();\n    int socket();\n    esp_err_t close();\n\n    IPAddress localIP();\n    IPAddress remoteIP();\n};\n\n#endif"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicCore.h",
    "content": "#ifndef PsychicCore_h\n#define PsychicCore_h\n\n#define PH_TAG \"🔮\"\n\n// version numbers\n#define PSYCHIC_HTTP_VERSION_MAJOR 1\n#define PSYCHIC_HTTP_VERSION_MINOR 1\n#define PSYCHIC_HTTP_VERSION_PATCH 0\n\n#ifndef MAX_COOKIE_SIZE\n#define MAX_COOKIE_SIZE 512\n#endif\n\n#ifndef FILE_CHUNK_SIZE\n#define FILE_CHUNK_SIZE 8 * 1024\n#endif\n\n#ifndef STREAM_CHUNK_SIZE\n#define STREAM_CHUNK_SIZE 1024\n#endif\n\n#ifndef MAX_UPLOAD_SIZE\n#define MAX_UPLOAD_SIZE (2048 * 1024) // 2MB\n#endif\n\n#ifndef MAX_REQUEST_BODY_SIZE\n#define MAX_REQUEST_BODY_SIZE (16 * 1024) // 16K\n#endif\n\n#ifdef ARDUINO\n#include <Arduino.h>\n#endif\n\n#include <esp_http_server.h>\n#include <map>\n#include <list>\n#include <libb64/cencode.h>\n#include \"esp_random.h\"\n#include \"MD5Builder.h\"\n#include <UrlEncode.h>\n#include \"FS.h\"\n#include <ArduinoJson.h>\n\nenum HTTPAuthMethod\n{\n    BASIC_AUTH,\n    DIGEST_AUTH\n};\n\nString urlDecode(const char *encoded);\n\nclass PsychicHttpServer;\nclass PsychicRequest;\nclass PsychicWebSocketRequest;\nclass PsychicClient;\n\n// filter function definition\ntypedef std::function<bool(PsychicRequest *request)> PsychicRequestFilterFunction;\n\n// client connect callback\ntypedef std::function<void(PsychicClient *client)> PsychicClientCallback;\n\n// callback definitions\ntypedef std::function<esp_err_t(PsychicRequest *request)> PsychicHttpRequestCallback;\ntypedef std::function<esp_err_t(PsychicRequest *request, JsonVariant &json)> PsychicJsonRequestCallback;\n\nstruct HTTPHeader\n{\n    char *field;\n    char *value;\n};\n\nclass DefaultHeaders\n{\n    std::list<HTTPHeader> _headers;\n\npublic:\n    DefaultHeaders() {}\n\n    void addHeader(const String &field, const String &value)\n    {\n        addHeader(field.c_str(), value.c_str());\n    }\n\n    void addHeader(const char *field, const char *value)\n    {\n        HTTPHeader header;\n\n        // these are just going to stick around forever.\n        header.field = (char *)malloc(strlen(field) + 1);\n        header.value = (char *)malloc(strlen(value) + 1);\n\n        strlcpy(header.field, field, strlen(field) + 1);\n        strlcpy(header.value, value, strlen(value) + 1);\n\n        _headers.push_back(header);\n    }\n\n    const std::list<HTTPHeader> &getHeaders() { return _headers; }\n\n    // delete the copy constructor, singleton class\n    DefaultHeaders(DefaultHeaders const &) = delete;\n    DefaultHeaders &operator=(DefaultHeaders const &) = delete;\n\n    // single static class interface\n    static DefaultHeaders &Instance()\n    {\n        static DefaultHeaders instance;\n        return instance;\n    }\n};\n\n#endif // PsychicCore_h\n"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicEndpoint.cpp",
    "content": "#include \"PsychicEndpoint.h\"\n#include \"PsychicHttpServer.h\"\n\nPsychicEndpoint::PsychicEndpoint() :\n  _server(NULL),\n  _uri(\"\"),\n  _method(HTTP_GET),\n  _handler(NULL)\n{\n}\n\nPsychicEndpoint::PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri) :\n  _server(server),\n  _uri(uri),\n  _method(method),\n  _handler(NULL)\n{\n}\n\nPsychicEndpoint * PsychicEndpoint::setHandler(PsychicHandler *handler)\n{\n  //clean up old / default handler\n  if (_handler != NULL)\n    delete _handler;\n\n  //get our new pointer\n  _handler = handler;\n\n  //keep a pointer to the server\n  _handler->_server = _server;\n\n  return this;\n}\n\nPsychicHandler * PsychicEndpoint::handler()\n{\n  return _handler;\n}\n\nString PsychicEndpoint::uri() {\n  return _uri;\n}\n\nesp_err_t PsychicEndpoint::requestCallback(httpd_req_t *req)\n{\n  PsychicEndpoint *self = (PsychicEndpoint *)req->user_ctx;\n  PsychicHandler *handler = self->handler();\n  PsychicRequest request(self->_server, req);\n\n  //make sure we have a handler\n  if (handler != NULL)\n  {\n    if (handler->filter(&request) && handler->canHandle(&request))\n    {\n      //check our credentials\n       if (handler->needsAuthentication(&request))\n        return handler->authenticate(&request);\n\n      //pass it to our handler\n      return handler->handleRequest(&request);\n    }\n    //pass it to our generic handlers\n    else\n      return PsychicHttpServer::notFoundHandler(req, HTTPD_500_INTERNAL_SERVER_ERROR);\n  }\n  else\n    return request.reply(500, \"text/html\", \"No handler registered.\");\n}\n\nPsychicEndpoint* PsychicEndpoint::setFilter(PsychicRequestFilterFunction fn) {\n  _handler->setFilter(fn);\n  return this;\n}\n\nPsychicEndpoint* PsychicEndpoint::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) {\n  _handler->setAuthentication(username, password, method, realm, authFailMsg);\n  return this;\n};"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicEndpoint.h",
    "content": "#ifndef PsychicEndpoint_h\n#define PsychicEndpoint_h\n\n#include \"PsychicCore.h\"\n\nclass PsychicHandler;\n\nclass PsychicEndpoint\n{\n  friend PsychicHttpServer;\n\n  private:\n    PsychicHttpServer *_server;\n    String _uri;\n    http_method _method;\n    PsychicHandler *_handler;\n\n  public:\n    PsychicEndpoint();\n    PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri);\n\n    PsychicEndpoint *setHandler(PsychicHandler *handler);\n    PsychicHandler *handler();\n\n    PsychicEndpoint* setFilter(PsychicRequestFilterFunction fn);\n    PsychicEndpoint* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = \"\", const char *authFailMsg = \"\");\n\n    String uri();\n\n    static esp_err_t requestCallback(httpd_req_t *req);\n};\n\n#endif // PsychicEndpoint_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicEventSource.cpp",
    "content": "/*\n  Asynchronous WebServer library for Espressif MCUs\n\n  Copyright (c) 2016 Hristo Gochkov. All rights reserved.\n\n  This library is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This library is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n*/\n\n#include \"PsychicEventSource.h\"\n\n/*****************************************/\n// PsychicEventSource - Handler\n/*****************************************/\n\nPsychicEventSource::PsychicEventSource() :\n  PsychicHandler(),\n  _onOpen(NULL),\n  _onClose(NULL)\n{}\n\nPsychicEventSource::~PsychicEventSource() {\n}\n\nPsychicEventSourceClient * PsychicEventSource::getClient(int socket)\n{\n  PsychicClient *client = PsychicHandler::getClient(socket);\n\n  if (client == NULL)\n    return NULL;\n\n  return (PsychicEventSourceClient *)client->_friend;\n}\n\nPsychicEventSourceClient * PsychicEventSource::getClient(PsychicClient *client) {\n  return getClient(client->socket());\n}\n\nesp_err_t PsychicEventSource::handleRequest(PsychicRequest *request)\n{\n  //start our open ended HTTP response\n  PsychicEventSourceResponse response(request);\n  esp_err_t err = response.send();\n\n  //lookup our client\n  PsychicClient *client = checkForNewClient(request->client());\n  if (client->isNew)\n  {\n    //did we get our last id?\n    if(request->hasHeader(\"Last-Event-ID\"))\n    {\n      PsychicEventSourceClient *buddy = getClient(client);\n      buddy->_lastId = atoi(request->header(\"Last-Event-ID\").c_str());\n    }\n\n    //let our handler know.\n    openCallback(client);\n  }\n\n  return err;\n}\n\nPsychicEventSource * PsychicEventSource::onOpen(PsychicEventSourceClientCallback fn) {\n  _onOpen = fn;\n  return this;\n}\n\nPsychicEventSource * PsychicEventSource::onClose(PsychicEventSourceClientCallback fn) {\n  _onClose = fn;\n  return this;\n}\n\nvoid PsychicEventSource::addClient(PsychicClient *client) {\n  client->_friend = new PsychicEventSourceClient(client);\n  PsychicHandler::addClient(client);\n}\n\nvoid PsychicEventSource::removeClient(PsychicClient *client) {\n  PsychicHandler::removeClient(client);\n  delete (PsychicEventSourceClient*)client->_friend;\n  client->_friend = NULL;\n}\n\nvoid PsychicEventSource::openCallback(PsychicClient *client) {\n  PsychicEventSourceClient *buddy = getClient(client);\n  if (buddy == NULL)\n  {\n    return;\n  }\n\n  if (_onOpen != NULL)\n    _onOpen(buddy);\n}\n\nvoid PsychicEventSource::closeCallback(PsychicClient *client) {\n  PsychicEventSourceClient *buddy = getClient(client);\n  if (buddy == NULL)\n  {\n    return;\n  }\n\n  if (_onClose != NULL)\n    _onClose(getClient(buddy));\n}\n\nvoid PsychicEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect)\n{\n  String ev = generateEventMessage(message, event, id, reconnect);\n  for(PsychicClient *c : _clients) {\n    ((PsychicEventSourceClient*)c->_friend)->sendEvent(ev.c_str());\n  }\n}\n\n/*****************************************/\n// PsychicEventSourceClient\n/*****************************************/\n\nPsychicEventSourceClient::PsychicEventSourceClient(PsychicClient *client) :\n  PsychicClient(client->server(), client->socket()),\n  _lastId(0)\n{\n}\n\nPsychicEventSourceClient::~PsychicEventSourceClient(){\n}\n\nvoid PsychicEventSourceClient::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){\n  String ev = generateEventMessage(message, event, id, reconnect);\n  sendEvent(ev.c_str());\n}\n\nvoid PsychicEventSourceClient::sendEvent(const char *event) {\n  int result;\n  do {\n    result = httpd_socket_send(this->server(), this->socket(), event, strlen(event), 0);\n  } while (result == HTTPD_SOCK_ERR_TIMEOUT);\n\n  //if (result < 0)\n  //error log here\n}\n\n/*****************************************/\n// PsychicEventSourceResponse\n/*****************************************/\n\nPsychicEventSourceResponse::PsychicEventSourceResponse(PsychicRequest *request) \n  : PsychicResponse(request)\n{\n}\n\nesp_err_t PsychicEventSourceResponse::send() {\n\n  //build our main header\n  String out = String();\n  out.concat(\"HTTP/1.1 200 OK\\r\\n\");\n  out.concat(\"Content-Type: text/event-stream\\r\\n\");\n  out.concat(\"Cache-Control: no-cache\\r\\n\");\n  out.concat(\"Connection: keep-alive\\r\\n\");\n\n  //get our global headers out of the way first\n  for (HTTPHeader header : DefaultHeaders::Instance().getHeaders())\n    out.concat(String(header.field) + \": \" + String(header.value) + \"\\r\\n\");\n\n  //separator\n  out.concat(\"\\r\\n\");\n\n  int result;\n  do {\n    result = httpd_send(_request->request(), out.c_str(), out.length());\n  } while (result == HTTPD_SOCK_ERR_TIMEOUT);\n\n  if (result < 0)\n    ESP_LOGE(PH_TAG, \"EventSource send failed with %s\", esp_err_to_name(result));\n\n  if (result > 0)\n    return ESP_OK;\n  else\n    return ESP_ERR_HTTPD_RESP_SEND;\n}\n\n/*****************************************/\n// Event Message Generator\n/*****************************************/\n\nString generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect) {\n  String ev = \"\";\n\n  if(reconnect){\n    ev += \"retry: \";\n    ev += String(reconnect);\n    ev += \"\\r\\n\";\n  }\n\n  if(id){\n    ev += \"id: \";\n    ev += String(id);\n    ev += \"\\r\\n\";\n  }\n\n  if(event != NULL){\n    ev += \"event: \";\n    ev += String(event);\n    ev += \"\\r\\n\";\n  }\n\n  if(message != NULL){\n    ev += \"data: \";\n    ev += String(message);\n    ev += \"\\r\\n\";\n  }\n  ev += \"\\r\\n\";\n\n  return ev;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicEventSource.h",
    "content": "/*\n  Asynchronous WebServer library for Espressif MCUs\n\n  Copyright (c) 2016 Hristo Gochkov. All rights reserved.\n\n  This library is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This library is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n*/\n#ifndef PsychicEventSource_H_\n#define PsychicEventSource_H_\n\n#include \"PsychicCore.h\"\n#include \"PsychicHandler.h\"\n#include \"PsychicClient.h\"\n#include \"PsychicResponse.h\"\n\nclass PsychicEventSource;\nclass PsychicEventSourceResponse;\nclass PsychicEventSourceClient;\nclass PsychicResponse;\n\ntypedef std::function<void(PsychicEventSourceClient *client)> PsychicEventSourceClientCallback;\n\nclass PsychicEventSourceClient : public PsychicClient {\n  friend PsychicEventSource;\n\n  protected:\n    uint32_t _lastId;\n\n  public:\n    PsychicEventSourceClient(PsychicClient *client);\n    ~PsychicEventSourceClient();\n\n    uint32_t lastId() const { return _lastId; }\n    void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0);\n    void sendEvent(const char *event);\n};\n\nclass PsychicEventSource : public PsychicHandler {\n  private:\n    PsychicEventSourceClientCallback _onOpen;\n    PsychicEventSourceClientCallback _onClose;\n\n  public:\n    PsychicEventSource();\n    ~PsychicEventSource();\n\n    PsychicEventSourceClient * getClient(int socket) override;\n    PsychicEventSourceClient * getClient(PsychicClient *client) override;\n    void addClient(PsychicClient *client) override;\n    void removeClient(PsychicClient *client) override;\n    void openCallback(PsychicClient *client) override;\n    void closeCallback(PsychicClient *client) override;\n\n    PsychicEventSource *onOpen(PsychicEventSourceClientCallback fn);\n    PsychicEventSource *onClose(PsychicEventSourceClientCallback fn);\n\n    esp_err_t handleRequest(PsychicRequest *request) override final;\n\n    void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0);\n};\n\nclass PsychicEventSourceResponse: public PsychicResponse {\n  public:\n    PsychicEventSourceResponse(PsychicRequest *request);\n    virtual esp_err_t send() override;\n};\n\nString generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect);\n\n#endif /* PsychicEventSource_H_ */"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicFileResponse.cpp",
    "content": "#include \"PsychicFileResponse.h\"\n#include \"PsychicResponse.h\"\n#include \"PsychicRequest.h\"\n\n\nPsychicFileResponse::PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType, bool download)\n : PsychicResponse(request) {\n  //_code = 200;\n  String _path(path);\n\n  if(!download && !fs.exists(_path) && fs.exists(_path+\".gz\")){\n    _path = _path+\".gz\";\n    addHeader(\"Content-Encoding\", \"gzip\");\n  }\n\n  _content = fs.open(_path, \"r\");\n  _contentLength = _content.size();\n\n  if(contentType == \"\")\n    _setContentType(path);\n  else\n    setContentType(contentType.c_str());\n\n  int filenameStart = path.lastIndexOf('/') + 1;\n  char buf[26+path.length()-filenameStart];\n  char* filename = (char*)path.c_str() + filenameStart;\n\n  if(download) {\n    // set filename and force download\n    snprintf(buf, sizeof (buf), \"attachment; filename=\\\"%s\\\"\", filename);\n  } else {\n    // set filename and force rendering\n    snprintf(buf, sizeof (buf), \"inline; filename=\\\"%s\\\"\", filename);\n  }\n  addHeader(\"Content-Disposition\", buf);\n}\n\nPsychicFileResponse::PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType, bool download)\n : PsychicResponse(request) {\n  String _path(path);\n\n  if(!download && String(content.name()).endsWith(\".gz\") && !path.endsWith(\".gz\")){\n    addHeader(\"Content-Encoding\", \"gzip\");\n  }\n\n  _content = content;\n  _contentLength = _content.size();\n\n  if(contentType == \"\")\n    _setContentType(path);\n  else\n    setContentType(contentType.c_str());\n\n  int filenameStart = path.lastIndexOf('/') + 1;\n  char buf[26+path.length()-filenameStart];\n  char* filename = (char*)path.c_str() + filenameStart;\n\n  if(download) {\n    snprintf(buf, sizeof (buf), \"attachment; filename=\\\"%s\\\"\", filename);\n  } else {\n    snprintf(buf, sizeof (buf), \"inline; filename=\\\"%s\\\"\", filename);\n  }\n  addHeader(\"Content-Disposition\", buf);\n}\n\nPsychicFileResponse::~PsychicFileResponse()\n{\n  if(_content)\n    _content.close();\n}\n\nvoid PsychicFileResponse::_setContentType(const String& path){\n  const char *_contentType;\n\t\n  if (path.endsWith(\".html\")) _contentType = \"text/html\";\n  else if (path.endsWith(\".htm\")) _contentType = \"text/html\";\n  else if (path.endsWith(\".css\")) _contentType = \"text/css\";\n  else if (path.endsWith(\".json\")) _contentType = \"application/json\";\n  else if (path.endsWith(\".js\")) _contentType = \"application/javascript\";\n  else if (path.endsWith(\".png\")) _contentType = \"image/png\";\n  else if (path.endsWith(\".gif\")) _contentType = \"image/gif\";\n  else if (path.endsWith(\".jpg\")) _contentType = \"image/jpeg\";\n  else if (path.endsWith(\".ico\")) _contentType = \"image/x-icon\";\n  else if (path.endsWith(\".svg\")) _contentType = \"image/svg+xml\";\n  else if (path.endsWith(\".eot\")) _contentType = \"font/eot\";\n  else if (path.endsWith(\".woff\")) _contentType = \"font/woff\";\n  else if (path.endsWith(\".woff2\")) _contentType = \"font/woff2\";\n  else if (path.endsWith(\".ttf\")) _contentType = \"font/ttf\";\n  else if (path.endsWith(\".xml\")) _contentType = \"text/xml\";\n  else if (path.endsWith(\".pdf\")) _contentType = \"application/pdf\";\n  else if (path.endsWith(\".zip\")) _contentType = \"application/zip\";\n  else if(path.endsWith(\".gz\")) _contentType = \"application/x-gzip\";\n  else _contentType = \"text/plain\";\n  \n  setContentType(_contentType);\n}\n\nesp_err_t PsychicFileResponse::send()\n{\n  esp_err_t err = ESP_OK;\n\n  //just send small files directly\n  size_t size = getContentLength();\n  if (size < FILE_CHUNK_SIZE)\n  {\n    uint8_t *buffer = (uint8_t *)malloc(size);\n    if (buffer == NULL)\n    {\n      /* Respond with 500 Internal Server Error */\n      httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, \"Unable to allocate memory.\");\n      return ESP_FAIL;\n    }\n\n    size_t readSize = _content.readBytes((char *)buffer, size);\n\n    this->setContent(buffer, readSize);\n    err = PsychicResponse::send();\n    \n    free(buffer);\n  }\n  else\n  {\n    /* Retrieve the pointer to scratch buffer for temporary storage */\n    char *chunk = (char *)malloc(FILE_CHUNK_SIZE);\n    if (chunk == NULL)\n    {\n      /* Respond with 500 Internal Server Error */\n      httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, \"Unable to allocate memory.\");\n      return ESP_FAIL;\n    }\n\n    this->sendHeaders();\n\n    size_t chunksize;\n    do {\n        /* Read file in chunks into the scratch buffer */\n        chunksize = _content.readBytes(chunk, FILE_CHUNK_SIZE);\n        if (chunksize > 0)\n        {\n          err = this->sendChunk((uint8_t *)chunk, chunksize);\n          if (err != ESP_OK)\n            break;\n        }\n\n        /* Keep looping till the whole file is sent */\n    } while (chunksize != 0);\n\n    //keep track of our memory\n    free(chunk);\n\n    if (err == ESP_OK)\n    {\n      ESP_LOGD(PH_TAG, \"File sending complete\");\n      this->finishChunking();\n    }\n  }\n\n  return err;\n}\n"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicFileResponse.h",
    "content": "#ifndef PsychicFileResponse_h\n#define PsychicFileResponse_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicResponse.h\"\n\nclass PsychicRequest;\n\nclass PsychicFileResponse: public PsychicResponse\n{\n  using File = fs::File;\n  using FS = fs::FS;\n  private:\n    File _content;\n    void _setContentType(const String& path);\n  public:\n    PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType=String(), bool download=false);\n    PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType=String(), bool download=false);\n    ~PsychicFileResponse();\n    esp_err_t send();\n};\n\n#endif // PsychicFileResponse_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHandler.cpp",
    "content": "#include \"PsychicHandler.h\"\n\nPsychicHandler::PsychicHandler() :\n  _filter(NULL),\n  _server(NULL),\n  _username(\"\"),\n  _password(\"\"),\n  _method(DIGEST_AUTH),\n  _realm(\"\"),\n  _authFailMsg(\"\"),\n  _subprotocol(\"\")\n  {}\n\nPsychicHandler::~PsychicHandler() {\n  // actual PsychicClient deletion handled by PsychicServer\n  // for (PsychicClient *client : _clients)\n  //   delete(client);\n  _clients.clear();\n}\n\nPsychicHandler* PsychicHandler::setFilter(PsychicRequestFilterFunction fn) {\n  _filter = fn;\n  return this;\n}\n\nbool PsychicHandler::filter(PsychicRequest *request){\n  return _filter == NULL || _filter(request);\n}\n\nvoid PsychicHandler::setSubprotocol(const String& subprotocol) {\n    this->_subprotocol = subprotocol;\n}\nconst char* PsychicHandler::getSubprotocol() const {\n    return _subprotocol.c_str();\n}\n\nPsychicHandler* PsychicHandler::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) {\n  _username = String(username);\n  _password = String(password);\n  _method = method;\n  _realm = String(realm);\n  _authFailMsg = String(authFailMsg);\n  return this;\n};\n\nbool PsychicHandler::needsAuthentication(PsychicRequest *request) {\n  return (_username != \"\" && _password != \"\") && !request->authenticate(_username.c_str(), _password.c_str());\n}\n\nesp_err_t PsychicHandler::authenticate(PsychicRequest *request) {\n  return request->requestAuthentication(_method, _realm.c_str(), _authFailMsg.c_str());\n}\n\nPsychicClient * PsychicHandler::checkForNewClient(PsychicClient *client)\n{\n  PsychicClient *c = PsychicHandler::getClient(client);\n  if (c == NULL)\n  {\n    c = client;\n    addClient(c);\n    c->isNew = true;\n  }\n  else\n    c->isNew = false;\n\n  return c;\n}\n\nvoid PsychicHandler::checkForClosedClient(PsychicClient *client)\n{\n  if (hasClient(client))\n  {\n    closeCallback(client);\n    removeClient(client);\n  }\n}\n\nvoid PsychicHandler::addClient(PsychicClient *client) {\n  _clients.push_back(client);\n}\n\nvoid PsychicHandler::removeClient(PsychicClient *client) {\n  _clients.remove(client);\n}\n\nPsychicClient * PsychicHandler::getClient(int socket)\n{\n  //make sure the server has it too.\n  if (!_server->hasClient(socket))\n    return NULL;\n\n  //what about us?\n  for (PsychicClient *client : _clients)\n    if (client->socket() == socket)\n      return client;\n\n  //nothing found.\n  return NULL;\n}\n\nPsychicClient * PsychicHandler::getClient(PsychicClient *client) {\n  return PsychicHandler::getClient(client->socket());\n}\n\nbool PsychicHandler::hasClient(PsychicClient *socket) {\n  return PsychicHandler::getClient(socket) != NULL;\n}\n\nconst std::list<PsychicClient*>& PsychicHandler::getClientList() {\n  return _clients;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHandler.h",
    "content": "#ifndef PsychicHandler_h\n#define PsychicHandler_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicRequest.h\"\n\nclass PsychicEndpoint;\nclass PsychicHttpServer;\n\n/*\n* HANDLER :: Can be attached to any endpoint or as a generic request handler.\n*/\n\nclass PsychicHandler {\n  friend PsychicEndpoint;\n\n  protected:\n    PsychicRequestFilterFunction _filter;\n    PsychicHttpServer *_server;\n\n    String _username;\n    String _password;\n    HTTPAuthMethod _method;\n    String _realm;\n    String _authFailMsg;\n\n    String _subprotocol;\n\n    std::list<PsychicClient*> _clients;\n\n  public:\n    PsychicHandler();\n    virtual ~PsychicHandler();\n\n    PsychicHandler* setFilter(PsychicRequestFilterFunction fn);\n    bool filter(PsychicRequest *request);\n\n    PsychicHandler* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = \"\", const char *authFailMsg = \"\");\n    bool needsAuthentication(PsychicRequest *request);\n    esp_err_t authenticate(PsychicRequest *request);\n\n    virtual bool isWebSocket() { return false; };\n\n    void setSubprotocol(const String& subprotocol);\n    const char* getSubprotocol() const;\n\n    PsychicClient * checkForNewClient(PsychicClient *client);\n    void checkForClosedClient(PsychicClient *client);\n\n    virtual void addClient(PsychicClient *client);\n    virtual void removeClient(PsychicClient *client);\n    virtual PsychicClient * getClient(int socket);\n    virtual PsychicClient * getClient(PsychicClient *client);\n    virtual void openCallback(PsychicClient *client) {};\n    virtual void closeCallback(PsychicClient *client) {};\n\n    bool hasClient(PsychicClient *client);\n    int count() { return _clients.size(); };\n    const std::list<PsychicClient*>& getClientList();\n\n    //derived classes must implement these functions\n    virtual bool canHandle(PsychicRequest *request) { return true; };\n    virtual esp_err_t handleRequest(PsychicRequest *request) = 0;\n};\n\n#endif"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHttp.h",
    "content": "#ifndef PsychicHttp_h\n#define PsychicHttp_h\n\n#include <http_status.h>\n#include \"PsychicHttpServer.h\"\n#include \"PsychicRequest.h\"\n#include \"PsychicResponse.h\"\n#include \"PsychicEndpoint.h\"\n#include \"PsychicHandler.h\"\n#include \"PsychicStaticFileHandler.h\"\n#include \"PsychicFileResponse.h\"\n#include \"PsychicStreamResponse.h\"\n#include \"PsychicUploadHandler.h\"\n#include \"PsychicWebSocket.h\"\n#include \"PsychicEventSource.h\"\n#include \"PsychicJson.h\"\n\n#endif /* PsychicHttp_h */"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHttpServer.cpp",
    "content": "#include \"PsychicHttpServer.h\"\n#include \"PsychicEndpoint.h\"\n#include \"PsychicHandler.h\"\n#include \"PsychicWebHandler.h\"\n#include \"PsychicStaticFileHandler.h\"\n#include \"PsychicWebSocket.h\"\n#include \"PsychicJson.h\"\n#include \"WiFi.h\"\n\nPsychicHttpServer::PsychicHttpServer() :\n  _onOpen(NULL),\n  _onClose(NULL)\n{\n  maxRequestBodySize = MAX_REQUEST_BODY_SIZE;\n  maxUploadSize = MAX_UPLOAD_SIZE;\n\n  defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, \"\");\n  onNotFound(PsychicHttpServer::defaultNotFoundHandler);\n  \n  //for a regular server\n  config = HTTPD_DEFAULT_CONFIG();\n  config.open_fn = PsychicHttpServer::openCallback;\n  config.close_fn = PsychicHttpServer::closeCallback;\n  config.uri_match_fn = httpd_uri_match_wildcard;\n  config.global_user_ctx = this;\n  config.global_user_ctx_free_fn = destroy;\n  config.max_uri_handlers = 20;\n}\n\nPsychicHttpServer::~PsychicHttpServer()\n{\n  for (auto *client : _clients)\n    delete(client);\n  _clients.clear();\n\n  for (auto *endpoint : _endpoints)\n    delete(endpoint);\n  _endpoints.clear();\n\n  for (auto *handler : _handlers)\n    delete(handler);\n  _handlers.clear();\n\n  delete defaultEndpoint;\n}\n\nvoid PsychicHttpServer::destroy(void *ctx)\n{\n  // do not release any resource for PsychicHttpServer in order to be able to restart it after stopping\n}\n\nesp_err_t PsychicHttpServer::listen(uint16_t port)\n{\n  this->_use_ssl = false;\n  this->config.server_port = port;\n\n  return this->_start();\n}\n\nesp_err_t PsychicHttpServer::_start()\n{\n  esp_err_t ret;\n\n  //fire it up.\n  ret = _startServer();\n  if (ret != ESP_OK)\n  {\n    ESP_LOGE(PH_TAG, \"Server start failed (%s)\", esp_err_to_name(ret));\n    return ret;\n  }\n\n  // Register handler\n  ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler);\n  if (ret != ESP_OK)\n    ESP_LOGE(PH_TAG, \"Add 404 handler failed (%s)\", esp_err_to_name(ret)); \n\n  return ret;\n}\n\nesp_err_t PsychicHttpServer::_startServer() {\n  return httpd_start(&this->server, &this->config);\n}\n\nvoid PsychicHttpServer::stop()\n{\n  httpd_stop(this->server);  \n}\n\nPsychicHandler& PsychicHttpServer::addHandler(PsychicHandler* handler){\n  _handlers.push_back(handler);\n  return *handler;\n}\n\nvoid PsychicHttpServer::removeHandler(PsychicHandler *handler){\n  _handlers.remove(handler);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri) {\n  return on(uri, HTTP_GET);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method)\n{\n  PsychicWebHandler *handler = new PsychicWebHandler();\n\n  return on(uri, method, handler);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler *handler)\n{\n  return on(uri, HTTP_GET, handler);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHandler *handler)\n{\n  //make our endpoint\n  PsychicEndpoint *endpoint = new PsychicEndpoint(this, method, uri);\n\n  //set our handler\n  endpoint->setHandler(handler);\n\n  // URI handler structure\n  httpd_uri_t my_uri {\n    .uri      = uri,\n    .method   = method,\n    .handler  = PsychicEndpoint::requestCallback,\n    .user_ctx = endpoint,\n    .is_websocket = handler->isWebSocket(),\n    .supported_subprotocol = handler->getSubprotocol()\n  };\n  \n  // Register endpoint with ESP-IDF server\n  esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri);\n  if (ret != ESP_OK)\n    ESP_LOGE(PH_TAG, \"Add endpoint failed (%s)\", esp_err_to_name(ret));\n\n  //save it for later\n  _endpoints.push_back(endpoint);\n\n  return endpoint;\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHttpRequestCallback fn)\n{\n  return on(uri, HTTP_GET, fn);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHttpRequestCallback fn)\n{\n  //these basic requests need a basic web handler\n  PsychicWebHandler *handler = new PsychicWebHandler();\n  handler->onRequest(fn);\n\n  return on(uri, method, handler);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicJsonRequestCallback fn)\n{\n  return on(uri, HTTP_GET, fn);\n}\n\nPsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicJsonRequestCallback fn)\n{\n  //these basic requests need a basic web handler\n  PsychicJsonHandler *handler = new PsychicJsonHandler();\n  handler->onRequest(fn);\n\n  return on(uri, method, handler);\n}\n\nvoid PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn)\n{\n  PsychicWebHandler *handler = new PsychicWebHandler();\n  handler->onRequest(fn == nullptr ? PsychicHttpServer::defaultNotFoundHandler : fn);\n\n  this->defaultEndpoint->setHandler(handler);\n}\n\nesp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t *req, httpd_err_code_t err)\n{\n  PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle);\n  PsychicRequest request(server, req);\n\n  //loop through our global handlers and see if anyone wants it\n  for(auto *handler: server->_handlers)\n  {\n    //are we capable of handling this?\n    if (handler->filter(&request) && handler->canHandle(&request))\n    {\n      //check our credentials\n       if (handler->needsAuthentication(&request))\n        return handler->authenticate(&request);\n      else\n        return handler->handleRequest(&request);\n    }\n  }\n\n  //nothing found, give it to our defaultEndpoint\n  PsychicHandler *handler = server->defaultEndpoint->handler();\n  if (handler->filter(&request) && handler->canHandle(&request))\n    return handler->handleRequest(&request);\n\n  //not sure how we got this far.\n  return ESP_ERR_HTTPD_INVALID_REQ;\n}\n\nesp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest *request)\n{\n  request->reply(404, \"text/html\", \"That URI does not exist.\");\n\n  return ESP_OK;\n}\n\nvoid PsychicHttpServer::onOpen(PsychicClientCallback handler) {\n  this->_onOpen = handler;\n}\n\nesp_err_t PsychicHttpServer::openCallback(httpd_handle_t hd, int sockfd)\n{\n  ESP_LOGD(PH_TAG, \"New client connected %d\", sockfd);\n\n  //get our global server reference\n  PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd);\n\n  //lookup our client\n  PsychicClient *client = server->getClient(sockfd);\n  if (client == NULL)\n  {\n    client = new PsychicClient(hd, sockfd);\n    server->addClient(client);\n  }\n\n  //user callback\n  if (server->_onOpen != NULL)\n    server->_onOpen(client);\n\n  return ESP_OK;\n}\n\nvoid PsychicHttpServer::onClose(PsychicClientCallback handler) {\n  this->_onClose = handler;\n}\n\nvoid PsychicHttpServer::closeCallback(httpd_handle_t hd, int sockfd)\n{\n  ESP_LOGD(PH_TAG, \"Client disconnected %d\", sockfd);\n\n  PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd);\n\n  //lookup our client\n  PsychicClient *client = server->getClient(sockfd);\n  if (client != NULL)\n  {\n    //give our handlers a chance to handle a disconnect first\n    for (PsychicEndpoint * endpoint : server->_endpoints)\n    {\n      PsychicHandler *handler = endpoint->handler();\n      handler->checkForClosedClient(client);\n    }\n\n    //do we have a callback attached?\n    if (server->_onClose != NULL)\n      server->_onClose(client);\n\n    //remove it from our list\n    server->removeClient(client);\n  }\n  else\n    ESP_LOGE(PH_TAG, \"No client record %d\", sockfd);\n\n  //finally close it out.\n  close(sockfd);\n}\n\nPsychicStaticFileHandler* PsychicHttpServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control)\n{\n  PsychicStaticFileHandler* handler = new PsychicStaticFileHandler(uri, fs, path, cache_control);\n  this->addHandler(handler);\n\n  return handler;\n}\n\nvoid PsychicHttpServer::addClient(PsychicClient *client) {\n  _clients.push_back(client);\n}\n\nvoid PsychicHttpServer::removeClient(PsychicClient *client) {\n  _clients.remove(client);\n  delete client;\n}\n\nPsychicClient * PsychicHttpServer::getClient(int socket) {\n  for (PsychicClient * client : _clients)\n    if (client->socket() == socket)\n      return client;\n\n  return NULL;\n}\n\nPsychicClient * PsychicHttpServer::getClient(httpd_req_t *req) {\n  return getClient(httpd_req_to_sockfd(req));\n}\n\nbool PsychicHttpServer::hasClient(int socket) {\n  return getClient(socket) != NULL;\n}\n\nconst std::list<PsychicClient*>& PsychicHttpServer::getClientList() {\n  return _clients;\n}\n\nbool ON_STA_FILTER(PsychicRequest *request) {\n  return WiFi.localIP() == request->client()->localIP();\n}\n\nbool ON_AP_FILTER(PsychicRequest *request) {\n  return WiFi.softAPIP() == request->client()->localIP();\n}\n\nString urlDecode(const char* encoded)\n{\n  size_t length = strlen(encoded);\n  char* decoded = (char*)malloc(length + 1);\n  if (!decoded) {\n    return \"\";\n  }\n\n  size_t i, j = 0;\n  for (i = 0; i < length; ++i) {\n      if (encoded[i] == '%' && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) {\n          // Valid percent-encoded sequence\n          int hex;\n          sscanf(encoded + i + 1, \"%2x\", &hex);\n          decoded[j++] = (char)hex;\n          i += 2;  // Skip the two hexadecimal characters\n      } else if (encoded[i] == '+') {\n          // Convert '+' to space\n          decoded[j++] = ' ';\n      } else {\n          // Copy other characters as they are\n          decoded[j++] = encoded[i];\n      }\n  }\n\n  decoded[j] = '\\0';  // Null-terminate the decoded string\n\n  String output(decoded);\n  free(decoded);\n\n  return output;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHttpServer.h",
    "content": "#ifndef PsychicHttpServer_h\n#define PsychicHttpServer_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicClient.h\"\n#include \"PsychicHandler.h\"\n\nclass PsychicEndpoint;\nclass PsychicHandler;\nclass PsychicStaticFileHandler;\n\nclass PsychicHttpServer\n{\n  protected:\n    bool _use_ssl = false;\n    std::list<PsychicEndpoint*> _endpoints;\n    std::list<PsychicHandler*> _handlers;\n    std::list<PsychicClient*> _clients;\n\n    PsychicClientCallback _onOpen;\n    PsychicClientCallback _onClose;\n\n    esp_err_t _start();\n    virtual esp_err_t _startServer();\n\n  public:\n    PsychicHttpServer();\n    virtual ~PsychicHttpServer();\n\n    //esp-idf specific stuff\n    httpd_handle_t server;\n    httpd_config_t config;\n\n    //some limits on what we will accept\n    unsigned long maxUploadSize;\n    unsigned long maxRequestBodySize;\n\n    PsychicEndpoint *defaultEndpoint;\n\n    static void destroy(void *ctx);\n\n    esp_err_t listen(uint16_t port);\n\n    virtual void stop();\n\n    PsychicHandler& addHandler(PsychicHandler* handler);\n    void removeHandler(PsychicHandler* handler);\n\n    void addClient(PsychicClient *client);\n    void removeClient(PsychicClient *client);\n    PsychicClient* getClient(int socket);\n    PsychicClient* getClient(httpd_req_t *req);\n    bool hasClient(int socket);\n    int count() { return _clients.size(); };\n    const std::list<PsychicClient*>& getClientList();\n\n    PsychicEndpoint* on(const char* uri);\n    PsychicEndpoint* on(const char* uri, http_method method);\n    PsychicEndpoint* on(const char* uri, PsychicHandler *handler);\n    PsychicEndpoint* on(const char* uri, http_method method, PsychicHandler *handler);\n    PsychicEndpoint* on(const char* uri, PsychicHttpRequestCallback onRequest);\n    PsychicEndpoint* on(const char* uri, http_method method, PsychicHttpRequestCallback onRequest);\n    PsychicEndpoint* on(const char* uri, PsychicJsonRequestCallback onRequest);\n    PsychicEndpoint* on(const char* uri, http_method method, PsychicJsonRequestCallback onRequest);\n\n    static esp_err_t notFoundHandler(httpd_req_t *req, httpd_err_code_t err);\n    static esp_err_t defaultNotFoundHandler(PsychicRequest *request);\n    void onNotFound(PsychicHttpRequestCallback fn);\n\n    void onOpen(PsychicClientCallback handler);\n    void onClose(PsychicClientCallback handler);\n    static esp_err_t openCallback(httpd_handle_t hd, int sockfd);\n    static void closeCallback(httpd_handle_t hd, int sockfd);\n\n    PsychicStaticFileHandler* serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL);\n};\n\nbool ON_STA_FILTER(PsychicRequest *request);\nbool ON_AP_FILTER(PsychicRequest *request);\n\n#endif // PsychicHttpServer_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHttpsServer.cpp",
    "content": "#include \"PsychicHttpsServer.h\"\n\n#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE\n\nPsychicHttpsServer::PsychicHttpsServer() : PsychicHttpServer()\n{\n  //for a SSL server\n  ssl_config = HTTPD_SSL_CONFIG_DEFAULT();\n  ssl_config.httpd.open_fn = PsychicHttpServer::openCallback;\n  ssl_config.httpd.close_fn = PsychicHttpServer::closeCallback;\n  ssl_config.httpd.uri_match_fn = httpd_uri_match_wildcard;\n  ssl_config.httpd.global_user_ctx = this;\n  ssl_config.httpd.global_user_ctx_free_fn = destroy;\n  ssl_config.httpd.max_uri_handlers = 20;\n\n  // each SSL connection takes about 45kb of heap\n  // a barebones sketch with PsychicHttp has ~150kb of heap available\n  // if we set it higher than 2 and use all the connections, we get lots of memory errors.\n  // not to mention there is no heap left over for the program itself.\n  ssl_config.httpd.max_open_sockets = 2;\n}\n\nPsychicHttpsServer::~PsychicHttpsServer() {}\n\nesp_err_t PsychicHttpsServer::listen(uint16_t port, const char *cert, const char *private_key)\n{\n  this->_use_ssl = true;\n\n  this->ssl_config.port_secure = port;\n\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2)\n    this->ssl_config.servercert = (uint8_t *)cert;\n    this->ssl_config.servercert_len = strlen(cert)+1;\n#else\n    this->ssl_config.cacert_pem = (uint8_t *)cert;\n    this->ssl_config.cacert_len = strlen(cert)+1;\n#endif\n\n  this->ssl_config.prvtkey_pem = (uint8_t *)private_key;\n  this->ssl_config.prvtkey_len = strlen(private_key)+1;\n\n  return this->_start();\n}\n\nesp_err_t PsychicHttpsServer::_startServer()\n{\n  if (this->_use_ssl)\n    return httpd_ssl_start(&this->server, &this->ssl_config);\n  else\n    return httpd_start(&this->server, &this->config);\n}\n\nvoid PsychicHttpsServer::stop()\n{\n  if (this->_use_ssl)\n    httpd_ssl_stop(this->server);\n  else\n    httpd_stop(this->server);\n}\n\n#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicHttpsServer.h",
    "content": "#ifndef PsychicHttpsServer_h\n#define PsychicHttpsServer_h\n\n#include <sdkconfig.h>\n\n#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE\n\n#include \"PsychicCore.h\"\n#include \"PsychicHttpServer.h\"\n#include <esp_https_server.h>\n#if !CONFIG_HTTPD_WS_SUPPORT\n  #error PsychicHttpsServer cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration\n#endif\n\n#define PSY_ENABLE_SSL //you can use this define in your code to enable/disable these features\n\nclass PsychicHttpsServer : public PsychicHttpServer\n{\n  protected:\n    bool _use_ssl = false;\n\n  public:\n    PsychicHttpsServer();\n    ~PsychicHttpsServer();\n\n    httpd_ssl_config_t ssl_config;\n\n    using PsychicHttpServer::listen; //keep the regular version\n    esp_err_t listen(uint16_t port, const char *cert, const char *private_key);\n\n    virtual esp_err_t _startServer() override final;\n    virtual void stop() override final;\n};\n\n#endif // PsychicHttpsServer_h\n\n#else\n  #error ESP-IDF https server support not enabled.\n#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicJson.cpp",
    "content": "#include \"PsychicJson.h\"\n\n#ifdef ARDUINOJSON_6_COMPATIBILITY\n  PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray, size_t maxJsonBufferSize) :\n    PsychicResponse(request),\n    _jsonBuffer(maxJsonBufferSize)\n  {\n    setContentType(JSON_MIMETYPE);\n    if (isArray)\n      _root = _jsonBuffer.createNestedArray();\n    else\n      _root = _jsonBuffer.createNestedObject();\n  }\n#else\n  PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray) : PsychicResponse(request)\n  {\n    setContentType(JSON_MIMETYPE);\n    if (isArray)\n      _root = _jsonBuffer.add<JsonArray>();\n    else\n      _root = _jsonBuffer.add<JsonObject>();\n  }\n#endif\n\nJsonVariant &PsychicJsonResponse::getRoot() { return _root; }\n\nsize_t PsychicJsonResponse::getLength()\n{\n  return measureJson(_root);\n}\n\nesp_err_t PsychicJsonResponse::send()\n{\n  esp_err_t err = ESP_OK;\n  size_t length = getLength();\n  size_t buffer_size;\n  char *buffer;\n\n  //how big of a buffer do we want?\n  if (length < JSON_BUFFER_SIZE)\n    buffer_size = length+1;\n  else\n    buffer_size = JSON_BUFFER_SIZE;\n\n  buffer = (char *)malloc(buffer_size);\n  if (buffer == NULL) {\n    httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, \"Unable to allocate memory.\");\n    return ESP_FAIL;\n  }\n\n  //send it in one shot or no?\n  if (length < JSON_BUFFER_SIZE)\n  {\n    serializeJson(_root, buffer, buffer_size);\n\n    this->setContent((uint8_t *)buffer, length);\n    this->setContentType(JSON_MIMETYPE);\n\n    err = PsychicResponse::send();\n  }\n  else\n  {\n    //helper class that acts as a stream to print chunked responses\n    ChunkPrinter dest(this, (uint8_t *)buffer, buffer_size);\n\n    //keep our headers\n    this->sendHeaders();\n\n    serializeJson(_root, dest);\n\n    //send the last bits\n    dest.flush();\n\n    //done with our chunked response too\n    err = this->finishChunking();\n  }\n\n  //let the buffer go\n  free(buffer);\n\n  return err;\n}\n\n#ifdef ARDUINOJSON_6_COMPATIBILITY\n  PsychicJsonHandler::PsychicJsonHandler(size_t maxJsonBufferSize) :\n    _onRequest(NULL),\n    _maxJsonBufferSize(maxJsonBufferSize)\n  {};\n\n  PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize) :\n    _onRequest(onRequest),\n    _maxJsonBufferSize(maxJsonBufferSize)\n  {}\n#else\n  PsychicJsonHandler::PsychicJsonHandler() :\n    _onRequest(NULL)\n  {};\n\n  PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest) :\n    _onRequest(onRequest)\n  {}\n#endif\n\nvoid PsychicJsonHandler::onRequest(PsychicJsonRequestCallback fn) { _onRequest = fn; }\n\nesp_err_t PsychicJsonHandler::handleRequest(PsychicRequest *request)\n{\n  //process basic stuff\n  PsychicWebHandler::handleRequest(request);\n\n  if (_onRequest)\n  {\n    #ifdef ARDUINOJSON_6_COMPATIBILITY\n      DynamicJsonDocument jsonBuffer(this->_maxJsonBufferSize);\n      DeserializationError error = deserializeJson(jsonBuffer, request->body());\n      if (error)\n        return request->reply(400);\n\n      JsonVariant json = jsonBuffer.as<JsonVariant>();\n    #else\n      JsonDocument jsonBuffer;\n      DeserializationError error = deserializeJson(jsonBuffer, request->body());\n      if (error)\n        return request->reply(400);\n\n      JsonVariant json = jsonBuffer.as<JsonVariant>();\n    #endif\n\n    return _onRequest(request, json);\n  }\n  else\n    return request->reply(500);\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicJson.h",
    "content": "// PsychicJson.h\n/*\n  Async Response to use with ArduinoJson and AsyncWebServer\n  Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon.\n  Ported to PsychicHttp by Zach Hoeken\n\n*/\n#ifndef PSYCHIC_JSON_H_\n#define PSYCHIC_JSON_H_\n\n#include \"PsychicRequest.h\"\n#include \"PsychicWebHandler.h\"\n#include \"ChunkPrinter.h\"\n#include <ArduinoJson.h>\n\n#if ARDUINOJSON_VERSION_MAJOR == 6\n  #define ARDUINOJSON_6_COMPATIBILITY\n  #ifndef DYNAMIC_JSON_DOCUMENT_SIZE\n    #define DYNAMIC_JSON_DOCUMENT_SIZE 4096\n  #endif\n#endif\n\n\n#ifndef JSON_BUFFER_SIZE\n  #define JSON_BUFFER_SIZE 4*1024\n#endif\n\nconstexpr const char *JSON_MIMETYPE = \"application/json\";\n\n/*\n * Json Response\n * */\n\nclass PsychicJsonResponse : public PsychicResponse\n{\n  protected:\n    #ifdef ARDUINOJSON_5_COMPATIBILITY\n      DynamicJsonBuffer _jsonBuffer;\n    #elif ARDUINOJSON_VERSION_MAJOR == 6\n      DynamicJsonDocument _jsonBuffer;\n    #else\n      JsonDocument _jsonBuffer;\n    #endif\n\n    JsonVariant _root;\n    size_t _contentLength;\n\n  public:\n    #ifdef ARDUINOJSON_5_COMPATIBILITY\n      PsychicJsonResponse(PsychicRequest *request, bool isArray = false);\n    #elif ARDUINOJSON_VERSION_MAJOR == 6\n      PsychicJsonResponse(PsychicRequest *request, bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);\n    #else\n      PsychicJsonResponse(PsychicRequest *request, bool isArray = false);\n    #endif\n\n    ~PsychicJsonResponse() {}\n\n    JsonVariant &getRoot();\n    size_t getLength();\n    \n    virtual esp_err_t send() override;\n};\n\nclass PsychicJsonHandler : public PsychicWebHandler\n{\n  protected:\n    PsychicJsonRequestCallback _onRequest;\n    #if ARDUINOJSON_VERSION_MAJOR == 6\n      const size_t _maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE;\n    #endif\n\n  public:\n    #ifdef ARDUINOJSON_5_COMPATIBILITY\n      PsychicJsonHandler();\n      PsychicJsonHandler(PsychicJsonRequestCallback onRequest);\n    #elif ARDUINOJSON_VERSION_MAJOR == 6\n      PsychicJsonHandler(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);\n      PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);\n    #else\n      PsychicJsonHandler();\n      PsychicJsonHandler(PsychicJsonRequestCallback onRequest);\n    #endif\n\n    void onRequest(PsychicJsonRequestCallback fn);\n    virtual esp_err_t handleRequest(PsychicRequest *request) override;\n};\n\n#endif"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicRequest.cpp",
    "content": "#include \"PsychicRequest.h\"\n#include \"http_status.h\"\n#include \"PsychicHttpServer.h\"\n\nPsychicRequest::PsychicRequest(PsychicHttpServer *server, httpd_req_t *req) : _server(server),\n                                                                              _req(req),\n                                                                              _method(HTTP_GET),\n                                                                              _query(\"\"),\n                                                                              _body(\"\"),\n                                                                              _tempObject(NULL)\n{\n    // load up our client.\n    this->_client = server->getClient(req);\n\n    // handle our session data\n    if (req->sess_ctx != NULL)\n        this->_session = (SessionData *)req->sess_ctx;\n    else\n    {\n        this->_session = new SessionData();\n        req->sess_ctx = this->_session;\n    }\n\n    // callback for freeing the session later\n    req->free_ctx = this->freeSession;\n\n    // load up some data\n    this->_uri = String(this->_req->uri);\n}\n\nPsychicRequest::~PsychicRequest()\n{\n    // temorary user object\n    if (_tempObject != NULL)\n        free(_tempObject);\n\n    // our web parameters\n    for (auto *param : _params)\n        delete (param);\n    _params.clear();\n}\n\nvoid PsychicRequest::freeSession(void *ctx)\n{\n    if (ctx != NULL)\n    {\n        SessionData *session = (SessionData *)ctx;\n        delete session;\n    }\n}\n\nPsychicHttpServer *PsychicRequest::server()\n{\n    return _server;\n}\n\nhttpd_req_t *PsychicRequest::request()\n{\n    return _req;\n}\n\nPsychicClient *PsychicRequest::client()\n{\n    return _client;\n}\n\nconst String PsychicRequest::getFilename()\n{\n    // parse the content-disposition header\n    if (this->hasHeader(\"Content-Disposition\"))\n    {\n        ContentDisposition cd = this->getContentDisposition();\n        if (cd.filename != \"\")\n            return cd.filename;\n    }\n\n    // fall back to passed in query string\n    PsychicWebParameter *param = getParam(\"_filename\");\n    if (param != NULL)\n        return param->name();\n\n    // fall back to parsing it from url (useful for wildcard uploads)\n    String uri = this->uri();\n    int filenameStart = uri.lastIndexOf('/') + 1;\n    String filename = uri.substring(filenameStart);\n    if (filename != \"\")\n        return filename;\n\n    // finally, unknown.\n    ESP_LOGE(PH_TAG, \"Did not get a valid filename from the upload.\");\n    return \"unknown.txt\";\n}\n\nconst ContentDisposition PsychicRequest::getContentDisposition()\n{\n    ContentDisposition cd;\n    String header = this->header(\"Content-Disposition\");\n    int start;\n    int end;\n\n    if (header.indexOf(\"form-data\") == 0)\n        cd.disposition = FORM_DATA;\n    else if (header.indexOf(\"attachment\") == 0)\n        cd.disposition = ATTACHMENT;\n    else if (header.indexOf(\"inline\") == 0)\n        cd.disposition = INLINE;\n    else\n        cd.disposition = NONE;\n\n    start = header.indexOf(\"filename=\");\n    if (start)\n    {\n        end = header.indexOf('\"', start + 10);\n        cd.filename = header.substring(start + 10, end - 1);\n    }\n\n    start = header.indexOf(\"name=\");\n    if (start)\n    {\n        end = header.indexOf('\"', start + 6);\n        cd.name = header.substring(start + 6, end - 1);\n    }\n\n    return cd;\n}\n\nesp_err_t PsychicRequest::loadBody()\n{\n    esp_err_t err = ESP_OK;\n\n    this->_body = String();\n\n    size_t remaining = this->_req->content_len;\n    size_t actuallyReceived = 0;\n    char *buf = (char *)malloc(remaining + 1);\n    if (buf == NULL)\n    {\n        ESP_LOGE(PH_TAG, \"Failed to allocate memory for body\");\n        return ESP_FAIL;\n    }\n\n    while (remaining > 0)\n    {\n        int received = httpd_req_recv(this->_req, buf + actuallyReceived, remaining);\n\n        if (received == HTTPD_SOCK_ERR_TIMEOUT)\n        {\n            continue;\n        }\n        else if (received == HTTPD_SOCK_ERR_FAIL)\n        {\n            ESP_LOGE(PH_TAG, \"Failed to receive data.\");\n            err = ESP_FAIL;\n            break;\n        }\n\n        remaining -= received;\n        actuallyReceived += received;\n    }\n\n    buf[actuallyReceived] = '\\0';\n    this->_body = String(buf);\n    free(buf);\n    return err;\n}\n\nhttp_method PsychicRequest::method()\n{\n    return (http_method)this->_req->method;\n}\n\nconst String PsychicRequest::methodStr()\n{\n    return String(http_method_str((http_method)this->_req->method));\n}\n\nconst String PsychicRequest::path()\n{\n    int index = _uri.indexOf(\"?\");\n    if (index == -1)\n        return _uri;\n    else\n        return _uri.substring(0, index);\n}\n\nconst String &PsychicRequest::uri()\n{\n    return this->_uri;\n}\n\nconst String &PsychicRequest::query()\n{\n    return this->_query;\n}\n\n// no way to get list of headers yet....\n// int PsychicRequest::headers()\n// {\n// }\n\nconst String PsychicRequest::header(const char *name)\n{\n    size_t header_len = httpd_req_get_hdr_value_len(this->_req, name);\n\n    // if we've got one, allocated it and load it\n    if (header_len)\n    {\n        char header[header_len + 1];\n        httpd_req_get_hdr_value_str(this->_req, name, header, sizeof(header));\n        return String(header);\n    }\n    else\n        return \"\";\n}\n\nbool PsychicRequest::hasHeader(const char *name)\n{\n    return httpd_req_get_hdr_value_len(this->_req, name) > 0;\n}\n\nconst String PsychicRequest::host()\n{\n    return this->header(\"Host\");\n}\n\nconst String PsychicRequest::contentType()\n{\n    return header(\"Content-Type\");\n}\n\nsize_t PsychicRequest::contentLength()\n{\n    return this->_req->content_len;\n}\n\nconst String &PsychicRequest::body()\n{\n    return this->_body;\n}\n\nbool PsychicRequest::isMultipart()\n{\n    const String &type = this->contentType();\n\n    return (this->contentType().indexOf(\"multipart/form-data\") >= 0);\n}\n\nesp_err_t PsychicRequest::redirect(const char *url)\n{\n    PsychicResponse response(this);\n    response.setCode(301);\n    response.addHeader(\"Location\", url);\n\n    return response.send();\n}\n\nbool PsychicRequest::hasCookie(const char *key)\n{\n    char cookie[MAX_COOKIE_SIZE];\n    size_t cookieSize = MAX_COOKIE_SIZE;\n    esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize);\n\n    // did we get anything?\n    if (err == ESP_OK)\n        return true;\n    else if (err == ESP_ERR_HTTPD_RESULT_TRUNC)\n        ESP_LOGE(PH_TAG, \"cookie too large (%d bytes).\\n\", cookieSize);\n\n    return false;\n}\n\nconst String PsychicRequest::getCookie(const char *key)\n{\n    char cookie[MAX_COOKIE_SIZE];\n    size_t cookieSize = MAX_COOKIE_SIZE;\n    esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize);\n\n    // did we get anything?\n    if (err == ESP_OK)\n        return String(cookie);\n    else\n        return \"\";\n}\n\nvoid PsychicRequest::loadParams()\n{\n    // did we get a query string?\n    size_t query_len = httpd_req_get_url_query_len(_req);\n    if (query_len)\n    {\n        char query[query_len + 1];\n        httpd_req_get_url_query_str(_req, query, sizeof(query));\n        _query.clear();\n        _query.concat(query);\n\n        // parse them.\n        _addParams(_query, false);\n    }\n\n    // did we get form data as body?\n    if (this->method() == HTTP_POST && this->contentType().startsWith(\"application/x-www-form-urlencoded\"))\n    {\n        _addParams(_body, true);\n    }\n}\n\nvoid PsychicRequest::_addParams(const String &params, bool post)\n{\n    size_t start = 0;\n    while (start < params.length())\n    {\n        int end = params.indexOf('&', start);\n        if (end < 0)\n            end = params.length();\n        int equal = params.indexOf('=', start);\n        if (equal < 0 || equal > end)\n            equal = end;\n        String name = params.substring(start, equal);\n        String value = equal + 1 < end ? params.substring(equal + 1, end) : String();\n        addParam(name, value, true, post);\n        start = end + 1;\n    }\n}\n\nPsychicWebParameter *PsychicRequest::addParam(const String &name, const String &value, bool decode, bool post)\n{\n    if (decode)\n        return addParam(new PsychicWebParameter(urlDecode(name.c_str()), urlDecode(value.c_str()), post));\n    else\n        return addParam(new PsychicWebParameter(name, value, post));\n}\n\nPsychicWebParameter *PsychicRequest::addParam(PsychicWebParameter *param)\n{\n    // ESP_LOGD(PH_TAG, \"Adding param: '%s' = '%s'\", param->name().c_str(), param->value().c_str());\n    _params.push_back(param);\n    return param;\n}\n\nbool PsychicRequest::hasParam(const char *key)\n{\n    return getParam(key) != NULL;\n}\n\nPsychicWebParameter *PsychicRequest::getParam(const char *key)\n{\n    for (auto *param : _params)\n        if (param->name().equals(key))\n            return param;\n\n    return NULL;\n}\n\nbool PsychicRequest::hasSessionKey(const String &key)\n{\n    return this->_session->find(key) != this->_session->end();\n}\n\nconst String PsychicRequest::getSessionKey(const String &key)\n{\n    auto it = this->_session->find(key);\n    if (it != this->_session->end())\n        return it->second;\n    else\n        return \"\";\n}\n\nvoid PsychicRequest::setSessionKey(const String &key, const String &value)\n{\n    this->_session->insert(std::pair<String, String>(key, value));\n}\n\nstatic const String md5str(const String &in)\n{\n    MD5Builder md5 = MD5Builder();\n    md5.begin();\n    md5.add(in);\n    md5.calculate();\n    return md5.toString();\n}\n\nbool PsychicRequest::authenticate(const char *username, const char *password)\n{\n    if (hasHeader(\"Authorization\"))\n    {\n        String authReq = header(\"Authorization\");\n        if (authReq.startsWith(\"Basic\"))\n        {\n            authReq = authReq.substring(6);\n            authReq.trim();\n            char toencodeLen = strlen(username) + strlen(password) + 1;\n            char *toencode = new char[toencodeLen + 1];\n            if (toencode == NULL)\n            {\n                authReq = \"\";\n                return false;\n            }\n            char *encoded = new char[base64_encode_expected_len(toencodeLen) + 1];\n            if (encoded == NULL)\n            {\n                authReq = \"\";\n                delete[] toencode;\n                return false;\n            }\n            sprintf(toencode, \"%s:%s\", username, password);\n            if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded))\n            {\n                authReq = \"\";\n                delete[] toencode;\n                delete[] encoded;\n                return true;\n            }\n            delete[] toencode;\n            delete[] encoded;\n        }\n        else if (authReq.startsWith(F(\"Digest\")))\n        {\n            authReq = authReq.substring(7);\n            String _username = _extractParam(authReq, F(\"username=\\\"\"), '\\\"');\n            if (!_username.length() || _username != String(username))\n            {\n                authReq = \"\";\n                return false;\n            }\n            // extracting required parameters for RFC 2069 simpler Digest\n            String _realm = _extractParam(authReq, F(\"realm=\\\"\"), '\\\"');\n            String _nonce = _extractParam(authReq, F(\"nonce=\\\"\"), '\\\"');\n            String _uri = _extractParam(authReq, F(\"uri=\\\"\"), '\\\"');\n            String _resp = _extractParam(authReq, F(\"response=\\\"\"), '\\\"');\n            String _opaque = _extractParam(authReq, F(\"opaque=\\\"\"), '\\\"');\n\n            if ((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_resp.length()) || (!_opaque.length()))\n            {\n                authReq = \"\";\n                return false;\n            }\n            if ((_opaque != this->getSessionKey(\"opaque\")) || (_nonce != this->getSessionKey(\"nonce\")) || (_realm != this->getSessionKey(\"realm\")))\n            {\n                authReq = \"\";\n                return false;\n            }\n            // parameters for the RFC 2617 newer Digest\n            String _nc, _cnonce;\n            if (authReq.indexOf(\"qop=auth\") != -1 || authReq.indexOf(\"qop=\\\"auth\\\"\") != -1)\n            {\n                _nc = _extractParam(authReq, F(\"nc=\"), ',');\n                _cnonce = _extractParam(authReq, F(\"cnonce=\\\"\"), '\\\"');\n            }\n\n            String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password));\n            // ESP_LOGD(PH_TAG, \"Hash of user:realm:pass=%s\", _H1.c_str());\n\n            String _H2 = \"\";\n            if (_method == HTTP_GET)\n            {\n                _H2 = md5str(String(F(\"GET:\")) + _uri);\n            }\n            else if (_method == HTTP_POST)\n            {\n                _H2 = md5str(String(F(\"POST:\")) + _uri);\n            }\n            else if (_method == HTTP_PUT)\n            {\n                _H2 = md5str(String(F(\"PUT:\")) + _uri);\n            }\n            else if (_method == HTTP_DELETE)\n            {\n                _H2 = md5str(String(F(\"DELETE:\")) + _uri);\n            }\n            else\n            {\n                _H2 = md5str(String(F(\"GET:\")) + _uri);\n            }\n            // ESP_LOGD(PH_TAG, \"Hash of GET:uri=%s\", _H2.c_str());\n\n            String _responsecheck = \"\";\n            if (authReq.indexOf(\"qop=auth\") != -1 || authReq.indexOf(\"qop=\\\"auth\\\"\") != -1)\n            {\n                _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(\":auth:\") + _H2);\n            }\n            else\n            {\n                _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2);\n            }\n\n            // ESP_LOGD(PH_TAG, \"The Proper response=%s\", _responsecheck.c_str());\n            if (_resp == _responsecheck)\n            {\n                authReq = \"\";\n                return true;\n            }\n        }\n        authReq = \"\";\n    }\n    return false;\n}\n\nconst String PsychicRequest::_extractParam(const String &authReq, const String &param, const char delimit)\n{\n    int _begin = authReq.indexOf(param);\n    if (_begin == -1)\n        return \"\";\n    return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));\n}\n\nconst String PsychicRequest::_getRandomHexString()\n{\n    char buffer[33]; // buffer to hold 32 Hex Digit + /0\n    int i;\n    for (i = 0; i < 4; i++)\n    {\n        sprintf(buffer + (i * 8), \"%08lx\", (unsigned long int)esp_random());\n    }\n    return String(buffer);\n}\n\nesp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char *realm, const char *authFailMsg)\n{\n    // what is thy realm, sire?\n    if (!strcmp(realm, \"\"))\n        this->setSessionKey(\"realm\", \"Login Required\");\n    else\n        this->setSessionKey(\"realm\", realm);\n\n    PsychicResponse response(this);\n    String authStr;\n\n    // what kind of auth?\n    if (mode == BASIC_AUTH)\n    {\n        authStr = \"Basic realm=\\\"\" + this->getSessionKey(\"realm\") + \"\\\"\";\n        response.addHeader(\"WWW-Authenticate\", authStr.c_str());\n    }\n    else\n    {\n        // only make new ones if we havent sent them yet\n        if (this->getSessionKey(\"nonce\").isEmpty())\n            this->setSessionKey(\"nonce\", _getRandomHexString());\n        if (this->getSessionKey(\"opaque\").isEmpty())\n            this->setSessionKey(\"opaque\", _getRandomHexString());\n\n        authStr = \"Digest realm=\\\"\" + this->getSessionKey(\"realm\") + \"\\\", qop=\\\"auth\\\", nonce=\\\"\" + this->getSessionKey(\"nonce\") + \"\\\", opaque=\\\"\" + this->getSessionKey(\"opaque\") + \"\\\"\";\n        response.addHeader(\"WWW-Authenticate\", authStr.c_str());\n    }\n\n    response.setCode(401);\n    response.setContentType(\"text/html\");\n    response.setContent(authStr.c_str());\n    return response.send();\n}\n\nesp_err_t PsychicRequest::reply(int code)\n{\n    PsychicResponse response(this);\n\n    response.setCode(code);\n    response.setContentType(\"text/plain\");\n    response.setContent(http_status_reason(code));\n\n    return response.send();\n}\n\nesp_err_t PsychicRequest::reply(const char *content)\n{\n    PsychicResponse response(this);\n\n    response.setCode(200);\n    response.setContentType(\"text/html\");\n    response.setContent(content);\n\n    return response.send();\n}\n\nesp_err_t PsychicRequest::reply(int code, const char *contentType, const char *content)\n{\n    PsychicResponse response(this);\n\n    response.setCode(code);\n    response.setContentType(contentType);\n    response.setContent(content);\n\n    return response.send();\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicRequest.h",
    "content": "#ifndef PsychicRequest_h\n#define PsychicRequest_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicHttpServer.h\"\n#include \"PsychicClient.h\"\n#include \"PsychicWebParameter.h\"\n#include \"PsychicResponse.h\"\n\ntypedef std::map<String, String> SessionData;\n\nenum Disposition { NONE, INLINE, ATTACHMENT, FORM_DATA};\n\nstruct ContentDisposition {\n  Disposition disposition;\n  String filename;\n  String name;\n};\n\nclass PsychicRequest {\n  friend PsychicHttpServer;\n\n  protected:\n    PsychicHttpServer *_server;\n    httpd_req_t *_req;\n    SessionData *_session;\n    PsychicClient *_client;\n\n    http_method _method;\n    String _uri;\n    String _query;\n    String _body;\n\n    std::list<PsychicWebParameter*> _params;\n\n    void _addParams(const String& params, bool post);\n    void _parseGETParams();\n    void _parsePOSTParams();\n\n    const String _extractParam(const String& authReq, const String& param, const char delimit);\n    const String _getRandomHexString();\n\n  public:\n    PsychicRequest(PsychicHttpServer *server, httpd_req_t *req);\n    virtual ~PsychicRequest();\n\n    void *_tempObject;\n\n    PsychicHttpServer * server();\n    httpd_req_t * request();\n    virtual PsychicClient * client();\n\n    bool isMultipart();\n    esp_err_t loadBody();\n\n    const String header(const char *name);\n    bool hasHeader(const char *name);\n\n    static void freeSession(void *ctx);\n    bool hasSessionKey(const String& key);\n    const String getSessionKey(const String& key);\n    void setSessionKey(const String& key, const String& value);\n\n    bool hasCookie(const char * key);\n    const String getCookie(const char * key);\n\n    http_method method();       // returns the HTTP method used as enum value (eg. HTTP_GET)\n    const String methodStr();   // returns the HTTP method used as a string (eg. \"GET\")\n    const String path();        // returns the request path (eg /page?foo=bar returns \"/page\")\n    const String& uri();        // returns the full request uri (eg /page?foo=bar)\n    const String& query();      // returns the request query data (eg /page?foo=bar returns \"foo=bar\")\n    const String host();        // returns the requested host (request to http://psychic.local/foo will return \"psychic.local\")\n    const String contentType(); // returns the Content-Type header value\n    size_t contentLength();     // returns the Content-Length header value\n    const String& body();       // returns the body of the request\n    const ContentDisposition getContentDisposition();\n\n    const String& queryString() { return query(); }  //compatability function.  same as query()\n    const String& url() { return uri(); }            //compatability function.  same as uri()\n\n    void loadParams();\n    PsychicWebParameter * addParam(PsychicWebParameter *param);\n    PsychicWebParameter * addParam(const String &name, const String &value, bool decode = true, bool post = false);\n    bool hasParam(const char *key);\n    PsychicWebParameter * getParam(const char *name);\n\n    const String getFilename();\n\n    bool authenticate(const char * username, const char * password);\n    esp_err_t requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg);\n\n    esp_err_t redirect(const char *url);\n    esp_err_t reply(int code);\n    esp_err_t reply(const char *content);\n    esp_err_t reply(int code, const char *contentType, const char *content);\n};\n\n#endif // PsychicRequest_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicResponse.cpp",
    "content": "#include \"PsychicResponse.h\"\n#include \"PsychicRequest.h\"\n#include <http_status.h>\n\nPsychicResponse::PsychicResponse(PsychicRequest *request) :\n  _request(request),\n  _code(200),\n  _status(\"\"),\n  _contentLength(0),\n  _body(\"\")\n{\n}\n\nPsychicResponse::~PsychicResponse()\n{\n  //clean up our header variables.  we have to do this on desctruct since httpd_resp_send doesn't store copies\n  for (HTTPHeader header : _headers)\n  {\n    free(header.field);\n    free(header.value);\n  }\n  _headers.clear();\n}\n\nvoid PsychicResponse::addHeader(const char *field, const char *value)\n{\n  //these get freed after send by the destructor\n  HTTPHeader header;\n  header.field =(char *)malloc(strlen(field)+1);\n  header.value = (char *)malloc(strlen(value)+1);\n\n  strlcpy(header.field, field, strlen(field)+1);\n  strlcpy(header.value, value, strlen(value)+1);\n\n  _headers.push_back(header);\n}\n\nvoid PsychicResponse::setCookie(const char *name, const char *value, unsigned long secondsFromNow, const char *extras)\n{\n  time_t now = time(nullptr);\n\n  String output;\n  output = urlEncode(name) + \"=\" + urlEncode(value);\n\n  //if current time isn't modern, default to using max age\n  if (now < 1700000000)\n    output += \"; Max-Age=\" + String(secondsFromNow);    \n  //otherwise, set an expiration date\n  else\n  {\n    time_t expirationTimestamp = now + secondsFromNow;\n\n    // Convert the expiration timestamp to a formatted string for the \"expires\" attribute\n    struct tm* tmInfo = gmtime(&expirationTimestamp);\n    char expires[30];\n    strftime(expires, sizeof(expires), \"%a, %d %b %Y %H:%M:%S GMT\", tmInfo);\n    output += \"; Expires=\" + String(expires);\n  }\n\n  //did we get any extras?\n  if (strlen(extras))\n    output += \"; \" + String(extras);\n\n  //okay, add it in.\n  addHeader(\"Set-Cookie\", output.c_str());\n}\n\nvoid PsychicResponse::setCode(int code)\n{\n  _code = code;\n}\n\nvoid PsychicResponse::setContentType(const char *contentType)\n{\n  httpd_resp_set_type(_request->request(), contentType);\n}\n\nvoid PsychicResponse::setContent(const char *content)\n{\n  _body = content;\n  setContentLength(strlen(content));\n}\n\nvoid PsychicResponse::setContent(const uint8_t *content, size_t len)\n{\n  _body = (char *)content;\n  setContentLength(len);\n}\n\nconst char * PsychicResponse::getContent()\n{\n  return _body;\n}\n\nsize_t PsychicResponse::getContentLength()\n{\n  return _contentLength;\n}\n\nesp_err_t PsychicResponse::send()\n{\n  //esp-idf makes you set the whole status.\n  sprintf(_status, \"%u %s\", _code, http_status_reason(_code));\n  httpd_resp_set_status(_request->request(), _status);\n\n  //our headers too\n  this->sendHeaders();\n\n  //now send it off\n  esp_err_t err = httpd_resp_send(_request->request(), getContent(), getContentLength());\n\n  //did something happen?\n  if (err != ESP_OK)\n    ESP_LOGE(PH_TAG, \"Send response failed (%s)\", esp_err_to_name(err));\n\n  return err;\n}\n\nvoid PsychicResponse::sendHeaders()\n{\n  //get our global headers out of the way first\n  for (HTTPHeader header : DefaultHeaders::Instance().getHeaders())\n    httpd_resp_set_hdr(_request->request(), header.field, header.value);\n\n  //now do our individual headers\n  for (HTTPHeader header : _headers)\n    httpd_resp_set_hdr(this->_request->request(), header.field, header.value);\n\n  // DO NOT RELEASE HEADERS HERE... released in the PsychicResponse destructor after they have been sent.\n  // httpd_resp_set_hdr just passes on the pointer, but its needed after this call.\n  // clean up our header variables after send\n  // for (HTTPHeader header : _headers)\n  // {\n  //   free(header.field);\n  //   free(header.value);\n  // }\n  // _headers.clear();\n}\n\nesp_err_t PsychicResponse::sendChunk(uint8_t *chunk, size_t chunksize)\n{\n  /* Send the buffer contents as HTTP response chunk */\n  esp_err_t err = httpd_resp_send_chunk(this->_request->request(), (char *)chunk, chunksize);\n  if (err != ESP_OK)\n  {\n    ESP_LOGE(PH_TAG, \"File sending failed (%s)\", esp_err_to_name(err));\n\n    /* Abort sending file */\n    httpd_resp_sendstr_chunk(this->_request->request(), NULL);\n\n    /* Respond with 500 Internal Server Error */\n    httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, \"Failed to send file\");\n  }\n\n  return err;\n}\n\nesp_err_t PsychicResponse::finishChunking()\n{\n  /* Respond with an empty chunk to signal HTTP response completion */\n  return httpd_resp_send_chunk(this->_request->request(), NULL, 0);\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicResponse.h",
    "content": "#ifndef PsychicResponse_h\n#define PsychicResponse_h\n\n#include \"PsychicCore.h\"\n#include \"time.h\"\n\nclass PsychicRequest;\n\nclass PsychicResponse\n{\n  protected:\n    PsychicRequest *_request;\n\n    int _code;\n    char _status[60];\n    std::list<HTTPHeader> _headers;\n    int64_t _contentLength;\n    const char * _body;\n\n  public:\n    PsychicResponse(PsychicRequest *request);\n    virtual ~PsychicResponse();\n\n    void setCode(int code);\n\n    void setContentType(const char *contentType);\n    void setContentLength(int64_t contentLength) { _contentLength = contentLength; }\n    int64_t getContentLength(int64_t contentLength) { return _contentLength; }\n\n    void addHeader(const char *field, const char *value);\n\n    void setCookie(const char *key, const char *value, unsigned long max_age = 60*60*24*30, const char *extras = \"\");\n\n    void setContent(const char *content);\n    void setContent(const uint8_t *content, size_t len);\n\n    const char * getContent();\n    size_t getContentLength();\n\n    virtual esp_err_t send();\n    void sendHeaders();\n    esp_err_t sendChunk(uint8_t *chunk, size_t chunksize);\n    esp_err_t finishChunking();\n};\n\n#endif // PsychicResponse_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicStaticFileHander.cpp",
    "content": "#include \"PsychicStaticFileHandler.h\"\n\n/*************************************/\n/*  PsychicStaticFileHandler         */\n/*************************************/\n\nPsychicStaticFileHandler::PsychicStaticFileHandler(const char *uri, FS &fs, const char *path, const char *cache_control)\n    : _fs(fs), _uri(uri), _path(path), _default_file(\"index.html\"), _cache_control(cache_control), _last_modified(\"\")\n{\n    // Ensure leading '/'\n    if (_uri.length() == 0 || _uri[0] != '/')\n        _uri = \"/\" + _uri;\n    if (_path.length() == 0 || _path[0] != '/')\n        _path = \"/\" + _path;\n\n    // If path ends with '/' we assume a hint that this is a directory to improve performance.\n    // However - if it does not end with '/' we, can't assume a file, path can still be a directory.\n    _isDir = _path[_path.length() - 1] == '/';\n\n    // Remove the trailing '/' so we can handle default file\n    // Notice that root will be \"\" not \"/\"\n    if (_uri[_uri.length() - 1] == '/')\n        _uri = _uri.substring(0, _uri.length() - 1);\n    if (_path[_path.length() - 1] == '/')\n        _path = _path.substring(0, _path.length() - 1);\n\n    // Reset stats\n    _gzipFirst = false;\n    _gzipStats = 0xF8;\n}\n\nPsychicStaticFileHandler &PsychicStaticFileHandler::setIsDir(bool isDir)\n{\n    _isDir = isDir;\n    return *this;\n}\n\nPsychicStaticFileHandler &PsychicStaticFileHandler::setDefaultFile(const char *filename)\n{\n    _default_file = String(filename);\n    return *this;\n}\n\nPsychicStaticFileHandler &PsychicStaticFileHandler::setCacheControl(const char *cache_control)\n{\n    _cache_control = String(cache_control);\n    return *this;\n}\n\nPsychicStaticFileHandler &PsychicStaticFileHandler::setLastModified(const char *last_modified)\n{\n    _last_modified = String(last_modified);\n    return *this;\n}\n\nPsychicStaticFileHandler &PsychicStaticFileHandler::setLastModified(struct tm *last_modified)\n{\n    char result[30];\n    strftime(result, 30, \"%a, %d %b %Y %H:%M:%S %Z\", last_modified);\n    return setLastModified((const char *)result);\n}\n\nbool PsychicStaticFileHandler::canHandle(PsychicRequest *request)\n{\n    if (request->method() != HTTP_GET || !request->uri().startsWith(_uri))\n        return false;\n\n    if (_getFile(request))\n        return true;\n\n    return false;\n}\n\nbool PsychicStaticFileHandler::_getFile(PsychicRequest *request)\n{\n    // Remove the found uri\n    String path = request->uri().substring(_uri.length());\n\n    // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/'\n    bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length() - 1] == '/');\n\n    path = _path + path;\n\n    // Do we have a file or .gz file\n    if (!canSkipFileCheck && _fileExists(path))\n        return true;\n\n    // Can't handle if not default file\n    if (_default_file.length() == 0)\n        return false;\n\n    // Try to add default file, ensure there is a trailing '/' ot the path.\n    if (path.length() == 0 || path[path.length() - 1] != '/')\n        path += \"/\";\n    path += _default_file;\n\n    return _fileExists(path);\n}\n\n#define FILE_IS_REAL(f) (f == true && !f.isDirectory())\n\nbool PsychicStaticFileHandler::_fileExists(const String &path)\n{\n    bool fileFound = false;\n    bool gzipFound = false;\n\n    String gzip = path + \".gz\";\n\n    if (_gzipFirst)\n    {\n        _file = _fs.open(gzip, \"r\");\n        gzipFound = FILE_IS_REAL(_file);\n        if (!gzipFound)\n        {\n            _file = _fs.open(path, \"r\");\n            fileFound = FILE_IS_REAL(_file);\n        }\n    }\n    else\n    {\n        _file = _fs.open(path, \"r\");\n        fileFound = FILE_IS_REAL(_file);\n        if (!fileFound)\n        {\n            _file = _fs.open(gzip, \"r\");\n            gzipFound = FILE_IS_REAL(_file);\n        }\n    }\n\n    bool found = fileFound || gzipFound;\n\n    if (found)\n    {\n        _filename = path;\n\n        // Calculate gzip statistic\n        _gzipStats = (_gzipStats << 1) + (gzipFound ? 1 : 0);\n        if (_gzipStats == 0x00)\n            _gzipFirst = false; // All files are not gzip\n        else if (_gzipStats == 0xFF)\n            _gzipFirst = true; // All files are gzip\n        else\n            _gzipFirst = _countBits(_gzipStats) > 4; // IF we have more gzip files - try gzip first\n    }\n\n    return found;\n}\n\nuint8_t PsychicStaticFileHandler::_countBits(const uint8_t value) const\n{\n    uint8_t w = value;\n    uint8_t n;\n    for (n = 0; w != 0; n++)\n        w &= w - 1;\n    return n;\n}\n\nesp_err_t PsychicStaticFileHandler::handleRequest(PsychicRequest *request)\n{\n    if (_file == true)\n    {\n        // is it not modified?\n        String etag = String(_file.size());\n        if (_last_modified.length() && _last_modified == request->header(\"If-Modified-Since\"))\n        {\n            _file.close();\n            request->reply(304); // Not modified\n        }\n        // does our Etag match?\n        else if (_cache_control.length() && request->hasHeader(\"If-None-Match\") && request->header(\"If-None-Match\").equals(etag))\n        {\n            _file.close();\n\n            PsychicResponse response(request);\n            response.addHeader(\"Cache-Control\", _cache_control.c_str());\n            response.addHeader(\"ETag\", etag.c_str());\n            response.setCode(304);\n            response.send();\n        }\n        // nope, send them the full file.\n        else\n        {\n            PsychicFileResponse response(request, _fs, _filename);\n\n            if (_last_modified.length())\n                response.addHeader(\"Last-Modified\", _last_modified.c_str());\n            if (_cache_control.length())\n            {\n                response.addHeader(\"Cache-Control\", _cache_control.c_str());\n                response.addHeader(\"ETag\", etag.c_str());\n            }\n\n            _file.close();\n\n            return response.send();\n        }\n    }\n    else\n    {\n        return request->reply(404);\n    }\n\n    return ESP_OK;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicStaticFileHandler.h",
    "content": "#ifndef PsychicStaticFileHandler_h\n#define PsychicStaticFileHandler_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicWebHandler.h\"\n#include \"PsychicRequest.h\"\n#include \"PsychicResponse.h\"\n#include \"PsychicFileResponse.h\"\n\nclass PsychicStaticFileHandler : public PsychicWebHandler {\n  using File = fs::File;\n  using FS = fs::FS;\n  private:\n    bool _getFile(PsychicRequest *request);\n    bool _fileExists(const String& path);\n    uint8_t _countBits(const uint8_t value) const;\n  protected:\n    FS _fs;\n    File _file;\n    String _filename;\n    String _uri;\n    String _path;\n    String _default_file;\n    String _cache_control;\n    String _last_modified;\n    bool _isDir;\n    bool _gzipFirst;\n    uint8_t _gzipStats;\n  public:\n    PsychicStaticFileHandler(const char* uri, FS& fs, const char* path, const char* cache_control);\n    bool canHandle(PsychicRequest *request) override;\n    esp_err_t handleRequest(PsychicRequest *request) override;\n    PsychicStaticFileHandler& setIsDir(bool isDir);\n    PsychicStaticFileHandler& setDefaultFile(const char* filename);\n    PsychicStaticFileHandler& setCacheControl(const char* cache_control);\n    PsychicStaticFileHandler& setLastModified(const char* last_modified);\n    PsychicStaticFileHandler& setLastModified(struct tm* last_modified);\n    //PsychicStaticFileHandler& setTemplateProcessor(AwsTemplateProcessor newCallback) {_callback = newCallback; return *this;}\n};\n\n#endif /* PsychicHttp_h */"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicStreamResponse.cpp",
    "content": "#include \"PsychicStreamResponse.h\"\n#include \"PsychicResponse.h\"\n#include \"PsychicRequest.h\"\n\nPsychicStreamResponse::PsychicStreamResponse(PsychicRequest *request, const String& contentType)\n : PsychicResponse(request), _buffer(NULL) {\n\n  setContentType(contentType.c_str());\n  addHeader(\"Content-Disposition\", \"inline\");\n}\n\n \nPsychicStreamResponse::PsychicStreamResponse(PsychicRequest *request, const String& contentType, const String& name)\n  : PsychicResponse(request), _buffer(NULL) {\n\n  setContentType(contentType.c_str());\n\n  char buf[26+name.length()];\n  snprintf(buf, sizeof (buf), \"attachment; filename=\\\"%s\\\"\", name.c_str());\n  addHeader(\"Content-Disposition\", buf);\n}\n\n \nPsychicStreamResponse::~PsychicStreamResponse()\n{\n  endSend();\n}\n\n\nesp_err_t PsychicStreamResponse::beginSend()\n{\n  if(_buffer)\n    return ESP_OK;\n\n  //Buffer to hold ChunkPrinter and stream buffer. Using placement new will keep us at a single allocation.\n  _buffer = (uint8_t*)malloc(STREAM_CHUNK_SIZE + sizeof(ChunkPrinter));\n  \n  if(!_buffer)\n  {\n    /* Respond with 500 Internal Server Error */\n    httpd_resp_send_err(_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, \"Unable to allocate memory.\");\n    return ESP_FAIL;\n  }\n\n  _printer = new (_buffer) ChunkPrinter(this, _buffer + sizeof(ChunkPrinter), STREAM_CHUNK_SIZE);\n\n  sendHeaders();\n  return ESP_OK;\n}\n\n\nesp_err_t PsychicStreamResponse::endSend()\n{\n  esp_err_t err = ESP_OK;\n  \n  if(!_buffer)\n    err = ESP_FAIL;\n  else\n  {\n    _printer->~ChunkPrinter(); //flushed on destruct\n    err = finishChunking();\n    free(_buffer);\n    _buffer = NULL;\n  }\n  return err;\n}\n\n\nvoid PsychicStreamResponse::flush()\n{\n  if(_buffer)\n    _printer->flush();\n}\n\n\nsize_t PsychicStreamResponse::write(uint8_t data)\n{\n  return _buffer ? _printer->write(data) : 0;\n}\n\n\nsize_t PsychicStreamResponse::write(const uint8_t *buffer, size_t size)\n{\n  return _buffer ? _printer->write(buffer, size) : 0;\n}\n\n\nsize_t PsychicStreamResponse::copyFrom(Stream &stream)\n{\n  if(_buffer)\n    return _printer->copyFrom(stream);\n\n  return 0;\n}\n"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicStreamResponse.h",
    "content": "#ifndef PsychicStreamResponse_h\n#define PsychicStreamResponse_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicResponse.h\"\n#include \"ChunkPrinter.h\"\n\nclass PsychicRequest;\n\nclass PsychicStreamResponse : public PsychicResponse, public Print\n{\n  private:\n    ChunkPrinter *_printer;\n    uint8_t *_buffer;\n  public:\n  \n    PsychicStreamResponse(PsychicRequest *request, const String& contentType);\n    PsychicStreamResponse(PsychicRequest *request, const String& contentType, const String& name); //Download\n  \n    ~PsychicStreamResponse();\n  \n    esp_err_t beginSend();\n    esp_err_t endSend();\n\n    void flush() override;\n\n    size_t write(uint8_t data) override;\n    size_t write(const uint8_t *buffer, size_t size) override;\n  \n    size_t copyFrom(Stream &stream);\n  \n    using Print::write;\n};\n\n#endif // PsychicStreamResponse_h\n"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicUploadHandler.cpp",
    "content": "#include \"PsychicUploadHandler.h\"\n\nPsychicUploadHandler::PsychicUploadHandler() : PsychicWebHandler(), _temp(), _parsedLength(0), _multiParseState(EXPECT_BOUNDARY), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false)\n{\n}\nPsychicUploadHandler::~PsychicUploadHandler() {}\n\nbool PsychicUploadHandler::canHandle(PsychicRequest *request)\n{\n    return true;\n}\n\nesp_err_t PsychicUploadHandler::handleRequest(PsychicRequest *request)\n{\n    esp_err_t err = ESP_OK;\n\n    // save it for later (multipart)\n    _request = request;\n    _parsedLength = 0;\n    /* File cannot be larger than a limit */\n    if (request->contentLength() > request->server()->maxUploadSize)\n    {\n        ESP_LOGE(PH_TAG, \"File too large : %d bytes\", request->contentLength());\n\n        /* Respond with 400 Bad Request */\n        char error[50];\n        sprintf(error, \"File size must be less than %lu bytes!\", request->server()->maxUploadSize);\n        httpd_resp_send_err(request->request(), HTTPD_400_BAD_REQUEST, error);\n\n        /* Return failure to close underlying connection else the incoming file content will keep the socket busy */\n        return ESP_FAIL;\n    }\n\n    // we might want to access some of these params\n    request->loadParams();\n\n    // TODO: support for the 100 header.  not sure if we can do it.\n    //  if (request->header(\"Expect\").equals(\"100-continue\"))\n    //  {\n    //    char response[] = \"100 Continue\";\n    //    httpd_socket_send(self->server, httpd_req_to_sockfd(req), response, strlen(response), 0);\n    //  }\n\n    // 2 types of upload requests\n    if (request->isMultipart())\n        err = _multipartUploadHandler(request);\n    else\n        err = _basicUploadHandler(request);\n\n    // we can also call onRequest for some final processing and response\n    if (err == ESP_OK)\n    {\n        if (_requestCallback != NULL)\n            err = _requestCallback(request);\n        else\n            err = request->reply(\"Upload Successful.\");\n    }\n    else\n        request->reply(500, \"text/html\", \"Error processing upload.\");\n\n    return err;\n}\n\nesp_err_t PsychicUploadHandler::_basicUploadHandler(PsychicRequest *request)\n{\n    esp_err_t err = ESP_OK;\n\n    String filename = request->getFilename();\n\n    /* Retrieve the pointer to scratch buffer for temporary storage */\n    char *buf = (char *)malloc(FILE_CHUNK_SIZE);\n    int received;\n    unsigned long index = 0;\n\n    /* Content length of the request gives the size of the file being uploaded */\n    int remaining = request->contentLength();\n\n    while (remaining > 0)\n    {\n        // ESP_LOGD(PH_TAG, \"Remaining size : %d\", remaining);\n\n        /* Receive the file part by part into a buffer */\n        if ((received = httpd_req_recv(request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0)\n        {\n            /* Retry if timeout occurred */\n            if (received == HTTPD_SOCK_ERR_TIMEOUT)\n                continue;\n            // bail if we got an error\n            else if (received == HTTPD_SOCK_ERR_FAIL)\n            {\n                ESP_LOGE(PH_TAG, \"Socket error\");\n                err = ESP_FAIL;\n                break;\n            }\n        }\n\n        // call our upload callback here.\n        if (_uploadCallback != NULL)\n        {\n            err = _uploadCallback(request, filename, index, (uint8_t *)buf, received, (remaining - received == 0));\n            if (err != ESP_OK)\n                break;\n        }\n        else\n        {\n            ESP_LOGE(PH_TAG, \"No upload callback specified!\");\n            err = ESP_FAIL;\n            break;\n        }\n\n        /* Keep track of remaining size of the file left to be uploaded */\n        remaining -= received;\n        index += received;\n    }\n\n    // dont forget to free our buffer\n    free(buf);\n\n    return err;\n}\n\nesp_err_t PsychicUploadHandler::_multipartUploadHandler(PsychicRequest *request)\n{\n    esp_err_t err = ESP_OK;\n\n    String value = request->header(\"Content-Type\");\n    if (value.startsWith(\"multipart/\"))\n    {\n        _boundary = value.substring(value.indexOf('=') + 1);\n        _boundary.replace(\"\\\"\", \"\");\n    }\n    else\n    {\n        ESP_LOGE(PH_TAG, \"No multipart boundary found.\");\n        return request->reply(400, \"text/html\", \"No multipart boundary found.\");\n    }\n\n    char *buf = (char *)malloc(FILE_CHUNK_SIZE);\n    int received;\n    unsigned long index = 0;\n\n    /* Content length of the request gives the size of the file being uploaded */\n    int remaining = request->contentLength();\n\n    while (remaining > 0)\n    {\n        // ESP_LOGD(PH_TAG, \"Remaining size : %d\", remaining);\n\n        /* Receive the file part by part into a buffer */\n        if ((received = httpd_req_recv(request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0)\n        {\n            /* Retry if timeout occurred */\n            if (received == HTTPD_SOCK_ERR_TIMEOUT)\n                continue;\n            // bail if we got an error\n            else if (received == HTTPD_SOCK_ERR_FAIL)\n            {\n                ESP_LOGE(PH_TAG, \"Socket error\");\n                err = ESP_FAIL;\n                break;\n            }\n        }\n\n        // parse it 1 byte at a time.\n        for (int i = 0; i < received; i++)\n        {\n            /* Keep track of remaining size of the file left to be uploaded */\n            remaining--;\n            index++;\n\n            // send it to our parser\n            _parseMultipartPostByte(buf[i], !remaining);\n            _parsedLength++;\n        }\n    }\n\n    // dont forget to free our buffer\n    free(buf);\n\n    return err;\n}\n\nPsychicUploadHandler *PsychicUploadHandler::onUpload(PsychicUploadCallback fn)\n{\n    _uploadCallback = fn;\n    return this;\n}\n\nvoid PsychicUploadHandler::_handleUploadByte(uint8_t data, bool last)\n{\n    _itemBuffer[_itemBufferIndex++] = data;\n\n    if (last || _itemBufferIndex == FILE_CHUNK_SIZE)\n    {\n        if (_uploadCallback)\n            _uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, last);\n        _itemBufferIndex = 0;\n    }\n}\n\n#define itemWriteByte(b)                \\\n    do                                  \\\n    {                                   \\\n        _itemSize++;                    \\\n        if (_itemIsFile)                \\\n            _handleUploadByte(b, last); \\\n        else                            \\\n            _itemValue += (char)(b);    \\\n    } while (0)\n\nvoid PsychicUploadHandler::_parseMultipartPostByte(uint8_t data, bool last)\n{\n    if (_multiParseState == PARSE_ERROR)\n    {\n        // not sure we can end up with an error during buffer fill, but jsut to be safe\n        if (_itemBuffer != NULL)\n        {\n            free(_itemBuffer);\n            _itemBuffer = NULL;\n        }\n\n        return;\n    }\n\n    if (!_parsedLength)\n    {\n        _multiParseState = EXPECT_BOUNDARY;\n        _temp = String();\n        _itemName = String();\n        _itemFilename = String();\n        _itemType = String();\n    }\n\n    if (_multiParseState == WAIT_FOR_RETURN1)\n    {\n        if (data != '\\r')\n        {\n            itemWriteByte(data);\n        }\n        else\n        {\n            _multiParseState = EXPECT_FEED1;\n        }\n    }\n    else if (_multiParseState == EXPECT_BOUNDARY)\n    {\n        if (_parsedLength < 2 && data != '-')\n        {\n            ESP_LOGE(PH_TAG, \"Multipart: No boundary\");\n            _multiParseState = PARSE_ERROR;\n            return;\n        }\n        else if (_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data)\n        {\n            ESP_LOGE(PH_TAG, \"Multipart: Multipart malformed\");\n            _multiParseState = PARSE_ERROR;\n            return;\n        }\n        else if (_parsedLength - 2 == _boundary.length() && data != '\\r')\n        {\n            ESP_LOGE(PH_TAG, \"Multipart: Multipart missing carriage return\");\n            _multiParseState = PARSE_ERROR;\n            return;\n        }\n        else if (_parsedLength - 3 == _boundary.length())\n        {\n            if (data != '\\n')\n            {\n                ESP_LOGE(PH_TAG, \"Multipart: Multipart missing newline\");\n                _multiParseState = PARSE_ERROR;\n                return;\n            }\n            _multiParseState = PARSE_HEADERS;\n            _itemIsFile = false;\n        }\n    }\n    else if (_multiParseState == PARSE_HEADERS)\n    {\n        if ((char)data != '\\r' && (char)data != '\\n')\n            _temp += (char)data;\n        if ((char)data == '\\n')\n        {\n            if (_temp.length())\n            {\n                if (_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase(\"Content-Type\"))\n                {\n                    _itemType = _temp.substring(14);\n                    _itemIsFile = true;\n                }\n                else if (_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase(\"Content-Disposition\"))\n                {\n                    _temp = _temp.substring(_temp.indexOf(';') + 2);\n                    while (_temp.indexOf(';') > 0)\n                    {\n                        String name = _temp.substring(0, _temp.indexOf('='));\n                        String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1);\n                        if (name == \"name\")\n                        {\n                            _itemName = nameVal;\n                        }\n                        else if (name == \"filename\")\n                        {\n                            _itemFilename = nameVal;\n                            _itemIsFile = true;\n                        }\n                        _temp = _temp.substring(_temp.indexOf(';') + 2);\n                    }\n                    String name = _temp.substring(0, _temp.indexOf('='));\n                    String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1);\n                    if (name == \"name\")\n                    {\n                        _itemName = nameVal;\n                    }\n                    else if (name == \"filename\")\n                    {\n                        _itemFilename = nameVal;\n                        _itemIsFile = true;\n                    }\n                }\n                _temp = String();\n            }\n            else\n            {\n                _multiParseState = WAIT_FOR_RETURN1;\n                // value starts from here\n                _itemSize = 0;\n                _itemStartIndex = _parsedLength;\n                _itemValue = String();\n                if (_itemIsFile)\n                {\n                    if (_itemBuffer)\n                        free(_itemBuffer);\n                    _itemBuffer = (uint8_t *)malloc(FILE_CHUNK_SIZE);\n                    if (_itemBuffer == NULL)\n                    {\n                        ESP_LOGE(PH_TAG, \"Multipart: Failed to allocate buffer\");\n                        _multiParseState = PARSE_ERROR;\n                        return;\n                    }\n                    _itemBufferIndex = 0;\n                }\n            }\n        }\n    }\n    else if (_multiParseState == EXPECT_FEED1)\n    {\n        if (data != '\\n')\n        {\n            _multiParseState = WAIT_FOR_RETURN1;\n            itemWriteByte('\\r');\n            _parseMultipartPostByte(data, last);\n        }\n        else\n        {\n            _multiParseState = EXPECT_DASH1;\n        }\n    }\n    else if (_multiParseState == EXPECT_DASH1)\n    {\n        if (data != '-')\n        {\n            _multiParseState = WAIT_FOR_RETURN1;\n            itemWriteByte('\\r');\n            itemWriteByte('\\n');\n            _parseMultipartPostByte(data, last);\n        }\n        else\n        {\n            _multiParseState = EXPECT_DASH2;\n        }\n    }\n    else if (_multiParseState == EXPECT_DASH2)\n    {\n        if (data != '-')\n        {\n            _multiParseState = WAIT_FOR_RETURN1;\n            itemWriteByte('\\r');\n            itemWriteByte('\\n');\n            itemWriteByte('-');\n            _parseMultipartPostByte(data, last);\n        }\n        else\n        {\n            _multiParseState = BOUNDARY_OR_DATA;\n            _boundaryPosition = 0;\n        }\n    }\n    else if (_multiParseState == BOUNDARY_OR_DATA)\n    {\n        if (_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data)\n        {\n            _multiParseState = WAIT_FOR_RETURN1;\n            itemWriteByte('\\r');\n            itemWriteByte('\\n');\n            itemWriteByte('-');\n            itemWriteByte('-');\n            uint8_t i;\n            for (i = 0; i < _boundaryPosition; i++)\n                itemWriteByte(_boundary.c_str()[i]);\n            _parseMultipartPostByte(data, last);\n        }\n        else if (_boundaryPosition == _boundary.length() - 1)\n        {\n            _multiParseState = DASH3_OR_RETURN2;\n            if (!_itemIsFile)\n            {\n                _request->addParam(_itemName, _itemValue);\n                //_addParam(new AsyncWebParameter(_itemName, _itemValue, true));\n            }\n            else\n            {\n                if (_itemSize)\n                {\n                    if (_uploadCallback)\n                        _uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true);\n                    _itemBufferIndex = 0;\n                    _request->addParam(new PsychicWebParameter(_itemName, _itemFilename, true, true, _itemSize));\n                }\n                free(_itemBuffer);\n                _itemBuffer = NULL;\n            }\n        }\n        else\n        {\n            _boundaryPosition++;\n        }\n    }\n    else if (_multiParseState == DASH3_OR_RETURN2)\n    {\n        if (data == '-' && (_request->contentLength() - _parsedLength - 4) != 0)\n        {\n            ESP_LOGE(PH_TAG, \"ERROR: The parser got to the end of the POST but is expecting more bytes!\");\n            _multiParseState = PARSE_ERROR;\n            return;\n        }\n        if (data == '\\r')\n        {\n            _multiParseState = EXPECT_FEED2;\n        }\n        else if (data == '-' && _request->contentLength() == (_parsedLength + 4))\n        {\n            _multiParseState = PARSING_FINISHED;\n        }\n        else\n        {\n            _multiParseState = WAIT_FOR_RETURN1;\n            itemWriteByte('\\r');\n            itemWriteByte('\\n');\n            itemWriteByte('-');\n            itemWriteByte('-');\n            uint8_t i;\n            for (i = 0; i < _boundary.length(); i++)\n                itemWriteByte(_boundary.c_str()[i]);\n            _parseMultipartPostByte(data, last);\n        }\n    }\n    else if (_multiParseState == EXPECT_FEED2)\n    {\n        if (data == '\\n')\n        {\n            _multiParseState = PARSE_HEADERS;\n            _itemIsFile = false;\n        }\n        else\n        {\n            _multiParseState = WAIT_FOR_RETURN1;\n            itemWriteByte('\\r');\n            itemWriteByte('\\n');\n            itemWriteByte('-');\n            itemWriteByte('-');\n            uint8_t i;\n            for (i = 0; i < _boundary.length(); i++)\n                itemWriteByte(_boundary.c_str()[i]);\n            itemWriteByte('\\r');\n            _parseMultipartPostByte(data, last);\n        }\n    }\n}\n"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicUploadHandler.h",
    "content": "#ifndef PsychicUploadHandler_h\n#define PsychicUploadHandler_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicHttpServer.h\"\n#include \"PsychicRequest.h\"\n#include \"PsychicWebHandler.h\"\n#include \"PsychicWebParameter.h\"\n\n//callback definitions\ntypedef std::function<esp_err_t(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final)> PsychicUploadCallback;\n\n/*\n* HANDLER :: Can be attached to any endpoint or as a generic request handler.\n*/\n\nclass PsychicUploadHandler : public PsychicWebHandler {\n  protected:\n    PsychicUploadCallback _uploadCallback;\n\n    PsychicRequest *_request;\n\n    String _temp;\n    size_t _parsedLength;\n    uint8_t _multiParseState;\n    String _boundary;\n    uint8_t _boundaryPosition;\n    size_t _itemStartIndex;\n    size_t _itemSize;\n    String _itemName;\n    String _itemFilename;\n    String _itemType;\n    String _itemValue;\n    uint8_t *_itemBuffer;\n    size_t _itemBufferIndex;\n    bool _itemIsFile;\n\n    esp_err_t _basicUploadHandler(PsychicRequest *request);\n    esp_err_t _multipartUploadHandler(PsychicRequest *request);\n\n    void _handleUploadByte(uint8_t data, bool last);\n    void _parseMultipartPostByte(uint8_t data, bool last);\n\n  public:\n    PsychicUploadHandler();\n    ~PsychicUploadHandler();\n\n    bool canHandle(PsychicRequest *request) override;\n    esp_err_t handleRequest(PsychicRequest *request) override;\n\n    PsychicUploadHandler * onUpload(PsychicUploadCallback fn);\n};\n\nenum {\n  EXPECT_BOUNDARY,\n  PARSE_HEADERS,\n  WAIT_FOR_RETURN1,\n  EXPECT_FEED1,\n  EXPECT_DASH1,\n  EXPECT_DASH2,\n  BOUNDARY_OR_DATA,\n  DASH3_OR_RETURN2,\n  EXPECT_FEED2,\n  PARSING_FINISHED,\n  PARSE_ERROR\n};\n\n#endif // PsychicUploadHandler_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicWebHandler.cpp",
    "content": "#include \"PsychicWebHandler.h\"\n\nPsychicWebHandler::PsychicWebHandler() : \n  PsychicHandler(),\n  _requestCallback(NULL),\n  _onOpen(NULL),\n  _onClose(NULL)\n  {}\nPsychicWebHandler::~PsychicWebHandler() {}\n\nbool PsychicWebHandler::canHandle(PsychicRequest *request) {\n  return true;\n}\n\nesp_err_t PsychicWebHandler::handleRequest(PsychicRequest *request)\n{\n  //lookup our client\n  PsychicClient *client = checkForNewClient(request->client());\n  if (client->isNew)\n    openCallback(client);\n\n  /* Request body cannot be larger than a limit */\n  if (request->contentLength() > request->server()->maxRequestBodySize)\n  {\n    ESP_LOGE(PH_TAG, \"Request body too large : %d bytes\", request->contentLength());\n\n    /* Respond with 400 Bad Request */\n    char error[60];\n    sprintf(error, \"Request body must be less than %lu bytes!\", request->server()->maxRequestBodySize);\n    httpd_resp_send_err(request->request(), HTTPD_400_BAD_REQUEST, error);\n\n    /* Return failure to close underlying connection else the incoming file content will keep the socket busy */\n    return ESP_FAIL;\n  }\n\n  //get our body loaded up.\n  esp_err_t err = request->loadBody();\n  if (err != ESP_OK)\n    return err;\n\n  //load our params in.\n  request->loadParams();\n\n  //okay, pass on to our callback.\n  if (this->_requestCallback != NULL)\n    err = this->_requestCallback(request);\n\n  return err;\n}\n\nPsychicWebHandler * PsychicWebHandler::onRequest(PsychicHttpRequestCallback fn) {\n  _requestCallback = fn;\n  return this;\n}\n\nvoid PsychicWebHandler::openCallback(PsychicClient *client) {\n  if (_onOpen != NULL)\n    _onOpen(client);\n}\n\nvoid PsychicWebHandler::closeCallback(PsychicClient *client) {\n  if (_onClose != NULL)\n    _onClose(getClient(client));\n}\n\nPsychicWebHandler * PsychicWebHandler::onOpen(PsychicClientCallback fn) {\n  _onOpen = fn;\n  return this;\n}\n\nPsychicWebHandler * PsychicWebHandler::onClose(PsychicClientCallback fn) {\n  _onClose = fn;\n  return this;\n}"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicWebHandler.h",
    "content": "#ifndef PsychicWebHandler_h\n#define PsychicWebHandler_h\n\n// #include \"PsychicCore.h\"\n// #include \"PsychicHttpServer.h\"\n// #include \"PsychicRequest.h\"\n#include \"PsychicHandler.h\"\n\n/*\n* HANDLER :: Can be attached to any endpoint or as a generic request handler.\n*/\n\nclass PsychicWebHandler : public PsychicHandler {\n  protected:\n    PsychicHttpRequestCallback _requestCallback;\n    PsychicClientCallback _onOpen;\n    PsychicClientCallback _onClose;\n\n  public:\n    PsychicWebHandler();\n    ~PsychicWebHandler();\n\n    virtual bool canHandle(PsychicRequest *request) override;\n    virtual esp_err_t handleRequest(PsychicRequest *request) override;\n    PsychicWebHandler * onRequest(PsychicHttpRequestCallback fn);\n\n    virtual void openCallback(PsychicClient *client);\n    virtual void closeCallback(PsychicClient *client);\n\n    PsychicWebHandler *onOpen(PsychicClientCallback fn);\n    PsychicWebHandler *onClose(PsychicClientCallback fn);\n};\n\n#endif"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicWebParameter.h",
    "content": "#ifndef PsychicWebParameter_h\n#define PsychicWebParameter_h\n\n/*\n * PARAMETER :: Chainable object to hold GET/POST and FILE parameters\n * */\n\nclass PsychicWebParameter {\n  private:\n    String _name;\n    String _value;\n    size_t _size;\n    bool _isForm;\n    bool _isFile;\n\n  public:\n    PsychicWebParameter(const String& name, const String& value, bool form=false, bool file=false, size_t size=0): _name(name), _value(value), _size(size), _isForm(form), _isFile(file){}\n    const String& name() const { return _name; }\n    const String& value() const { return _value; }\n    size_t size() const { return _size; }\n    bool isPost() const { return _isForm; }\n    bool isFile() const { return _isFile; }\n};\n\n#endif //PsychicWebParameter_h"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicWebSocket.cpp",
    "content": "#include \"PsychicWebSocket.h\"\n\n/*************************************/\n/*  PsychicWebSocketRequest      */\n/*************************************/\n\nPsychicWebSocketRequest::PsychicWebSocketRequest(PsychicRequest *req) :\n  PsychicRequest(req->server(), req->request()),\n  _client(req->client())\n{\n}\n\nPsychicWebSocketRequest::~PsychicWebSocketRequest()\n{\n}\n\nPsychicWebSocketClient * PsychicWebSocketRequest::client() {\n  return &_client;\n}\n\nesp_err_t PsychicWebSocketRequest::reply(httpd_ws_frame_t * ws_pkt)\n{\n  return httpd_ws_send_frame(this->_req, ws_pkt);\n} \n\nesp_err_t PsychicWebSocketRequest::reply(httpd_ws_type_t op, const void *data, size_t len)\n{\n  httpd_ws_frame_t ws_pkt;\n  memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));\n\n  ws_pkt.payload = (uint8_t*)data;\n  ws_pkt.len = len;\n  ws_pkt.type = op;\n\n  return this->reply(&ws_pkt);\n}\n\nesp_err_t PsychicWebSocketRequest::reply(const char *buf)\n{\n  return this->reply(HTTPD_WS_TYPE_TEXT, buf, strlen(buf));\n}\n\n/*************************************/\n/*  PsychicWebSocketClient   */\n/*************************************/\n\nPsychicWebSocketClient::PsychicWebSocketClient(PsychicClient *client)\n  : PsychicClient(client->server(), client->socket())\n{\n}\n\nPsychicWebSocketClient::~PsychicWebSocketClient() {\n}\n\nesp_err_t PsychicWebSocketClient::sendMessage(httpd_ws_frame_t * ws_pkt)\n{\n  return httpd_ws_send_frame_async(this->server(), this->socket(), ws_pkt);\n} \n\nesp_err_t PsychicWebSocketClient::sendMessage(httpd_ws_type_t op, const void *data, size_t len)\n{\n  httpd_ws_frame_t ws_pkt;\n  memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));\n\n  ws_pkt.payload = (uint8_t*)data;\n  ws_pkt.len = len;\n  ws_pkt.type = op;\n\n  return this->sendMessage(&ws_pkt);\n}\n\nesp_err_t PsychicWebSocketClient::sendMessage(const char *buf)\n{\n  return this->sendMessage(HTTPD_WS_TYPE_TEXT, buf, strlen(buf));\n}\n\nPsychicWebSocketHandler::PsychicWebSocketHandler() :\n  PsychicHandler(),\n  _onOpen(NULL),\n  _onFrame(NULL),\n  _onClose(NULL)\n  {\n  }\n\nPsychicWebSocketHandler::~PsychicWebSocketHandler() {\n}\n\nPsychicWebSocketClient * PsychicWebSocketHandler::getClient(int socket)\n{\n  PsychicClient *client = PsychicHandler::getClient(socket);\n  if (client == NULL)\n    return NULL;\n\n  if (client->_friend == NULL)\n  {\n    return NULL;\n  }\n\n  return (PsychicWebSocketClient *)client->_friend;\n}\n\nPsychicWebSocketClient * PsychicWebSocketHandler::getClient(PsychicClient *client) {\n  return getClient(client->socket());\n}\n\nvoid PsychicWebSocketHandler::addClient(PsychicClient *client) {\n  client->_friend = new PsychicWebSocketClient(client);\n  PsychicHandler::addClient(client);\n}\n\nvoid PsychicWebSocketHandler::removeClient(PsychicClient *client) {\n  PsychicHandler::removeClient(client);\n  delete (PsychicWebSocketClient*)client->_friend;\n  client->_friend = NULL;\n}\n\nvoid PsychicWebSocketHandler::openCallback(PsychicClient *client) {\n  PsychicWebSocketClient *buddy = getClient(client);\n  if (buddy == NULL)\n  {\n    return;\n  }\n\n  if (_onOpen != NULL)\n    _onOpen(getClient(buddy));\n}\n\nvoid PsychicWebSocketHandler::closeCallback(PsychicClient *client) {\n  PsychicWebSocketClient *buddy = getClient(client);\n  if (buddy == NULL)\n  {\n    return;\n  }\n\n  if (_onClose != NULL)\n    _onClose(getClient(buddy));\n}\n\nbool PsychicWebSocketHandler::isWebSocket() { return true; }\n\nesp_err_t PsychicWebSocketHandler::handleRequest(PsychicRequest *request)\n{\n  //lookup our client\n  PsychicClient *client = checkForNewClient(request->client());\n\n  // beginning of the ws URI handler and our onConnect hook\n  if (request->method() == HTTP_GET)\n  {\n    if (client->isNew)\n      openCallback(client);\n\n    return ESP_OK;\n  }\n\n  //prep our request\n  PsychicWebSocketRequest wsRequest(request);\n\n  //init our memory for storing the packet\n  httpd_ws_frame_t ws_pkt;\n  memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));\n  ws_pkt.type = HTTPD_WS_TYPE_TEXT;\n  uint8_t *buf = NULL;\n\n  /* Set max_len = 0 to get the frame len */\n  esp_err_t ret = httpd_ws_recv_frame(wsRequest.request(), &ws_pkt, 0);\n  if (ret != ESP_OK) {\n    ESP_LOGE(PH_TAG, \"httpd_ws_recv_frame failed to get frame len with %s\", esp_err_to_name(ret));\n    return ret;\n  }\n\n  //okay, now try to load the packet\n  //ESP_LOGD(PH_TAG, \"frame len is %d\", ws_pkt.len);\n  if (ws_pkt.len) {\n    /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */\n    buf = (uint8_t*) calloc(1, ws_pkt.len + 1);\n    if (buf == NULL) {\n      ESP_LOGE(PH_TAG, \"Failed to calloc memory for buf\");\n      return ESP_ERR_NO_MEM;\n    }\n    ws_pkt.payload = buf;\n    /* Set max_len = ws_pkt.len to get the frame payload */\n    ret = httpd_ws_recv_frame(wsRequest.request(), &ws_pkt, ws_pkt.len);\n    if (ret != ESP_OK) {\n      ESP_LOGE(PH_TAG, \"httpd_ws_recv_frame failed with %s\", esp_err_to_name(ret));\n      free(buf);\n      return ret;\n    }\n    //ESP_LOGD(PH_TAG, \"Got packet with message: %s\", ws_pkt.payload);\n  }\n\n  // Text messages are our payload.\n  if (ws_pkt.type == HTTPD_WS_TYPE_TEXT || ws_pkt.type == HTTPD_WS_TYPE_BINARY)\n  {\n    if (this->_onFrame != NULL)\n      ret = this->_onFrame(&wsRequest, &ws_pkt);\n  }\n\n  //logging housekeeping\n  if (ret != ESP_OK)\n    ESP_LOGE(PH_TAG, \"httpd_ws_send_frame failed with %s\", esp_err_to_name(ret));\n    // ESP_LOGD(PH_TAG, \"ws_handler: httpd_handle_t=%p, sockfd=%d, client_info:%d\", \n    //   request->server(),\n    //   httpd_req_to_sockfd(request->request()),\n    //   httpd_ws_get_fd_info(request->server()->server, httpd_req_to_sockfd(request->request())));\n\n  //dont forget to release our buffer memory\n  free(buf);\n\n  return ret;\n}\n\nPsychicWebSocketHandler * PsychicWebSocketHandler::onOpen(PsychicWebSocketClientCallback fn) {\n  _onOpen = fn;\n  return this;\n}\n\nPsychicWebSocketHandler * PsychicWebSocketHandler::onFrame(PsychicWebSocketFrameCallback fn) {\n  _onFrame = fn;\n  return this;\n}\n\nPsychicWebSocketHandler * PsychicWebSocketHandler::onClose(PsychicWebSocketClientCallback fn) {\n  _onClose = fn;\n  return this;\n}\n\nvoid PsychicWebSocketHandler::sendAll(httpd_ws_frame_t * ws_pkt)\n{\n  for (PsychicClient *client : _clients)\n  {\n    //ESP_LOGD(PH_TAG, \"Active client (fd=%d) -> sending async message\", client->socket());\n\n    if (client->_friend == NULL)\n    {\n      return;\n    }\n\n    if (((PsychicWebSocketClient*)client->_friend)->sendMessage(ws_pkt) != ESP_OK)\n      break;\n  }\n}\n\nvoid PsychicWebSocketHandler::sendAll(httpd_ws_type_t op, const void *data, size_t len)\n{\n  httpd_ws_frame_t ws_pkt;\n  memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));\n\n  ws_pkt.payload = (uint8_t*)data;\n  ws_pkt.len = len;\n  ws_pkt.type = op;\n\n  this->sendAll(&ws_pkt);\n}\n\nvoid PsychicWebSocketHandler::sendAll(const char *buf)\n{\n  this->sendAll(HTTPD_WS_TYPE_TEXT, buf, strlen(buf));\n}\n"
  },
  {
    "path": "lib/PsychicHttp/src/PsychicWebSocket.h",
    "content": "#ifndef PsychicWebSocket_h\n#define PsychicWebSocket_h\n\n#include \"PsychicCore.h\"\n#include \"PsychicRequest.h\"\n\nclass PsychicWebSocketRequest;\nclass PsychicWebSocketClient;\n\n//callback function definitions\ntypedef std::function<void(PsychicWebSocketClient *client)> PsychicWebSocketClientCallback;\ntypedef std::function<esp_err_t(PsychicWebSocketRequest *request, httpd_ws_frame *frame)> PsychicWebSocketFrameCallback;\n\nclass PsychicWebSocketClient : public PsychicClient\n{\n  public:\n    PsychicWebSocketClient(PsychicClient *client);\n    ~PsychicWebSocketClient();\n    \n    esp_err_t sendMessage(httpd_ws_frame_t * ws_pkt);\n    esp_err_t sendMessage(httpd_ws_type_t op, const void *data, size_t len);\n    esp_err_t sendMessage(const char *buf);\n};\n\nclass PsychicWebSocketRequest : public PsychicRequest\n{\n  private:\n    PsychicWebSocketClient _client;\n\n  public:\n    PsychicWebSocketRequest(PsychicRequest *req);\n    virtual ~PsychicWebSocketRequest();\n\n    PsychicWebSocketClient * client() override;\n\n    esp_err_t reply(httpd_ws_frame_t * ws_pkt);\n    esp_err_t reply(httpd_ws_type_t op, const void *data, size_t len);\n    esp_err_t reply(const char *buf);\n};\n\nclass PsychicWebSocketHandler : public PsychicHandler {\n  protected:\n    PsychicWebSocketClientCallback _onOpen;\n    PsychicWebSocketFrameCallback _onFrame;\n    PsychicWebSocketClientCallback _onClose;\n\n  public:\n    PsychicWebSocketHandler();\n    ~PsychicWebSocketHandler();\n\n    PsychicWebSocketClient * getClient(int socket) override;\n    PsychicWebSocketClient * getClient(PsychicClient *client) override;\n    void addClient(PsychicClient *client) override;\n    void removeClient(PsychicClient *client) override;\n    void openCallback(PsychicClient *client) override;\n    void closeCallback(PsychicClient *client) override;\n\n    bool isWebSocket() override final;\n    esp_err_t handleRequest(PsychicRequest *request) override;\n\n    PsychicWebSocketHandler *onOpen(PsychicWebSocketClientCallback fn);\n    PsychicWebSocketHandler *onFrame(PsychicWebSocketFrameCallback fn);\n    PsychicWebSocketHandler *onClose(PsychicWebSocketClientCallback fn);\n\n    void sendAll(httpd_ws_frame_t * ws_pkt);\n    void sendAll(httpd_ws_type_t op, const void *data, size_t len);\n    void sendAll(const char *buf);\n};\n\n#endif // PsychicWebSocket_h"
  },
  {
    "path": "lib/PsychicHttp/src/TemplatePrinter.cpp",
    "content": "  /************************************************************\n  \n\tTemplatePrinter Class\n\t\n\tA basic templating engine for a stream of text.\n\tThis wraps the Arduino Print interface and writes to any\n\tPrint interface.\n\t\n\tWritten by Christopher Andrews (https://github.com/Chris--A) \n  \n  ************************************************************/\n  \n#include \"TemplatePrinter.h\"\n\nvoid TemplatePrinter::resetParam(bool flush){\n  if(flush && _inParam){\n    _stream.write(_delimiter);\n    \n    if(_paramPos)\n      _stream.print(_paramBuffer);\n  }\n  \n  memset(_paramBuffer, 0, sizeof(_paramBuffer));\n  _paramPos = 0;\n  _inParam = false;\n}\n\n\nvoid TemplatePrinter::flush(){ \n  resetParam(true);\n  _stream.flush();\n}\n\nsize_t TemplatePrinter::write(uint8_t data){\n  \n  if(data == _delimiter){\n    \n    // End of parameter, send to callback\n    if(_inParam){\n      \n      // On false, return the parameter place holder as is: not a parameter\n\t  // Bug fix: ignore parameters that are zero length.\n      if(!_paramPos || !_cb(_stream, _paramBuffer)){\n        resetParam(true);\n        _stream.write(data);\n      }else{\n        resetParam(false);\n      }\n      \n    // Start collecting parameter\n    }else{\n      _inParam = true;\n    }\n  }else{\n    \n    // Are we collecting\n    if(_inParam){\n      \n      // Is param still valid\n      if(isalnum(data) || data == '_'){\n        \n        // Total param len must be 63, 1 for null.\n        if(_paramPos < sizeof(_paramBuffer) - 1){\n          _paramBuffer[_paramPos++] = data;\n          \n        // Not a valid param\n        }else{\n          resetParam(true);\n        }\n      }else{\n        resetParam(true);\n        _stream.write(data);\n      }\n\n    // Just output\n    }else{\n      _stream.write(data);\n    }\n  }\n  return 1;\n}\n    \nsize_t TemplatePrinter::copyFrom(Stream &stream){\n  size_t count = 0;\n  \n  while(stream.available())\n    count += this->write(stream.read());\n  \n  return count;\n}\n"
  },
  {
    "path": "lib/PsychicHttp/src/TemplatePrinter.h",
    "content": "#ifndef TemplatePrinter_h\n  #define TemplatePrinter_h\n\n  #include \"PsychicCore.h\"\n  #include <Print.h>\n  \n  /************************************************************\n  \n\tTemplatePrinter Class\n\t\n\tA basic templating engine for a stream of text.\n\tThis wraps the Arduino Print interface and writes to any\n\tPrint interface.\n\t\n\tWritten by Christopher Andrews (https://github.com/Chris--A) \n  \n  ************************************************************/\n  \n  class TemplatePrinter;\n\n  typedef std::function<bool(Print &output, const char *parameter)> TemplateCallback;\n  typedef std::function<void(TemplatePrinter &printer)> TemplateSourceCallback;\n\n  class TemplatePrinter : public Print{\n    private:\n      bool _inParam;\n      char _paramBuffer[64];\n      uint8_t _paramPos;\n      Print &_stream;\n      TemplateCallback _cb;\n      char _delimiter;\n    \n      void resetParam(bool flush);\n      \n    public:\n      using Print::write;\n\n      static void start(Print &stream, TemplateCallback cb, TemplateSourceCallback entry){\n        TemplatePrinter printer(stream, cb);\n        entry(printer);\n      }\n\n      TemplatePrinter(Print &stream, TemplateCallback cb, const char delimeter = '%') : _stream(stream), _cb(cb), _delimiter(delimeter) { resetParam(false); }\n      ~TemplatePrinter(){ flush(); }\n\n      void flush() override;\n      size_t write(uint8_t data) override;\n      size_t copyFrom(Stream &stream);\n  };\n\n#endif\n"
  },
  {
    "path": "lib/PsychicHttp/src/http_status.cpp",
    "content": "#include \"http_status.h\"\n\nbool http_informational(int code)\n{\n    return code >= 100 && code < 200;\n}\n\nbool http_success(int code)\n{\n    return code >= 200 && code < 300;\n}\n\nbool http_redirection(int code)\n{\n    return code >= 300 && code < 400;\n}\n\nbool http_client_error(int code)\n{\n    return code >= 400 && code < 500;\n}\n\nbool http_server_error(int code)\n{\n    return code >= 500 && code < 600;\n}\n\nbool http_failure(int code)\n{\n    return code >= 400 && code < 600;\n}\n\nconst char *http_status_group(int code)\n{\n    if (http_informational(code))\n        return \"Informational\";\n\n    if (http_success(code))\n        return \"Success\";\n\n    if (http_redirection(code))\n        return \"Redirection\";\n\n    if (http_client_error(code))\n        return \"Client Error\";\n\n    if (http_server_error(code))\n        return \"Server Error\";\n\n    return \"Unknown\";\n}\n\nconst char *http_status_reason(int code)\n{\n    switch (code)\n    {\n    /*####### 1xx - Informational #######*/\n    case 100:\n        return \"Continue\";\n    case 101:\n        return \"Switching Protocols\";\n    case 102:\n        return \"Processing\";\n    case 103:\n        return \"Early Hints\";\n\n    /*####### 2xx - Successful #######*/\n    case 200:\n        return \"OK\";\n    case 201:\n        return \"Created\";\n    case 202:\n        return \"Accepted\";\n    case 203:\n        return \"Non-Authoritative Information\";\n    case 204:\n        return \"No Content\";\n    case 205:\n        return \"Reset Content\";\n    case 206:\n        return \"Partial Content\";\n    case 207:\n        return \"Multi-Status\";\n    case 208:\n        return \"Already Reported\";\n    case 226:\n        return \"IM Used\";\n\n    /*####### 3xx - Redirection #######*/\n    case 300:\n        return \"Multiple Choices\";\n    case 301:\n        return \"Moved Permanently\";\n    case 302:\n        return \"Found\";\n    case 303:\n        return \"See Other\";\n    case 304:\n        return \"Not Modified\";\n    case 305:\n        return \"Use Proxy\";\n    case 307:\n        return \"Temporary Redirect\";\n    case 308:\n        return \"Permanent Redirect\";\n\n    /*####### 4xx - Client Error #######*/\n    case 400:\n        return \"Bad Request\";\n    case 401:\n        return \"Unauthorized\";\n    case 402:\n        return \"Payment Required\";\n    case 403:\n        return \"Forbidden\";\n    case 404:\n        return \"Not Found\";\n    case 405:\n        return \"Method Not Allowed\";\n    case 406:\n        return \"Not Acceptable\";\n    case 407:\n        return \"Proxy Authentication Required\";\n    case 408:\n        return \"Request Timeout\";\n    case 409:\n        return \"Conflict\";\n    case 410:\n        return \"Gone\";\n    case 411:\n        return \"Length Required\";\n    case 412:\n        return \"Precondition Failed\";\n    case 413:\n        return \"Content Too Large\";\n    case 414:\n        return \"URI Too Long\";\n    case 415:\n        return \"Unsupported Media Type\";\n    case 416:\n        return \"Range Not Satisfiable\";\n    case 417:\n        return \"Expectation Failed\";\n    case 418:\n        return \"I'm a teapot\";\n    case 421:\n        return \"Misdirected Request\";\n    case 422:\n        return \"Unprocessable Content\";\n    case 423:\n        return \"Locked\";\n    case 424:\n        return \"Failed Dependency\";\n    case 425:\n        return \"Too Early\";\n    case 426:\n        return \"Upgrade Required\";\n    case 428:\n        return \"Precondition Required\";\n    case 429:\n        return \"Too Many Requests\";\n    case 431:\n        return \"Request Header Fields Too Large\";\n    case 451:\n        return \"Unavailable For Legal Reasons\";\n\n    /*####### 5xx - Server Error #######*/\n    case 500:\n        return \"Internal Server Error\";\n    case 501:\n        return \"Not Implemented\";\n    case 502:\n        return \"Bad Gateway\";\n    case 503:\n        return \"Service Unavailable\";\n    case 504:\n        return \"Gateway Timeout\";\n    case 505:\n        return \"HTTP Version Not Supported\";\n    case 506:\n        return \"Variant Also Negotiates\";\n    case 507:\n        return \"Insufficient Storage\";\n    case 508:\n        return \"Loop Detected\";\n    case 510:\n        return \"Not Extended\";\n    case 511:\n        return \"Network Authentication Required\";\n\n    default:\n        return \"Unknown\";\n    }\n}"
  },
  {
    "path": "lib/PsychicHttp/src/http_status.h",
    "content": "#ifndef MICRO_HTTP_STATUS_H\n#define MICRO_HTTP_STATUS_H\n\n#include <stdbool.h>\n\nbool http_informational(int code);\nbool http_success(int code);\nbool http_redirection(int code);\nbool http_client_error(int code);\nbool http_server_error(int code);\nbool http_failure(int code);\nconst char *http_status_group(int code);\nconst char *http_status_reason(int code);\n\n#endif // MICRO_HTTP_STATUS_H"
  },
  {
    "path": "lib/framework/APSettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <APSettingsService.h>\n\nAPSettingsService::APSettingsService(PsychicHttpServer *server,\n                                     FS *fs,\n                                     SecurityManager *securityManager) : _server(server),\n                                                                         _securityManager(securityManager),\n                                                                         _httpEndpoint(APSettings::read, APSettings::update, this, server, AP_SETTINGS_SERVICE_PATH, securityManager),\n                                                                         _fsPersistence(APSettings::read, APSettings::update, this, fs, AP_SETTINGS_FILE),\n                                                                         _dnsServer(nullptr),\n                                                                         _lastManaged(0),\n                                                                         _reconfigureAp(false)\n{\n    addUpdateHandler([&](const String &originId)\n                     { reconfigureAP(); },\n                     false);\n}\n\nvoid APSettingsService::begin()\n{\n    _httpEndpoint.begin();\n    _fsPersistence.readFromFS();\n    reconfigureAP();\n}\n\nvoid APSettingsService::reconfigureAP()\n{\n    _lastManaged = millis() - MANAGE_NETWORK_DELAY;\n    _reconfigureAp = true;\n    _recoveryMode = false;\n}\n\nvoid APSettingsService::recoveryMode()\n{\n#ifdef SERIAL_INFO\n    Serial.println(\"Recovery Mode needed\");\n#endif\n    _lastManaged = millis() - MANAGE_NETWORK_DELAY;\n    _recoveryMode = true;\n    _reconfigureAp = true;\n}\n\nvoid APSettingsService::loop()\n{\n    unsigned long currentMillis = millis();\n    unsigned long manageElapsed = (unsigned long)(currentMillis - _lastManaged);\n    if (manageElapsed >= MANAGE_NETWORK_DELAY)\n    {\n        _lastManaged = currentMillis;\n        manageAP();\n    }\n    handleDNS();\n}\n\nvoid APSettingsService::manageAP()\n{\n    WiFiMode_t currentWiFiMode = WiFi.getMode();\n    if (_state.provisionMode == AP_MODE_ALWAYS ||\n        (_state.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED) || _recoveryMode)\n    {\n        if (_reconfigureAp || currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA)\n        {\n            startAP();\n        }\n    }\n    else if ((currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA) &&\n             (_reconfigureAp || !WiFi.softAPgetStationNum()))\n    {\n        stopAP();\n    }\n    _reconfigureAp = false;\n}\n\nvoid APSettingsService::startAP()\n{\n#ifdef SERIAL_INFO\n    Serial.println(\"Starting software access point\");\n#endif\n    WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);\n    WiFi.softAP(_state.ssid.c_str(), _state.password.c_str(), _state.channel, _state.ssidHidden, _state.maxClients);\n#if CONFIG_IDF_TARGET_ESP32C3\n    WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi\n#endif\n    if (!_dnsServer)\n    {\n        IPAddress apIp = WiFi.softAPIP();\n#ifdef SERIAL_INFO\n        Serial.print(\"Starting captive portal on \");\n        Serial.println(apIp);\n#endif\n        _dnsServer = new DNSServer;\n        _dnsServer->start(DNS_PORT, \"*\", apIp);\n    }\n}\n\nvoid APSettingsService::stopAP()\n{\n    if (_dnsServer)\n    {\n#ifdef SERIAL_INFO\n        Serial.println(\"Stopping captive portal\");\n#endif\n        _dnsServer->stop();\n        delete _dnsServer;\n        _dnsServer = nullptr;\n    }\n#ifdef SERIAL_INFO\n    Serial.println(\"Stopping software access point\");\n#endif\n    WiFi.softAPdisconnect(true);\n}\n\nvoid APSettingsService::handleDNS()\n{\n    if (_dnsServer)\n    {\n        _dnsServer->processNextRequest();\n    }\n}\n\nAPNetworkStatus APSettingsService::getAPNetworkStatus()\n{\n    WiFiMode_t currentWiFiMode = WiFi.getMode();\n    bool apActive = currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA;\n    if (apActive && _state.provisionMode != AP_MODE_ALWAYS && WiFi.status() == WL_CONNECTED)\n    {\n        return APNetworkStatus::LINGERING;\n    }\n    return apActive ? APNetworkStatus::ACTIVE : APNetworkStatus::INACTIVE;\n}\n"
  },
  {
    "path": "lib/framework/APSettingsService.h",
    "content": "#ifndef APSettingsConfig_h\n#define APSettingsConfig_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <SettingValue.h>\n#include <HttpEndpoint.h>\n#include <FSPersistence.h>\n#include <JsonUtils.h>\n#include <WiFi.h>\n\n#include <DNSServer.h>\n#include <IPAddress.h>\n\n#ifndef FACTORY_AP_PROVISION_MODE\n#define FACTORY_AP_PROVISION_MODE AP_MODE_DISCONNECTED\n#endif\n\n#ifndef FACTORY_AP_SSID\n#define FACTORY_AP_SSID \"ESP32-SvelteKit-#{unique_id}\"\n#endif\n\n#ifndef FACTORY_AP_PASSWORD\n#define FACTORY_AP_PASSWORD \"esp-sveltekit\"\n#endif\n\n#ifndef FACTORY_AP_LOCAL_IP\n#define FACTORY_AP_LOCAL_IP \"192.168.4.1\"\n#endif\n\n#ifndef FACTORY_AP_GATEWAY_IP\n#define FACTORY_AP_GATEWAY_IP \"192.168.4.1\"\n#endif\n\n#ifndef FACTORY_AP_SUBNET_MASK\n#define FACTORY_AP_SUBNET_MASK \"255.255.255.0\"\n#endif\n\n#ifndef FACTORY_AP_CHANNEL\n#define FACTORY_AP_CHANNEL 1\n#endif\n\n#ifndef FACTORY_AP_SSID_HIDDEN\n#define FACTORY_AP_SSID_HIDDEN false\n#endif\n\n#ifndef FACTORY_AP_MAX_CLIENTS\n#define FACTORY_AP_MAX_CLIENTS 4\n#endif\n\n#define AP_SETTINGS_FILE \"/config/apSettings.json\"\n#define AP_SETTINGS_SERVICE_PATH \"/rest/apSettings\"\n\n#define AP_MODE_ALWAYS 0\n#define AP_MODE_DISCONNECTED 1\n#define AP_MODE_NEVER 2\n\n#define MANAGE_NETWORK_DELAY 10000\n#define DNS_PORT 53\n\nenum APNetworkStatus\n{\n    ACTIVE = 0,\n    INACTIVE,\n    LINGERING\n};\n\nclass APSettings\n{\npublic:\n    uint8_t provisionMode;\n    String ssid;\n    String password;\n    uint8_t channel;\n    bool ssidHidden;\n    uint8_t maxClients;\n\n    IPAddress localIP;\n    IPAddress gatewayIP;\n    IPAddress subnetMask;\n\n    bool operator==(const APSettings &settings) const\n    {\n        return provisionMode == settings.provisionMode && ssid == settings.ssid && password == settings.password &&\n               channel == settings.channel && ssidHidden == settings.ssidHidden && maxClients == settings.maxClients &&\n               localIP == settings.localIP && gatewayIP == settings.gatewayIP && subnetMask == settings.subnetMask;\n    }\n\n    static void read(APSettings &settings, JsonObject &root)\n    {\n        root[\"provision_mode\"] = settings.provisionMode;\n        root[\"ssid\"] = settings.ssid;\n        root[\"password\"] = settings.password;\n        root[\"channel\"] = settings.channel;\n        root[\"ssid_hidden\"] = settings.ssidHidden;\n        root[\"max_clients\"] = settings.maxClients;\n        root[\"local_ip\"] = settings.localIP.toString();\n        root[\"gateway_ip\"] = settings.gatewayIP.toString();\n        root[\"subnet_mask\"] = settings.subnetMask.toString();\n    }\n\n    static StateUpdateResult update(JsonObject &root, APSettings &settings, const String &originId)\n    {\n        APSettings newSettings = {};\n        newSettings.provisionMode = root[\"provision_mode\"] | FACTORY_AP_PROVISION_MODE;\n        switch (settings.provisionMode)\n        {\n        case AP_MODE_ALWAYS:\n        case AP_MODE_DISCONNECTED:\n        case AP_MODE_NEVER:\n            break;\n        default:\n            newSettings.provisionMode = AP_MODE_DISCONNECTED;\n        }\n        newSettings.ssid = root[\"ssid\"] | SettingValue::format(FACTORY_AP_SSID);\n        newSettings.password = root[\"password\"] | FACTORY_AP_PASSWORD;\n        newSettings.channel = root[\"channel\"] | FACTORY_AP_CHANNEL;\n        newSettings.ssidHidden = root[\"ssid_hidden\"] | FACTORY_AP_SSID_HIDDEN;\n        newSettings.maxClients = root[\"max_clients\"] | FACTORY_AP_MAX_CLIENTS;\n\n        JsonUtils::readIPStr(root, \"local_ip\", newSettings.localIP, FACTORY_AP_LOCAL_IP);\n        JsonUtils::readIPStr(root, \"gateway_ip\", newSettings.gatewayIP, FACTORY_AP_GATEWAY_IP);\n        JsonUtils::readIPStr(root, \"subnet_mask\", newSettings.subnetMask, FACTORY_AP_SUBNET_MASK);\n\n        if (newSettings == settings)\n        {\n            return StateUpdateResult::UNCHANGED;\n        }\n        settings = newSettings;\n        return StateUpdateResult::CHANGED;\n    }\n};\n\nclass APSettingsService : public StatefulService<APSettings>\n{\npublic:\n    APSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);\n\n    void begin();\n    void loop();\n    APNetworkStatus getAPNetworkStatus();\n    void recoveryMode();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    HttpEndpoint<APSettings> _httpEndpoint;\n    FSPersistence<APSettings> _fsPersistence;\n\n    // for the captive portal\n    DNSServer *_dnsServer;\n\n    // for the mangement delay loop\n    volatile unsigned long _lastManaged;\n    volatile boolean _reconfigureAp;\n    volatile boolean _recoveryMode = false;\n\n    void reconfigureAP();\n    void manageAP();\n    void startAP();\n    void stopAP();\n    void handleDNS();\n};\n\n#endif // end APSettingsConfig_h\n"
  },
  {
    "path": "lib/framework/APStatus.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <APStatus.h>\n\nAPStatus::APStatus(PsychicHttpServer *server,\n                   SecurityManager *securityManager,\n                   APSettingsService *apSettingsService) : _server(server),\n                                                           _securityManager(securityManager),\n                                                           _apSettingsService(apSettingsService)\n{\n}\nvoid APStatus::begin()\n{\n    _server->on(AP_STATUS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", AP_STATUS_SERVICE_PATH);\n}\n\nesp_err_t APStatus::apStatus(PsychicRequest *request)\n{\n    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n    JsonObject root = response.getRoot();\n\n    root[\"status\"] = _apSettingsService->getAPNetworkStatus();\n    root[\"ip_address\"] = WiFi.softAPIP().toString();\n    root[\"mac_address\"] = WiFi.softAPmacAddress();\n    root[\"station_num\"] = WiFi.softAPgetStationNum();\n\n    return response.send();\n}\n\nbool APStatus::isActive()\n{\n    return _apSettingsService->getAPNetworkStatus() == APNetworkStatus::ACTIVE ? true : false;\n}\n"
  },
  {
    "path": "lib/framework/APStatus.h",
    "content": "#ifndef APStatus_h\n#define APStatus_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <IPAddress.h>\n#include <SecurityManager.h>\n#include <APSettingsService.h>\n\n#define AP_STATUS_SERVICE_PATH \"/rest/apStatus\"\n\nclass APStatus\n{\npublic:\n    APStatus(PsychicHttpServer *server, SecurityManager *securityManager, APSettingsService *apSettingsService);\n\n    void begin();\n\n    bool isActive();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    APSettingsService *_apSettingsService;\n    esp_err_t apStatus(PsychicRequest *request);\n};\n\n#endif // end APStatus_h\n"
  },
  {
    "path": "lib/framework/AnalyticsService.h",
    "content": "#pragma once\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n#include <ArduinoJson.h>\n#include <ESPFS.h>\n#include <EventSocket.h>\n\n#define MAX_ESP_ANALYTICS_SIZE 1024\n#define EVENT_ANALYTICS \"analytics\"\n#define ANALYTICS_INTERVAL 2000\n\nclass AnalyticsService\n{\npublic:\n    AnalyticsService(EventSocket *socket) : _socket(socket) {};\n\n    void begin()\n    {\n        _socket->registerEvent(EVENT_ANALYTICS);\n    }\n\n    void loop()\n    {\n        if (millis() - lastMillis > ANALYTICS_INTERVAL)\n        {\n            lastMillis = millis();\n            JsonDocument doc;\n            doc[\"uptime\"] = millis() / 1000;\n            doc[\"free_heap\"] = ESP.getFreeHeap();\n            doc[\"used_heap\"] = ESP.getHeapSize() - ESP.getFreeHeap();\n            doc[\"total_heap\"] = ESP.getHeapSize();\n            doc[\"min_free_heap\"] = ESP.getMinFreeHeap();\n            doc[\"max_alloc_heap\"] = ESP.getMaxAllocHeap();\n            doc[\"fs_used\"] = ESPFS.usedBytes();\n            doc[\"fs_total\"] = ESPFS.totalBytes();\n            doc[\"core_temp\"] = temperatureRead();\n            if (psramFound())\n            {\n                doc[\"free_psram\"] = ESP.getFreePsram();\n                doc[\"used_psram\"] = ESP.getPsramSize() - ESP.getFreePsram();\n                doc[\"psram_size\"] = ESP.getPsramSize();\n            }\n\n            JsonObject jsonObject = doc.as<JsonObject>();\n            _socket->emitEvent(EVENT_ANALYTICS, jsonObject);\n        }\n    };\n\nprotected:\n    EventSocket *_socket;\n\n    unsigned long lastMillis = 0;\n};\n"
  },
  {
    "path": "lib/framework/ArduinoJsonJWT.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include \"ArduinoJsonJWT.h\"\n\nArduinoJsonJWT::ArduinoJsonJWT(String secret) : _secret(secret)\n{\n}\n\nvoid ArduinoJsonJWT::setSecret(String secret)\n{\n    _secret = secret;\n}\n\nString ArduinoJsonJWT::getSecret()\n{\n    return _secret;\n}\n\n/*\n * ESP32 uses mbedtls,\n *\n * Both come with decent HMAC implmentations supporting sha256, as well as others.\n *\n * No need to pull in additional crypto libraries - lets use what we already have.\n */\nString ArduinoJsonJWT::sign(String &payload)\n{\n    unsigned char hmacResult[32];\n    {\n        mbedtls_md_context_t ctx;\n        mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;\n        mbedtls_md_init(&ctx);\n        mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);\n        mbedtls_md_hmac_starts(&ctx, (unsigned char *)_secret.c_str(), _secret.length());\n        mbedtls_md_hmac_update(&ctx, (unsigned char *)payload.c_str(), payload.length());\n        mbedtls_md_hmac_finish(&ctx, hmacResult);\n        mbedtls_md_free(&ctx);\n    }\n    return encode((char *)hmacResult, 32);\n}\n\nString ArduinoJsonJWT::buildJWT(JsonObject &payload)\n{\n    // serialize, then encode payload\n    String jwt;\n    serializeJson(payload, jwt);\n    jwt = encode(jwt.c_str(), jwt.length());\n\n    // add the header to payload\n    jwt = JWT_HEADER + '.' + jwt;\n\n    // add signature\n    jwt += '.' + sign(jwt);\n\n    return jwt;\n}\n\nvoid ArduinoJsonJWT::parseJWT(String jwt, JsonDocument &jsonDocument)\n{\n    // clear json document before we begin, jsonDocument wil be null on failure\n    jsonDocument.clear();\n\n    // must have the correct header and delimiter\n    if (!jwt.startsWith(JWT_HEADER) || jwt.indexOf('.') != JWT_HEADER_SIZE)\n    {\n        return;\n    }\n\n    // check there is a signature delimieter\n    int signatureDelimiterIndex = jwt.lastIndexOf('.');\n    if (signatureDelimiterIndex == JWT_HEADER_SIZE)\n    {\n        return;\n    }\n\n    // check the signature is valid\n    String signature = jwt.substring(signatureDelimiterIndex + 1);\n    jwt = jwt.substring(0, signatureDelimiterIndex);\n    if (sign(jwt) != signature)\n    {\n        return;\n    }\n\n    // decode payload\n    jwt = jwt.substring(JWT_HEADER_SIZE + 1);\n    jwt = decode(jwt);\n\n    // parse payload, clearing json document after failure\n    DeserializationError error = deserializeJson(jsonDocument, jwt);\n    if (error != DeserializationError::Ok || !jsonDocument.is<JsonObject>())\n    {\n        jsonDocument.clear();\n    }\n}\n\nString ArduinoJsonJWT::encode(const char *cstr, int inputLen)\n{\n    // prepare encoder\n    base64_encodestate _state;\n    base64_init_encodestate(&_state);\n    size_t encodedLength = base64_encode_expected_len(inputLen) + 1;\n    // prepare buffer of correct length, returning an empty string on failure\n    char *buffer = (char *)malloc(encodedLength * sizeof(char));\n    if (buffer == nullptr)\n    {\n        return \"\";\n    }\n\n    // encode to buffer\n    int len = base64_encode_block(cstr, inputLen, &buffer[0], &_state);\n    len += base64_encode_blockend(&buffer[len], &_state);\n    buffer[len] = 0;\n\n    // convert to arduino string, freeing buffer\n    String value = String(buffer);\n    free(buffer);\n    buffer = nullptr;\n\n    // remove padding and convert to URL safe form\n    while (value.length() > 0 && value.charAt(value.length() - 1) == '=')\n    {\n        value.remove(value.length() - 1);\n    }\n    value.replace('+', '-');\n    value.replace('/', '_');\n\n    // return as string\n    return value;\n}\n\nString ArduinoJsonJWT::decode(String value)\n{\n    // convert to standard base64\n    value.replace('-', '+');\n    value.replace('_', '/');\n\n    // prepare buffer of correct length\n    char buffer[base64_decode_expected_len(value.length()) + 1];\n\n    // decode\n    int len = base64_decode_chars(value.c_str(), value.length(), &buffer[0]);\n    buffer[len] = 0;\n\n    // return as string\n    return String(buffer);\n}\n"
  },
  {
    "path": "lib/framework/ArduinoJsonJWT.h",
    "content": "#ifndef ArduinoJsonJWT_H\n#define ArduinoJsonJWT_H\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <libb64/cdecode.h>\n#include <libb64/cencode.h>\n#include <mbedtls/md.h>\n\nclass ArduinoJsonJWT\n{\nprivate:\n    String _secret;\n\n    const String JWT_HEADER = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\";\n    const int JWT_HEADER_SIZE = JWT_HEADER.length();\n\n    String sign(String &value);\n\n    static String encode(const char *cstr, int len);\n    static String decode(String value);\n\npublic:\n    ArduinoJsonJWT(String secret);\n\n    void setSecret(String secret);\n    String getSecret();\n\n    String buildJWT(JsonObject &payload);\n    void parseJWT(String jwt, JsonDocument &jsonDocument);\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/AuthenticationService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <AuthenticationService.h>\n\n#if FT_ENABLED(FT_SECURITY)\n\nAuthenticationService::AuthenticationService(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server),\n                                                                                                            _securityManager(securityManager)\n{\n}\n\nvoid AuthenticationService::begin()\n{\n    // Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in subsequent requests\n    _server->on(SIGN_IN_PATH, HTTP_POST, [this](PsychicRequest *request, JsonVariant &json)\n                {\n        if (json.is<JsonObject>()) {\n            String username = json[\"username\"];\n            String password = json[\"password\"];\n            Authentication authentication = _securityManager->authenticate(username, password);\n            if (authentication.authenticated) {\n                PsychicJsonResponse response = PsychicJsonResponse(request, false);\n                JsonObject root = response.getRoot();\n                root[\"access_token\"] = _securityManager->generateJWT(authentication.user);\n                return response.send();\n            }\n        }\n        return request->reply(401); });\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", SIGN_IN_PATH);\n\n    // Verifies that the request supplied a valid JWT\n    _server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, [this](PsychicRequest *request)\n                {\n        Authentication authentication = _securityManager->authenticateRequest(request);\n        return request->reply(authentication.authenticated ? 200 : 401); });\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", VERIFY_AUTHORIZATION_PATH);\n}\n\n#endif // end FT_ENABLED(FT_SECURITY)\n"
  },
  {
    "path": "lib/framework/AuthenticationService.h",
    "content": "#ifndef AuthenticationService_H_\n#define AuthenticationService_H_\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Features.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n\n#define VERIFY_AUTHORIZATION_PATH \"/rest/verifyAuthorization\"\n#define SIGN_IN_PATH \"/rest/signIn\"\n\n#if FT_ENABLED(FT_SECURITY)\n\nclass AuthenticationService\n{\npublic:\n    AuthenticationService(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\nprivate:\n    SecurityManager *_securityManager;\n    PsychicHttpServer *_server;\n};\n\n#endif // end FT_ENABLED(FT_SECURITY)\n#endif // end SecurityManager_h\n"
  },
  {
    "path": "lib/framework/BatteryService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <BatteryService.h>\n\nBatteryService::BatteryService(EventSocket *socket) : _socket(socket)\n{\n}\n\nvoid BatteryService::updateSOC(float stateOfCharge)\n{\n    _lastSOC = (int)round(stateOfCharge);\n    batteryEvent();\n}\n\nvoid BatteryService::setCharging(boolean isCharging)\n{\n    _isCharging = isCharging;\n    batteryEvent();\n}\n\nboolean BatteryService::isCharging()\n{\n    return _isCharging;\n}\n\nint BatteryService::getSOC()\n{\n    return _lastSOC;\n}\n\nvoid BatteryService::begin()\n{\n    _socket->registerEvent(EVENT_BATTERY);\n}\n\nvoid BatteryService::batteryEvent()\n{\n    JsonDocument doc;\n    doc[\"soc\"] = _lastSOC;\n    doc[\"charging\"] = _isCharging;\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_BATTERY, jsonObject);\n}\n"
  },
  {
    "path": "lib/framework/BatteryService.h",
    "content": "#pragma once\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <EventSocket.h>\n#include <JsonUtils.h>\n\n#define EVENT_BATTERY \"battery\"\n\nclass BatteryService\n{\npublic:\n    BatteryService(EventSocket *socket);\n\n    void begin();\n\n    void updateSOC(float stateOfCharge);\n\n    void setCharging(boolean isCharging);\n\n    boolean isCharging();\n\n    int getSOC();\n\nprivate:\n    EventSocket *_socket;\n    int _lastSOC = 100;\n    boolean _isCharging = false;\n\n    void batteryEvent();\n};\n"
  },
  {
    "path": "lib/framework/CoreDump.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <CoreDump.h>\n#include <esp32-hal.h>\n\n#include \"esp_core_dump.h\"\n#include \"esp_partition.h\"\n#include \"esp_flash.h\"\n\n#define MIN(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; })\n\nCoreDump::CoreDump(PsychicHttpServer *server,\n                   SecurityManager *securityManager) : _server(server),\n                                                       _securityManager(securityManager)\n{\n}\n\nvoid CoreDump::begin()\n{\n    _server->on(CORE_DUMP_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&CoreDump::coreDump, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(\"CoreDump\", \"Registered GET endpoint: %s\", CORE_DUMP_SERVICE_PATH);\n}\n\nesp_err_t CoreDump::coreDump(PsychicRequest *request)\n{\n    size_t coredump_addr;\n    size_t coredump_size;\n    esp_err_t err = esp_core_dump_image_get(&coredump_addr, &coredump_size);\n    if (err != ESP_OK)\n    {\n        request->reply(500, \"application/json\", \"{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"core dump not available\\\"}\");\n        return err;\n    }\n    size_t const chunk_len = 3 * 16; // must be multiple of 3\n    size_t const b64_len = chunk_len / 3 * 4 + 4;\n    uint8_t *const chunk = (uint8_t *)malloc(chunk_len);\n    char *const b64 = (char *)malloc(b64_len);\n    assert(chunk && b64);\n\n    /*if (write_cfg->start) {\n        if ((err = write_cfg->start(write_cfg->priv)) != ESP_OK) {\n            return err;\n        }\n    }*/\n\n    ESP_LOGI(SVK_TAG, \"Coredump is %u bytes\", coredump_size);\n    httpd_resp_set_status(request->request(), \"200 OK\");\n    PsychicResponse response(request);\n    response.setCode(200);\n    response.setContentType(\"application/octet-stream\");\n    response.sendHeaders();\n    for (size_t offset = 0; offset < coredump_size; offset += chunk_len)\n    {\n        uint const read_len = MIN(chunk_len, coredump_size - offset);\n        if (esp_flash_read(esp_flash_default_chip, chunk, coredump_addr + offset, read_len))\n        {\n            ESP_LOGE(SVK_TAG, \"Coredump read failed\");\n            break;\n        }\n        err = response.sendChunk(chunk, read_len);\n        if (err != ESP_OK)\n        {\n            break;\n        }\n    }\n    free(chunk);\n    free(b64);\n\n    err = response.finishChunking();\n\n    /*uint32_t sec_num = coredump_size / SPI_FLASH_SEC_SIZE;\n    if (coredump_size % SPI_FLASH_SEC_SIZE) {\n        sec_num++;\n    }\n    err = esp_flash_erase_region(esp_flash_default_chip, coredump_addr, sec_num * SPI_FLASH_SEC_SIZE);\n    if (err != ESP_OK) {\n        ESP_LOGE(SVK_TAG, \"Failed to erase coredump (%d)!\", err);\n    }*/\n    return err;\n}"
  },
  {
    "path": "lib/framework/CoreDump.h",
    "content": "#ifndef CoreDump_h\n#define CoreDump_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <ArduinoJson.h>\n#include <ESPFS.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <WiFi.h>\n\n#define CORE_DUMP_SERVICE_PATH \"/rest/coreDump\"\n\nclass CoreDump\n{\npublic:\n    CoreDump(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    esp_err_t coreDump(PsychicRequest *request);\n};\n\n#endif // end CoreDump_h"
  },
  {
    "path": "lib/framework/DownloadFirmwareService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <DownloadFirmwareService.h>\n\nextern const uint8_t rootca_crt_bundle_start[] asm(\"_binary_src_certs_x509_crt_bundle_bin_start\");\nextern const uint8_t rootca_crt_bundle_end[] asm(\"_binary_src_certs_x509_crt_bundle_bin_end\");\n\n/**\n * This is github-io.pem\n */\nconst char *githubCACertificate = \"-----BEGIN CERTIFICATE-----\\n\"\n                                  \"MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB\\n\"\n                                  \"iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\\n\"\n                                  \"cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\\n\"\n                                  \"BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx\\n\"\n                                  \"MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV\\n\"\n                                  \"BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE\\n\"\n                                  \"ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g\\n\"\n                                  \"VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC\\n\"\n                                  \"AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N\\n\"\n                                  \"TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj\\n\"\n                                  \"eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E\\n\"\n                                  \"oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk\\n\"\n                                  \"Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY\\n\"\n                                  \"uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j\\n\"\n                                  \"BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb\\n\"\n                                  \"+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G\\n\"\n                                  \"A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw\\n\"\n                                  \"CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0\\n\"\n                                  \"LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr\\n\"\n                                  \"BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv\\n\"\n                                  \"bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov\\n\"\n                                  \"L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H\\n\"\n                                  \"ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH\\n\"\n                                  \"7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi\\n\"\n                                  \"H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx\\n\"\n                                  \"RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv\\n\"\n                                  \"xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38\\n\"\n                                  \"sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL\\n\"\n                                  \"l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq\\n\"\n                                  \"6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY\\n\"\n                                  \"LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5\\n\"\n                                  \"yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K\\n\"\n                                  \"00u/I5sUKUErmgQfky3xxzlIPK1aEn8=\\n\"\n                                  \"-----END CERTIFICATE-----\\n\";\n\nstatic EventSocket *_socket = nullptr;\nstatic int previousProgress = 0;\nstatic String *otaURL = nullptr;\nJsonDocument doc;\n\nvoid update_started()\n{\n    String output;\n    doc[\"status\"] = \"preparing\";\n    doc[\"progress\"] = 0;\n    doc[\"bytes_written\"] = 0;\n    doc[\"total_bytes\"] = 0;\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n    ESP_LOGI(SVK_TAG, \"HTTP Update started\");\n#ifdef SERIAL_INFO\n    Serial.println(\"HTTP Update started\");\n#endif\n}\n\nvoid update_progress(int currentBytes, int totalBytes)\n{\n    doc[\"status\"] = \"progress\";\n    int progress = ((currentBytes * 100) / totalBytes);\n    if (progress > previousProgress)\n    {\n        doc[\"progress\"] = progress;\n        doc[\"bytes_written\"] = currentBytes;\n        doc[\"total_bytes\"] = totalBytes;\n        JsonObject jsonObject = doc.as<JsonObject>();\n        _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n        ESP_LOGV(SVK_TAG, \"HTTP update process at %d of %d bytes... (%d %%)\", currentBytes, totalBytes, progress);\n    }\n    previousProgress = progress;\n}\n\nvoid update_finished()\n{\n    String output;\n    doc[\"status\"] = \"finished\";\n    doc[\"progress\"] = 100;\n    // Keep the last known bytes_written and total_bytes from progress\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n    ESP_LOGI(SVK_TAG, \"HTTP Update successful - Restarting\");\n#ifdef SERIAL_INFO\n    Serial.println(\"HTTP Update successful - Restarting\");\n#endif\n\n    vTaskDelay(250 / portTICK_PERIOD_MS);\n}\n\nvoid updateTask(void *param)\n{\n    String url = *((String *)param);\n    delete (String *)param; // Clean up the allocated memory\n\n    WiFiClientSecure client;\n\n#ifndef DOWNLOAD_OTA_SKIP_CERT_VERIFY\n\n#if ESP_ARDUINO_VERSION_MAJOR == 3\n    client.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);\n#else\n    client.setCACertBundle(rootca_crt_bundle_start);\n#endif\n\n#else\n    ESP_LOGW(SVK_TAG, \"Skipping SSL certificate verification for OTA update!\");\n    client.setInsecure();\n#endif\n\n    client.setTimeout(12000);\n\n    httpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);\n    httpUpdate.rebootOnUpdate(true);\n\n    String output;\n    httpUpdate.onStart(update_started);\n    httpUpdate.onProgress(update_progress);\n    httpUpdate.onEnd(update_finished);\n\n    t_httpUpdate_return ret = httpUpdate.update(client, url.c_str());\n    JsonObject jsonObject;\n\n    // Reduce task priority to allow other tasks to run\n    vTaskPrioritySet(NULL, tskIDLE_PRIORITY + 1);\n\n    bool _emitEvent = false;\n\n    switch (ret)\n    {\n    case HTTP_UPDATE_FAILED:\n\n        doc[\"status\"] = \"error\";\n        doc[\"error\"] = httpUpdate.getLastErrorString().c_str();\n        _emitEvent = true;\n\n        ESP_LOGE(SVK_TAG, \"HTTP Update failed with error (%d): %s\", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());\n#ifdef SERIAL_INFO\n        Serial.printf(\"HTTP Update failed with error (%d): %s\\n\", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());\n#endif\n        break;\n    case HTTP_UPDATE_NO_UPDATES:\n\n        doc[\"status\"] = \"error\";\n        doc[\"error\"] = \"Update failed, has same firmware version\";\n        _emitEvent = true;\n\n        ESP_LOGE(SVK_TAG, \"HTTP Update failed, has same firmware version\");\n#ifdef SERIAL_INFO\n        Serial.println(\"HTTP Update failed, has same firmware version\");\n#endif\n        break;\n    }\n\n    if (_emitEvent)\n    {\n        jsonObject = doc.as<JsonObject>();\n        _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n    }\n\n    // delay to allow the event to be sent out\n    vTaskDelay(100 / portTICK_PERIOD_MS);\n\n    vTaskDelete(NULL);\n}\n\nDownloadFirmwareService::DownloadFirmwareService(PsychicHttpServer *server,\n                                                 SecurityManager *securityManager,\n                                                 EventSocket *socket) : _server(server),\n                                                                        _securityManager(securityManager),\n                                                                        _socket(socket)\n{\n}\n\nvoid DownloadFirmwareService::begin()\n{\n    ::_socket = _socket;\n\n    if (!_socket->isEventValid(EVENT_OTA_UPDATE))\n    {\n        _socket->registerEvent(EVENT_OTA_UPDATE);\n    }\n\n    _server->on(GITHUB_FIRMWARE_PATH,\n                HTTP_POST,\n                _securityManager->wrapCallback(\n                    std::bind(&DownloadFirmwareService::downloadUpdate, this, std::placeholders::_1, std::placeholders::_2),\n                    AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", GITHUB_FIRMWARE_PATH);\n}\n\nesp_err_t DownloadFirmwareService::downloadUpdate(PsychicRequest *request, JsonVariant &json)\n{\n    if (!json.is<JsonObject>())\n    {\n        return request->reply(400);\n    }\n\n    String downloadURL = json[\"download_url\"];\n    ESP_LOGI(SVK_TAG, \"Starting OTA from: %s\", downloadURL.c_str());\n#ifdef SERIAL_INFO\n    Serial.println(\"Starting OTA from: \" + downloadURL);\n#endif\n\n    doc[\"status\"] = \"preparing\";\n    doc[\"progress\"] = 0;\n    doc[\"bytes_written\"] = 0;\n    doc[\"total_bytes\"] = 0;\n    doc[\"error\"] = \"\";\n\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n\n    // Allocate memory for the URL on the heap\n    String *urlPtr = new String(downloadURL);\n\n    if (xTaskCreatePinnedToCore(\n            &updateTask,                // Function that should be called\n            \"Update\",                   // Name of the task (for debugging)\n            OTA_TASK_STACK_SIZE,        // Stack size (bytes)\n            urlPtr,                     // Pass reference to this class instance\n            (configMAX_PRIORITIES - 1), // Pretty high task priority\n            NULL,                       // Task handle\n            1                           // Have it on application core\n            ) != pdPASS)\n    {\n        delete urlPtr; // Clean up if task creation fails\n        ESP_LOGE(SVK_TAG, \"Couldn't create download OTA task\");\n#ifdef SERIAL_INFO\n        Serial.println(\"Couldn't create download OTA task\");\n#endif\n        return request->reply(500);\n    }\n    return request->reply(200);\n}\n"
  },
  {
    "path": "lib/framework/DownloadFirmwareService.h",
    "content": "#pragma once\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n\n#include <WiFi.h>\n#include <ArduinoJson.h>\n#include <EventSocket.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <FirmwareUpdateEvents.h>\n\n#include <WiFiClientSecure.h>\n#include <HTTPUpdate.h>\n\n#define GITHUB_FIRMWARE_PATH \"/rest/downloadUpdate\"\n#define OTA_TASK_STACK_SIZE 9216\n\nclass DownloadFirmwareService\n{\npublic:\n    DownloadFirmwareService(PsychicHttpServer *server, SecurityManager *securityManager, EventSocket *socket);\n\n    void begin();\n\nprivate:\n    SecurityManager *_securityManager;\n    PsychicHttpServer *_server;\n    EventSocket *_socket;\n    esp_err_t downloadUpdate(PsychicRequest *request, JsonVariant &json);\n};\n"
  },
  {
    "path": "lib/framework/ESP32SvelteKit.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <ESP32SvelteKit.h>\n\nESP32SvelteKit::ESP32SvelteKit(PsychicHttpServer *server, unsigned int numberEndpoints) : _server(server),\n                                                                                          _numberEndpoints(numberEndpoints),\n                                                                                          _featureService(server, &_socket),\n                                                                                          _securitySettingsService(server, &ESPFS),\n                                                                                          _wifiSettingsService(server, &ESPFS, &_securitySettingsService, &_socket),\n                                                                                          _wifiScanner(server, &_securitySettingsService),\n                                                                                          _wifiStatus(server, &_securitySettingsService),\n                                                                                          _apSettingsService(server, &ESPFS, &_securitySettingsService),\n                                                                                          _apStatus(server, &_securitySettingsService, &_apSettingsService),\n#if FT_ENABLED(FT_ETHERNET)\n                                                                                          _ethernetSettingsService(server, &ESPFS, &_securitySettingsService, &_socket),\n                                                                                          _ethernetStatus(server, &_securitySettingsService),\n#endif\n                                                                                          _socket(server, &_securitySettingsService, AuthenticationPredicates::IS_AUTHENTICATED),\n                                                                                          _notificationService(&_socket),\n#if FT_ENABLED(FT_NTP)\n                                                                                          _ntpSettingsService(server, &ESPFS, &_securitySettingsService),\n                                                                                          _ntpStatus(server, &_securitySettingsService),\n#endif\n#if FT_ENABLED(FT_UPLOAD_FIRMWARE)\n                                                                                          _uploadFirmwareService(server, &_securitySettingsService, &_socket),\n#endif\n#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)\n                                                                                          _downloadFirmwareService(server, &_securitySettingsService, &_socket),\n#endif\n#if FT_ENABLED(FT_MQTT)\n                                                                                          _mqttSettingsService(server, &ESPFS, &_securitySettingsService),\n                                                                                          _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService),\n#endif\n#if FT_ENABLED(FT_SECURITY)\n                                                                                          _authenticationService(server, &_securitySettingsService),\n#endif\n#if FT_ENABLED(FT_SLEEP)\n                                                                                          _sleepService(server, &_securitySettingsService),\n#endif\n#if FT_ENABLED(FT_BATTERY)\n                                                                                          _batteryService(&_socket),\n#endif\n#if FT_ENABLED(FT_ANALYTICS)\n                                                                                          _analyticsService(&_socket),\n#endif\n                                                                                          _restartService(server, &_securitySettingsService),\n                                                                                          _factoryResetService(server, &ESPFS, &_securitySettingsService),\n#if FT_ENABLED(FT_COREDUMP)\n                                                                                          _coreDump(server, &_securitySettingsService),\n#endif\n                                                                                          _systemStatus(server, &_securitySettingsService)\n{\n}\n\nvoid ESP32SvelteKit::begin()\n{\n    ESP_LOGV(SVK_TAG, \"Loading settings from files system\");\n    ESPFS.begin(true);\n\n#if FT_ENABLED(FT_ETHERNET)\n    _ethernetSettingsService.initEthernet();\n#endif\n\n    _wifiSettingsService.initWiFi();\n\n    // SvelteKit uses a lot of handlers, so we need to increase the max_uri_handlers\n    // WWWData has 77 Endpoints, Framework has 27, and Lighstate Demo has 4\n    _server->config.max_uri_handlers = _numberEndpoints;\n    _server->listen(80);\n\n#ifdef EMBED_WWW\n    // Serve static resources from PROGMEM\n    ESP_LOGV(SVK_TAG, \"Registering routes from PROGMEM static resources\");\n    WWWData::registerRoutes(\n        [&](const String &uri, const String &contentType, const uint8_t *content, size_t len)\n        {\n            PsychicHttpRequestCallback requestHandler = [contentType, content, len](PsychicRequest *request)\n            {\n                PsychicResponse response(request);\n                response.setCode(200);\n                response.setContentType(contentType.c_str());\n                response.addHeader(\"Content-Encoding\", \"gzip\");\n                response.addHeader(\"Cache-Control\", \"public, immutable, max-age=31536000\");\n                response.setContent(content, len);\n                return response.send();\n            };\n            PsychicWebHandler *handler = new PsychicWebHandler();\n            handler->onRequest(requestHandler);\n            _server->on(uri.c_str(), HTTP_GET, handler);\n\n            // Set default end-point for all non matching requests\n            // this is easier than using webServer.onNotFound()\n            if (uri.equals(\"/index.html\"))\n            {\n                _server->defaultEndpoint->setHandler(handler);\n            }\n        });\n#else\n    // Serve static resources from /www/\n    ESP_LOGV(SVK_TAG, \"Registering routes from FS /www/ static resources\");\n    _server->serveStatic(\"/_app/\", ESPFS, \"/www/_app/\");\n    _server->serveStatic(\"/favicon.png\", ESPFS, \"/www/favicon.png\");\n    //  Serving all other get requests with \"/www/index.htm\"\n    _server->onNotFound([](PsychicRequest *request)\n                        {\n        if (request->method() == HTTP_GET) {\n            PsychicFileResponse response(request, ESPFS, \"/www/index.html\", \"text/html\");\n            return response.send();\n            // String url = \"http://\" + request->host() + \"/index.html\";\n            // request->redirect(url.c_str());\n        } });\n#endif\n\n    // Serve static resources from /config/ if set by platformio.ini\n#if SERVE_CONFIG_FILES\n    _server->serveStatic(\"/config/\", ESPFS, \"/config/\");\n#endif\n\n#if defined(ENABLE_CORS)\n    ESP_LOGV(SVK_TAG, \"Enabling CORS headers\");\n    DefaultHeaders::Instance().addHeader(\"Access-Control-Allow-Origin\", CORS_ORIGIN);\n    DefaultHeaders::Instance().addHeader(\"Access-Control-Allow-Headers\", \"Accept, Content-Type, Authorization\");\n    DefaultHeaders::Instance().addHeader(\"Access-Control-Allow-Credentials\", \"true\");\n#endif\n\n    ESP_LOGV(SVK_TAG, \"Starting MDNS\");\n    MDNS.begin(_wifiSettingsService.getHostname().c_str());\n    MDNS.setInstanceName(_appName);\n    MDNS.addService(\"http\", \"tcp\", 80);\n    MDNS.addService(\"ws\", \"tcp\", 80);\n    MDNS.addServiceTxt(\"http\", \"tcp\", \"Firmware Version\", APP_VERSION);\n\n#ifdef SERIAL_INFO\n    Serial.printf(\"Running Firmware Version: %s\\n\", APP_VERSION);\n#endif\n\n    // Start the services\n    _apStatus.begin();\n    _socket.begin();\n    _notificationService.begin();\n    _apSettingsService.begin();\n    _factoryResetService.begin();\n    _featureService.begin();\n    _restartService.begin();\n    _systemStatus.begin();\n    _wifiSettingsService.begin();\n    _wifiScanner.begin();\n    _wifiStatus.begin();\n#if FT_ENABLED(FT_ETHERNET)\n    _ethernetSettingsService.begin();\n    _ethernetStatus.begin();\n#endif\n\n\n#if FT_ENABLED(FT_COREDUMP)\n    _coreDump.begin();\n#endif\n\n#if FT_ENABLED(FT_UPLOAD_FIRMWARE)\n    _uploadFirmwareService.begin();\n#endif\n\n#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)\n    _downloadFirmwareService.begin();\n#endif\n\n#if FT_ENABLED(FT_NTP)\n    _ntpSettingsService.begin();\n    _ntpStatus.begin();\n#endif\n\n#if FT_ENABLED(FT_MQTT)\n    _mqttSettingsService.begin();\n    _mqttStatus.begin();\n#endif\n\n#if FT_ENABLED(FT_SECURITY)\n    _authenticationService.begin();\n    _securitySettingsService.begin();\n#endif\n\n#if FT_ENABLED(FT_SLEEP)\n    _sleepService.begin();\n    _sleepService.attachOnSleepCallback([&]()\n                                        {   ESP_LOGI(SVK_TAG, \"Attempting to stop server\");\n                                            for (auto client : _server->getClientList())\n                                            {\n                                                client->close();\n                                            }\n                                            vTaskDelete(_loopTaskHandle);\n                                            ESP_LOGI(SVK_TAG, \"Server stopped\"); });\n#if FT_ENABLED(FT_MQTT)\n    _sleepService.attachOnSleepCallback([&]()\n                                        { _mqttSettingsService.disconnect(); });\n#endif\n#endif\n\n#if FT_ENABLED(FT_BATTERY)\n    _batteryService.begin();\n#endif\n\n#if FT_ENABLED(FT_ANALYTICS)\n    _analyticsService.begin();\n#endif\n\n    // Start the loop task\n    ESP_LOGV(SVK_TAG, \"Starting loop task\");\n    xTaskCreatePinnedToCore(\n        this->_loopImpl,            // Function that should be called\n        \"ESP32 SvelteKit Loop\",     // Name of the task (for debugging)\n        4096,                       // Stack size (bytes)\n        this,                       // Pass reference to this class instance\n        (tskIDLE_PRIORITY + 2),     // task priority\n        &_loopTaskHandle,           // Task handle\n        ESP32SVELTEKIT_RUNNING_CORE // Pin to application core\n    );\n}\n\nvoid ESP32SvelteKit::_loop()\n{\n    TickType_t xLastWakeTime = xTaskGetTickCount();\n\n    bool wifi = false;\n    bool ap = false;\n    bool event = false;\n    bool mqtt = false;\n#if FT_ENABLED(FT_ETHERNET)\n    bool eth = false;\n#endif\n    bool wifi_eth_combined = false;\n\n    while (1)\n    {\n        _wifiSettingsService.loop(); // 30 seconds\n        _apSettingsService.loop();   // 10 seconds\n#if FT_ENABLED(FT_MQTT)\n        _mqttSettingsService.loop(); // 5 seconds\n#endif\n#if FT_ENABLED(FT_ANALYTICS)\n        _analyticsService.loop();\n#endif\n#if FT_ENABLED(FT_ETHERNET)\n        _ethernetSettingsService.loop();\n        eth = _ethernetStatus.isConnected();\n        if (eth) { wifi_eth_combined = true; }\n#endif\n\n        // Query the connectivity status\n        wifi = _wifiStatus.isConnected();\n        if (wifi) { wifi_eth_combined = true; }\n        ap = _apStatus.isActive();\n        event = _socket.getConnectedClients() > 0;\n#if FT_ENABLED(FT_MQTT)\n        mqtt = _mqttStatus.isConnected();\n#endif\n\n        // Update the system status\n        if (wifi_eth_combined && mqtt)\n        {\n            _connectionStatus = ConnectionStatus::STA_MQTT;\n        }\n        else if (wifi_eth_combined)\n        {\n            _connectionStatus = event ? ConnectionStatus::STA_CONNECTED : ConnectionStatus::STA;\n        }\n        else if (ap)\n        {\n            _connectionStatus = event ? ConnectionStatus::AP_CONNECTED : ConnectionStatus::AP;\n        }\n        else\n        {\n            _connectionStatus = ConnectionStatus::OFFLINE;\n        }\n\n        // iterate over all loop functions\n        for (auto &function : _loopFunctions)\n        {\n            function();\n        }\n\n#ifdef TELEPLOT_TASKS\n        static int lastTime = 0;\n        if (millis() - lastTime > 1000)\n        {\n            lastTime = millis();\n            Serial.printf(\">ESP32SveltekitTask:%i:%i\\n\", millis(), uxTaskGetStackHighWaterMark(NULL));\n        }\n#endif\n        vTaskDelayUntil(&xLastWakeTime, ESP32SVELTEKIT_LOOP_INTERVAL / portTICK_PERIOD_MS);\n    }\n}\n"
  },
  {
    "path": "lib/framework/ESP32SvelteKit.h",
    "content": "#ifndef ESP32SvelteKit_h\n#define ESP32SvelteKit_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n\n#include <WiFi.h>\n#include <ESPmDNS.h>\n#include <AnalyticsService.h>\n#include <FeaturesService.h>\n#include <APSettingsService.h>\n#include <APStatus.h>\n#include <AuthenticationService.h>\n#include <BatteryService.h>\n#include <FactoryResetService.h>\n#include <DownloadFirmwareService.h>\n#include <EventSocket.h>\n#include <MqttSettingsService.h>\n#include <MqttStatus.h>\n#include <NotificationService.h>\n#include <NTPSettingsService.h>\n#include <NTPStatus.h>\n#include <UploadFirmwareService.h>\n#include <RestartService.h>\n#include <SecuritySettingsService.h>\n#include <SleepService.h>\n#include <SystemStatus.h>\n#include <CoreDump.h>\n#include <WiFiScanner.h>\n#include <WiFiSettingsService.h>\n#include <WiFiStatus.h>\n#include <EthernetSettingsService.h>\n#include <EthernetStatus.h>\n#include <ESPFS.h>\n#include <PsychicHttp.h>\n#include <vector>\n\n#ifdef EMBED_WWW\n#include <WWWData.h>\n#endif\n\n#ifndef CORS_ORIGIN\n#define CORS_ORIGIN \"*\"\n#endif\n\n#ifndef APP_VERSION\n#define APP_VERSION \"demo\"\n#endif\n\n#ifndef APP_NAME\n#define APP_NAME \"ESP32 SvelteKit Demo\"\n#endif\n\n#ifndef ESP32SVELTEKIT_RUNNING_CORE\n#define ESP32SVELTEKIT_RUNNING_CORE -1\n#endif\n\n#ifndef ESP32SVELTEKIT_LOOP_INTERVAL\n#define ESP32SVELTEKIT_LOOP_INTERVAL 10\n#endif\n\n// define callback function to include into the main loop\ntypedef std::function<void()> loopCallback;\n\n// enum for connection status\nenum class ConnectionStatus\n{\n    OFFLINE,\n    AP,\n    AP_CONNECTED,\n    STA,\n    STA_CONNECTED,\n    STA_MQTT\n};\n\nclass ESP32SvelteKit\n{\npublic:\n    ESP32SvelteKit(PsychicHttpServer *server, unsigned int numberEndpoints = 115);\n\n    void begin();\n\n    ConnectionStatus getConnectionStatus()\n    {\n        return _connectionStatus;\n    }\n\n    FS *getFS()\n    {\n        return &ESPFS;\n    }\n\n    PsychicHttpServer *getServer()\n    {\n        return _server;\n    }\n\n    SecurityManager *getSecurityManager()\n    {\n        return &_securitySettingsService;\n    }\n\n    EventSocket *getSocket()\n    {\n        return &_socket;\n    }\n\n#if FT_ENABLED(FT_SECURITY)\n    SecuritySettingsService *getSecuritySettingsService()\n    {\n        return &_securitySettingsService;\n    }\n#endif\n\n    WiFiSettingsService *getWiFiSettingsService()\n    {\n        return &_wifiSettingsService;\n    }\n\n    APSettingsService *getAPSettingsService()\n    {\n        return &_apSettingsService;\n    }\n\n    NotificationService *getNotificationService()\n    {\n        return &_notificationService;\n    }\n\n#if FT_ENABLED(FT_NTP)\n    NTPSettingsService *getNTPSettingsService()\n    {\n        return &_ntpSettingsService;\n    }\n#endif\n\n#if FT_ENABLED(FT_MQTT)\n    MqttSettingsService *getMqttSettingsService()\n    {\n        return &_mqttSettingsService;\n    }\n\n    PsychicMqttClient *getMqttClient()\n    {\n        return _mqttSettingsService.getMqttClient();\n    }\n#endif\n\n#if FT_ENABLED(FT_SLEEP)\n    SleepService *getSleepService()\n    {\n        return &_sleepService;\n    }\n#endif\n\n#if FT_ENABLED(FT_BATTERY)\n    BatteryService *getBatteryService()\n    {\n        return &_batteryService;\n    }\n#endif\n\n    FeaturesService *getFeatureService()\n    {\n        return &_featureService;\n    }\n\n    RestartService *getRestartService()\n    {\n        return &_restartService;\n    }\n\n    void factoryReset()\n    {\n        _factoryResetService.factoryReset();\n    }\n\n    void setMDNSAppName(String name)\n    {\n        _appName = name;\n    }\n\n    void recoveryMode()\n    {\n        _apSettingsService.recoveryMode();\n    }\n\n    void addLoopFunction(loopCallback function)\n    {\n        _loopFunctions.push_back(function);\n    }\n\nprivate:\n    PsychicHttpServer *_server;\n    TaskHandle_t _loopTaskHandle;\n    unsigned int _numberEndpoints;\n    FeaturesService _featureService;\n    SecuritySettingsService _securitySettingsService;\n    WiFiSettingsService _wifiSettingsService;\n    WiFiScanner _wifiScanner;\n    WiFiStatus _wifiStatus;\n    APSettingsService _apSettingsService;\n    APStatus _apStatus;\n#if FT_ENABLED(FT_ETHERNET)\n    EthernetSettingsService _ethernetSettingsService;\n    EthernetStatus _ethernetStatus;\n#endif\n    EventSocket _socket;\n    NotificationService _notificationService;\n#if FT_ENABLED(FT_NTP)\n    NTPSettingsService _ntpSettingsService;\n    NTPStatus _ntpStatus;\n#endif\n#if FT_ENABLED(FT_UPLOAD_FIRMWARE)\n    UploadFirmwareService _uploadFirmwareService;\n#endif\n#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)\n    DownloadFirmwareService _downloadFirmwareService;\n#endif\n#if FT_ENABLED(FT_MQTT)\n    MqttSettingsService _mqttSettingsService;\n    MqttStatus _mqttStatus;\n#endif\n#if FT_ENABLED(FT_SECURITY)\n    AuthenticationService _authenticationService;\n#endif\n#if FT_ENABLED(FT_SLEEP)\n    SleepService _sleepService;\n#endif\n#if FT_ENABLED(FT_BATTERY)\n    BatteryService _batteryService;\n#endif\n#if FT_ENABLED(FT_ANALYTICS)\n    AnalyticsService _analyticsService;\n#endif\n#if FT_ENABLED(FT_COREDUMP)\n    CoreDump _coreDump;\n#endif\n    RestartService _restartService;\n    FactoryResetService _factoryResetService;\n    SystemStatus _systemStatus;\n\n    String _appName = APP_NAME;\n\nprotected:\n    static void _loopImpl(void *_this) { static_cast<ESP32SvelteKit *>(_this)->_loop(); }\n    void _loop();\n\n    std::vector<loopCallback> _loopFunctions;\n\n    // Connectivity status\n    ConnectionStatus _connectionStatus = ConnectionStatus::OFFLINE;\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/ESPFS.h",
    "content": "#ifndef ESPFS_H_\n#define ESPFS_H_\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <LittleFS.h>\n#define ESPFS LittleFS\n\n#endif"
  },
  {
    "path": "lib/framework/EthernetSettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <EthernetSettingsService.h>\n\n#if FT_ENABLED(FT_ETHERNET)\n\nEthernetSettingsService::EthernetSettingsService(PsychicHttpServer *server,\n                                                 FS *fs,\n                                                 SecurityManager *securityManager,\n                                                 EventSocket *socket) : _server(server),\n                                                                        _securityManager(securityManager),\n                                                                        _httpEndpoint(EthernetSettings::read, EthernetSettings::update, this, server, ETHERNET_SETTINGS_SERVICE_PATH, securityManager,\n                                                                                      AuthenticationPredicates::IS_ADMIN),\n                                                                        _fsPersistence(EthernetSettings::read, EthernetSettings::update, this, fs, ETHERNET_SETTINGS_FILE),\n                                                                        _socket(socket)\n{\n    addUpdateHandler([&](const String &originId)\n                     { reconfigureEthernet(); },\n                     false);\n}\n\nvoid EthernetSettingsService::initEthernet()\n{\n    // make sure the interface is stopped before continuing and initializing\n    ETH.end();\n    _fsPersistence.readFromFS();\n    configureNetwork(_state.ethernetSettings);\n}\n\nvoid EthernetSettingsService::begin()\n{\n    _socket->registerEvent(EVENT_ETHERNET);\n    _httpEndpoint.begin();\n}\n\nvoid EthernetSettingsService::loop()\n{\n    unsigned long currentMillis = millis();\n\n    if (!_lastEthernetUpdate || (unsigned long)(currentMillis - _lastEthernetUpdate) >= ETHERNET_EVENT_DELAY)\n    {\n        _lastEthernetUpdate = currentMillis;\n        updateEthernet();\n    }\n}\n\nString EthernetSettingsService::getHostname()\n{\n    return _state.hostname;\n}\n\nString EthernetSettingsService::getIP()\n{\n    if (ETH.connected())\n    {\n        return ETH.localIP().toString();\n    }\n    return \"Not connected\";\n}\n\nvoid EthernetSettingsService::configureNetwork(ethernet_settings_t &network)\n{\n    // set hostname before IP configuration starts\n    ETH.setHostname(_state.hostname.c_str());\n    if (network.staticIPConfig)\n    {\n        // configure for static IP\n        ETH.config(network.localIP, network.gatewayIP, network.subnetMask, network.dnsIP1, network.dnsIP2);\n    }\n    else\n    {\n        // configure for DHCP\n        ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);\n    }\n// (re)start ethernet\n#if CONFIG_IDF_TARGET_ESP32\n    // ESP32 chips with built-in ethernet MAC/PHY\n    ETH.begin();\n#else\n    // For SPI based ethernet modules like W5500, ENC28J60 etc.\n    SPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI);\n    ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, SPI);\n#endif\n    // set hostname (again) after (re)starting ethernet due to a bug in the ESP-IDF implementation\n    ETH.setHostname(_state.hostname.c_str());\n}\n\nvoid EthernetSettingsService::reconfigureEthernet()\n{\n    configureNetwork(_state.ethernetSettings);\n}\n\nvoid EthernetSettingsService::updateEthernet()\n{\n    JsonDocument doc;\n    doc[\"connected\"] = ETH.connected();\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_ETHERNET, jsonObject);\n}\n\n#endif // end FT_ENABLED(FT_ETHERNET)\n"
  },
  {
    "path": "lib/framework/EthernetSettingsService.h",
    "content": "#ifndef EthernetSettingsService_h\n#define EthernetSettingsService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n#include <ETH.h>\n#include <SettingValue.h>\n#include <StatefulService.h>\n#include <EventSocket.h>\n#include <FSPersistence.h>\n#include <HttpEndpoint.h>\n#include <JsonUtils.h>\n#include <SecurityManager.h>\n#include <PsychicHttp.h>\n#include <vector>\n\n#ifndef FACTORY_ETHERNET_HOSTNAME\n#define FACTORY_ETHERNET_HOSTNAME \"#{platform}-#{unique_id}\"\n#endif\n\n#define ETHERNET_EVENT_DELAY 500\n\n#define ETHERNET_SETTINGS_FILE \"/config/ethernetSettings.json\"\n#define ETHERNET_SETTINGS_SERVICE_PATH \"/rest/ethernetSettings\"\n\n#define EVENT_ETHERNET \"ethernet\"\n\n#if FT_ENABLED(FT_ETHERNET)\n\n// Struct defining the ethernet settings\ntypedef struct\n{\n    bool staticIPConfig;\n    IPAddress localIP;\n    IPAddress gatewayIP;\n    IPAddress subnetMask;\n    IPAddress dnsIP1;\n    IPAddress dnsIP2;\n    bool available;\n} ethernet_settings_t;\n\nclass EthernetSettings\n{\npublic:\n    // core ethernet configuration\n    String hostname;\n    ethernet_settings_t ethernetSettings;\n\n    static void read(EthernetSettings &settings, JsonObject &root)\n    {\n        root[\"hostname\"] = settings.hostname;\n        root[\"static_ip_config\"] = settings.ethernetSettings.staticIPConfig;\n        JsonUtils::writeIP(root, \"local_ip\", settings.ethernetSettings.localIP);\n        JsonUtils::writeIP(root, \"gateway_ip\", settings.ethernetSettings.gatewayIP);\n        JsonUtils::writeIP(root, \"subnet_mask\", settings.ethernetSettings.subnetMask);\n        JsonUtils::writeIP(root, \"dns_ip_1\", settings.ethernetSettings.dnsIP1);\n        JsonUtils::writeIP(root, \"dns_ip_2\", settings.ethernetSettings.dnsIP2);\n        ESP_LOGV(SVK_TAG, \"Ethernet Settings read\");\n    }\n\n    static StateUpdateResult update(JsonObject &root, EthernetSettings &settings, const String &originId)\n    {\n        settings.hostname = root[\"hostname\"] | SettingValue::format(FACTORY_ETHERNET_HOSTNAME);\n        settings.ethernetSettings.staticIPConfig = root[\"static_ip_config\"] | false;\n        JsonUtils::readIP(root, \"local_ip\", settings.ethernetSettings.localIP);\n        JsonUtils::readIP(root, \"gateway_ip\", settings.ethernetSettings.gatewayIP);\n        JsonUtils::readIP(root, \"subnet_mask\", settings.ethernetSettings.subnetMask);\n        JsonUtils::readIP(root, \"dns_ip_1\", settings.ethernetSettings.dnsIP1);\n        JsonUtils::readIP(root, \"dns_ip_2\", settings.ethernetSettings.dnsIP2);\n\n        // Swap around the dns servers if 2 is populated but 1 is not\n        if (IPUtils::isNotSet(settings.ethernetSettings.dnsIP1) && IPUtils::isSet(settings.ethernetSettings.dnsIP2))\n        {\n            settings.ethernetSettings.dnsIP1 = settings.ethernetSettings.dnsIP2;\n            settings.ethernetSettings.dnsIP2 = INADDR_NONE;\n        }\n\n        // Turning off static ip config if we don't meet the minimum requirements\n        // of ipAddress and subnet. This may change to static ip only\n        // as sensible defaults can be assumed for gateway and subnet\n        if (settings.ethernetSettings.staticIPConfig && (IPUtils::isNotSet(settings.ethernetSettings.localIP) || IPUtils::isNotSet(settings.ethernetSettings.subnetMask)))\n        {\n            settings.ethernetSettings.staticIPConfig = false;\n        }\n        ESP_LOGV(SVK_TAG, \"Ethernet Settings updated\");\n\n        return StateUpdateResult::CHANGED;\n    };\n};\n\nclass EthernetSettingsService : public StatefulService<EthernetSettings>\n{\npublic:\n    EthernetSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket);\n\n    void initEthernet();\n    void begin();\n    void loop();\n    String getHostname();\n    String getIP();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    HttpEndpoint<EthernetSettings> _httpEndpoint;\n    FSPersistence<EthernetSettings> _fsPersistence;\n    EventSocket *_socket;\n    unsigned long _lastEthernetUpdate;\n\n    void configureNetwork(ethernet_settings_t &network);\n    void reconfigureEthernet();\n    void updateEthernet();\n};\n\n#endif // end FT_ENABLED(FT_ETHERNET)\n#endif // end EthernetSettingsService_h\n"
  },
  {
    "path": "lib/framework/EthernetStatus.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <EthernetStatus.h>\n\n#if FT_ENABLED(FT_ETHERNET)\n\nEthernetStatus::EthernetStatus(PsychicHttpServer *server,\n                               SecurityManager *securityManager) : _server(server),\n                                                                   _securityManager(securityManager)\n{\n}\n\nvoid EthernetStatus::begin()\n{\n    _server->on(ETHERNET_STATUS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&EthernetStatus::ethernetStatus, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", ETHERNET_STATUS_SERVICE_PATH);\n\n    // arduino also uses WiFi events for Ethernet\n    WiFi.onEvent(onConnected, WiFiEvent_t::ARDUINO_EVENT_ETH_CONNECTED);\n    WiFi.onEvent(onDisconnected, WiFiEvent_t::ARDUINO_EVENT_ETH_DISCONNECTED);\n    WiFi.onEvent(onGotIP, WiFiEvent_t::ARDUINO_EVENT_ETH_GOT_IP);\n}\n\nvoid EthernetStatus::onConnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    ESP_LOGI(SVK_TAG, \"Ethernet Connected.\");\n#ifdef SERIAL_INFO\n    Serial.println(\"Ethernet Connected.\");\n#endif\n}\n\nvoid EthernetStatus::onDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    ESP_LOGI(SVK_TAG, \"Ethernet Disconnected.\");\n#ifdef SERIAL_INFO\n    Serial.print(\"Ethernet Disconnected.\");\n#endif\n}\n\nvoid EthernetStatus::onGotIP(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    ESP_LOGI(SVK_TAG, \"Ethernet Got IP. localIP=%s, hostName=%s\", ETH.localIP().toString().c_str(), ETH.getHostname());\n#ifdef SERIAL_INFO\n    Serial.printf(\"Ethernet Got IP. localIP=%s, hostName=%s\\r\\n\", ETH.localIP().toString().c_str(), ETH.getHostname());\n#endif\n}\n\nesp_err_t EthernetStatus::ethernetStatus(PsychicRequest *request)\n{\n    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n    JsonObject root = response.getRoot();\n    bool isConnected = ETH.connected();\n    root[\"connected\"] = isConnected;\n    if (isConnected)\n    {\n        root[\"local_ip\"] = ETH.localIP().toString();\n        root[\"mac_address\"] = ETH.macAddress();\n        root[\"subnet_mask\"] = ETH.subnetMask().toString();\n        root[\"gateway_ip\"] = ETH.gatewayIP().toString();\n        IPAddress dnsIP1 = ETH.dnsIP(0);\n        IPAddress dnsIP2 = ETH.dnsIP(1);\n        if (IPUtils::isSet(dnsIP1))\n        {\n            root[\"dns_ip_1\"] = dnsIP1.toString();\n        }\n        if (IPUtils::isSet(dnsIP2))\n        {\n            root[\"dns_ip_2\"] = dnsIP2.toString();\n        }\n        root[\"link_speed\"] = ETH.linkSpeed();\n    }\n\n    return response.send();\n}\n\nbool EthernetStatus::isConnected()\n{\n    return ETH.connected();\n}\n\n#endif // end FT_ENABLED(FT_ETHERNET)\n"
  },
  {
    "path": "lib/framework/EthernetStatus.h",
    "content": "#ifndef EthernetStatus_h\n#define EthernetStatus_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n#include <ETH.h>\n\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <IPUtils.h>\n#include <SecurityManager.h>\n\n#define ETHERNET_STATUS_SERVICE_PATH \"/rest/ethernetStatus\"\n\n#if FT_ENABLED(FT_ETHERNET)\n\nclass EthernetStatus\n{\npublic:\n    EthernetStatus(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\n    bool isConnected();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n\n    // static functions for logging Ethernet events to the UART\n    // they are using the same signature as WiFi events\n    static void onConnected(WiFiEvent_t event, WiFiEventInfo_t info);\n    static void onDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);\n    static void onGotIP(WiFiEvent_t event, WiFiEventInfo_t info);\n    esp_err_t ethernetStatus(PsychicRequest *request);\n};\n\n#endif // end FT_ENABLED(FT_ETHERNET)\n\n#endif // end EthernetStatus_h\n"
  },
  {
    "path": "lib/framework/EventEndpoint.h",
    "content": "#ifndef EventEndpoint_h\n#define EventEndpoint_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <EventSocket.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <StatefulService.h>\n\ntemplate <class T>\nclass EventEndpoint\n{\npublic:\n    EventEndpoint(JsonStateReader<T> stateReader,\n                  JsonStateUpdater<T> stateUpdater,\n                  StatefulService<T> *statefulService,\n                  EventSocket *socket, const char *event) : _stateReader(stateReader),\n                                                            _stateUpdater(stateUpdater),\n                                                            _statefulService(statefulService),\n                                                            _socket(socket),\n                                                            _event(event)\n    {\n        _statefulService->addUpdateHandler([&](const String &originId)\n                                           { syncState(originId); },\n                                           false);\n    }\n\n    void begin()\n    {\n        _socket->registerEvent(_event);\n        _socket->onEvent(_event, std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));\n        _socket->onSubscribe(_event, [&](const String &originId)\n                             { syncState(originId, true); });\n    }\n\nprivate:\n    JsonStateReader<T> _stateReader;\n    JsonStateUpdater<T> _stateUpdater;\n    StatefulService<T> *_statefulService;\n    EventSocket *_socket;\n    const char *_event;\n\n    void updateState(JsonObject &root, int originId)\n    {\n        _statefulService->update(root, _stateUpdater, String(originId));\n    }\n\n    void syncState(const String &originId, bool sync = false)\n    {\n        JsonDocument jsonDocument;\n        JsonObject root = jsonDocument.to<JsonObject>();\n        _statefulService->read(root, _stateReader);\n        JsonObject jsonObject = jsonDocument.as<JsonObject>();\n        _socket->emitEvent(_event, jsonObject, originId.c_str(), sync);\n    }\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/EventSocket.cpp",
    "content": "#include <EventSocket.h>\n\nSemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex();\n\nEventSocket::EventSocket(PsychicHttpServer *server,\n                         SecurityManager *securityManager,\n                         AuthenticationPredicate authenticationPredicate) : _server(server),\n                                                                            _securityManager(securityManager),\n                                                                            _authenticationPredicate(authenticationPredicate)\n{\n}\n\nvoid EventSocket::begin()\n{\n    _socket.setFilter(_securityManager->filterRequest(_authenticationPredicate));\n    _socket.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1)));\n    _socket.onClose(std::bind(&EventSocket::onWSClose, this, std::placeholders::_1));\n    _socket.onFrame(std::bind(&EventSocket::onFrame, this, std::placeholders::_1, std::placeholders::_2));\n    _server->on(EVENT_SERVICE_PATH, &_socket);\n\n    ESP_LOGV(SVK_TAG, \"Registered event socket endpoint: %s\", EVENT_SERVICE_PATH);\n}\n\nvoid EventSocket::registerEvent(String event)\n{\n    if (!isEventValid(event))\n    {\n        ESP_LOGD(SVK_TAG, \"Registering event: %s\", event.c_str());\n        events.push_back(event);\n    }\n    else\n    {\n        ESP_LOGW(SVK_TAG, \"Event already registered: %s\", event.c_str());\n    }\n}\n\nvoid EventSocket::onWSOpen(PsychicWebSocketClient *client)\n{\n    ESP_LOGI(SVK_TAG, \"ws[%s][%u] connect\", client->remoteIP().toString().c_str(), client->socket());\n}\n\nvoid EventSocket::onWSClose(PsychicWebSocketClient *client)\n{\n    xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);\n    for (auto &event_subscriptions : client_subscriptions)\n    {\n        event_subscriptions.second.remove(client->socket());\n    }\n    xSemaphoreGive(clientSubscriptionsMutex);\n    ESP_LOGI(SVK_TAG, \"ws[%s][%u] disconnect\", client->remoteIP().toString().c_str(), client->socket());\n}\n\nesp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame)\n{\n    ESP_LOGV(SVK_TAG, \"ws[%s][%u] opcode[%d]\", request->client()->remoteIP().toString().c_str(),\n             request->client()->socket(), frame->type);\n\n    JsonDocument doc;\n#if FT_ENABLED(EVENT_USE_JSON)\n    if (frame->type == HTTPD_WS_TYPE_TEXT)\n    {\n        ESP_LOGV(SVK_TAG, \"ws[%s][%u] request: %s\", request->client()->remoteIP().toString().c_str(),\n                 request->client()->socket(), (char *)frame->payload);\n\n        DeserializationError error = deserializeJson(doc, (char *)frame->payload, frame->len);\n#else\n    if (frame->type == HTTPD_WS_TYPE_BINARY)\n    {\n        ESP_LOGV(SVK_TAG, \"ws[%s][%u] request: %s\", request->client()->remoteIP().toString().c_str(),\n                 request->client()->socket(), (char *)frame->payload);\n\n        DeserializationError error = deserializeMsgPack(doc, (char *)frame->payload, frame->len);\n#endif\n\n        if (!error && doc.is<JsonObject>())\n        {\n            String event = doc[\"event\"];\n            if (event == \"subscribe\")\n            {\n                // only subscribe to events that are registered\n                if (isEventValid(doc[\"data\"].as<String>()))\n                {\n                    client_subscriptions[doc[\"data\"]].push_back(request->client()->socket());\n                    handleSubscribeCallbacks(doc[\"data\"], String(request->client()->socket()));\n                }\n                else\n                {\n                    ESP_LOGW(SVK_TAG, \"Client tried to subscribe to unregistered event: %s\", doc[\"data\"].as<String>().c_str());\n                }\n            }\n            else if (event == \"unsubscribe\")\n            {\n                client_subscriptions[doc[\"data\"]].remove(request->client()->socket());\n            }\n            else\n            {\n                JsonObject jsonObject = doc[\"data\"].as<JsonObject>();\n                handleEventCallbacks(event, jsonObject, request->client()->socket());\n            }\n            return ESP_OK;\n        }\n        ESP_LOGW(SVK_TAG, \"Error[%d] parsing JSON: %s\", error, (char *)frame->payload);\n    }\n    return ESP_OK;\n}\n\nvoid EventSocket::emitEvent(String event, JsonObject &jsonObject, const char *originId, bool onlyToSameOrigin)\n{\n    // Only process valid events\n    if (!isEventValid(String(event)))\n    {\n        ESP_LOGW(SVK_TAG, \"Method tried to emit unregistered event: %s\", event);\n        return;\n    }\n\n    int originSubscriptionId = originId[0] ? atoi(originId) : -1;\n    xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);\n    auto &subscriptions = client_subscriptions[event];\n    if (subscriptions.empty())\n    {\n        xSemaphoreGive(clientSubscriptionsMutex);\n        return;\n    }\n\n    JsonDocument doc;\n    doc[\"event\"] = event;\n    doc[\"data\"] = jsonObject;\n\n#if FT_ENABLED(EVENT_USE_JSON)\n    size_t len = measureJson(doc);\n#else\n    size_t len = measureMsgPack(doc);\n#endif\n\n    char *output = new char[len + 1];\n\n#if FT_ENABLED(EVENT_USE_JSON)\n    serializeJson(doc, output, len + 1);\n#else\n    serializeMsgPack(doc, output, len);\n#endif\n\n    // null terminate the string\n    output[len] = '\\0';\n\n    // if onlyToSameOrigin == true, send the message back to the origin\n    if (onlyToSameOrigin && originSubscriptionId > 0)\n    {\n        auto *client = _socket.getClient(originSubscriptionId);\n        if (client)\n        {\n            ESP_LOGV(SVK_TAG, \"Emitting event: %s to %s[%u], Message[%d]: %s\", event, client->remoteIP().toString().c_str(), client->socket(), len, output);\n#if FT_ENABLED(EVENT_USE_JSON)\n            client->sendMessage(HTTPD_WS_TYPE_TEXT, output, len);\n#else\n            client->sendMessage(HTTPD_WS_TYPE_BINARY, output, len);\n#endif\n        }\n    }\n    else\n    { // else send the message to all other clients\n\n        for (int subscription : client_subscriptions[event])\n        {\n            if (subscription == originSubscriptionId)\n                continue;\n            auto *client = _socket.getClient(subscription);\n            if (!client)\n            {\n                subscriptions.remove(subscription);\n                continue;\n            }\n            ESP_LOGV(SVK_TAG, \"Emitting event: %s to %s[%u], Message[%d]: %s\", event, client->remoteIP().toString().c_str(), client->socket(), len, output);\n#if FT_ENABLED(EVENT_USE_JSON)\n            client->sendMessage(HTTPD_WS_TYPE_TEXT, output, len);\n#else\n            client->sendMessage(HTTPD_WS_TYPE_BINARY, output, len);\n#endif\n        }\n    }\n\n    delete[] output;\n    xSemaphoreGive(clientSubscriptionsMutex);\n}\n\nvoid EventSocket::handleEventCallbacks(String event, JsonObject &jsonObject, int originId)\n{\n    for (auto &callback : event_callbacks[event])\n    {\n        callback(jsonObject, originId);\n    }\n}\n\nvoid EventSocket::handleSubscribeCallbacks(String event, const String &originId)\n{\n    for (auto &callback : subscribe_callbacks[event])\n    {\n        callback(originId);\n    }\n}\n\nvoid EventSocket::onEvent(String event, EventCallback callback)\n{\n    if (!isEventValid(event))\n    {\n        ESP_LOGW(SVK_TAG, \"Method tried to register unregistered event: %s\", event.c_str());\n        return;\n    }\n    event_callbacks[event].push_back(callback);\n}\n\nvoid EventSocket::onSubscribe(String event, SubscribeCallback callback)\n{\n    if (!isEventValid(event))\n    {\n        ESP_LOGW(SVK_TAG, \"Method tried to subscribe to unregistered event: %s\", event.c_str());\n        return;\n    }\n    subscribe_callbacks[event].push_back(callback);\n    ESP_LOGI(SVK_TAG, \"onSubscribe for event: %s\", event.c_str());\n}\n\nbool EventSocket::isEventValid(String event)\n{\n    return std::find(events.begin(), events.end(), event) != events.end();\n}\n\nunsigned int EventSocket::getConnectedClients()\n{\n    return (unsigned int)_socket.getClientList().size();\n}\n"
  },
  {
    "path": "lib/framework/EventSocket.h",
    "content": "#ifndef Socket_h\n#define Socket_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <StatefulService.h>\n#include <list>\n#include <map>\n#include <vector>\n\n#define EVENT_SERVICE_PATH \"/ws/events\"\n\ntypedef std::function<void(JsonObject &root, int originId)> EventCallback;\ntypedef std::function<void(const String &originId)> SubscribeCallback;\n\nclass EventSocket\n{\npublic:\n    EventSocket(PsychicHttpServer *server, SecurityManager *_securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_AUTHENTICATED);\n\n    void begin();\n\n    void registerEvent(String event);\n\n    void onEvent(String event, EventCallback callback);\n\n    void onSubscribe(String event, SubscribeCallback callback);\n\n    void emitEvent(String event, JsonObject &jsonObject, const char *originId = \"\", bool onlyToSameOrigin = false);\n    // if onlyToSameOrigin == true, the message will be sent to the originId only, otherwise it will be broadcasted to all clients except the originId\n\n    bool isEventValid(String event);\n\n    unsigned int getConnectedClients();\n\nprivate:\n    PsychicHttpServer *_server;\n    PsychicWebSocketHandler _socket;\n    SecurityManager *_securityManager;\n    AuthenticationPredicate _authenticationPredicate;\n\n    std::vector<String> events;\n    std::map<String, std::list<int>> client_subscriptions;\n    std::map<String, std::list<EventCallback>> event_callbacks;\n    std::map<String, std::list<SubscribeCallback>> subscribe_callbacks;\n    void handleEventCallbacks(String event, JsonObject &jsonObject, int originId);\n    void handleSubscribeCallbacks(String event, const String &originId);\n\n    void onWSOpen(PsychicWebSocketClient *client);\n    void onWSClose(PsychicWebSocketClient *client);\n    esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/FSPersistence.h",
    "content": "#ifndef FSPersistence_h\n#define FSPersistence_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <StatefulService.h>\n#include <FS.h>\n\ntemplate <class T>\nclass FSPersistence\n{\npublic:\n    FSPersistence(JsonStateReader<T> stateReader,\n                  JsonStateUpdater<T> stateUpdater,\n                  StatefulService<T> *statefulService,\n                  FS *fs,\n                  const char *filePath) : _stateReader(stateReader),\n                                          _stateUpdater(stateUpdater),\n                                          _statefulService(statefulService),\n                                          _fs(fs),\n                                          _filePath(filePath),\n                                          _updateHandlerId(0)\n    {\n        enableUpdateHandler();\n    }\n\n    void readFromFS()\n    {\n        File settingsFile = _fs->open(_filePath, \"r\");\n\n        if (settingsFile)\n        {\n            JsonDocument jsonDocument;\n            DeserializationError error = deserializeJson(jsonDocument, settingsFile);\n            if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>())\n            {\n                JsonObject jsonObject = jsonDocument.as<JsonObject>();\n                _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater, _filePath);\n                settingsFile.close();\n                return;\n            }\n            settingsFile.close();\n        }\n\n        // If we reach here we have not been successful in loading the config and hard-coded defaults are now applied.\n        // The settings are then written back to the file system so the defaults persist between resets. This last step is\n        // required as in some cases defaults contain randomly generated values which would otherwise be modified on reset.\n        applyDefaults();\n        writeToFS();\n    }\n\n    bool writeToFS()\n    {\n        // create and populate a new json object\n        JsonDocument jsonDocument;\n        JsonObject jsonObject = jsonDocument.to<JsonObject>();\n        _statefulService->read(jsonObject, _stateReader);\n\n        // make directories if required\n        mkdirs();\n\n        // serialize it to filesystem\n        File settingsFile = _fs->open(_filePath, \"w\");\n\n        // failed to open file, return false\n        if (!settingsFile)\n        {\n            return false;\n        }\n\n        // serialize the data to the file\n        serializeJson(jsonDocument, settingsFile);\n        settingsFile.close();\n        return true;\n    }\n\n    void disableUpdateHandler()\n    {\n        if (_updateHandlerId)\n        {\n            _statefulService->removeUpdateHandler(_updateHandlerId);\n            _updateHandlerId = 0;\n        }\n    }\n\n    void enableUpdateHandler()\n    {\n        if (!_updateHandlerId)\n        {\n            _updateHandlerId = _statefulService->addUpdateHandler([&](const String &originId)\n                                                                  { writeToFS(); });\n        }\n    }\n\nprivate:\n    JsonStateReader<T> _stateReader;\n    JsonStateUpdater<T> _stateUpdater;\n    StatefulService<T> *_statefulService;\n    FS *_fs;\n    const char *_filePath;\n    update_handler_id_t _updateHandlerId;\n\n    // We assume we have a _filePath with format \"/directory1/directory2/filename\"\n    // We create a directory for each missing parent\n    void mkdirs()\n    {\n        String path(_filePath);\n        int index = 0;\n        while ((index = path.indexOf('/', index + 1)) != -1)\n        {\n            String segment = path.substring(0, index);\n            if (!_fs->exists(segment))\n            {\n                _fs->mkdir(segment);\n            }\n        }\n    }\n\nprotected:\n    // We assume the updater supplies sensible defaults if an empty object\n    // is supplied, this virtual function allows that to be changed.\n    virtual void applyDefaults()\n    {\n        JsonDocument jsonDocument;\n        JsonObject jsonObject = jsonDocument.as<JsonObject>();\n        _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater, _filePath);\n    }\n};\n\n#endif // end FSPersistence\n"
  },
  {
    "path": "lib/framework/FactoryResetService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <FactoryResetService.h>\n\nusing namespace std::placeholders;\n\nFactoryResetService::FactoryResetService(PsychicHttpServer *server,\n                                         FS *fs,\n                                         SecurityManager *securityManager) : _server(server),\n                                                                             fs(fs),\n                                                                             _securityManager(securityManager)\n{\n}\n\nvoid FactoryResetService::begin()\n{\n    _server->on(FACTORY_RESET_SERVICE_PATH,\n                HTTP_POST,\n                _securityManager->wrapRequest(std::bind(&FactoryResetService::handleRequest, this, _1), AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", FACTORY_RESET_SERVICE_PATH);\n}\n\nesp_err_t FactoryResetService::handleRequest(PsychicRequest *request)\n{\n    request->reply(200);\n    factoryReset();\n\n    return ESP_OK;\n}\n\n/**\n * Delete function assumes that all files are stored flat, within the config directory.\n */\nvoid FactoryResetService::factoryReset()\n{\n    File root = fs->open(FS_CONFIG_DIRECTORY);\n    File file;\n    while (file = root.openNextFile())\n    {\n        String path = file.path();\n        file.close();\n        fs->remove(path);\n    }\n    RestartService::restartNow();\n}\n"
  },
  {
    "path": "lib/framework/FactoryResetService.h",
    "content": "#ifndef FactoryResetService_h\n#define FactoryResetService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <RestartService.h>\n#include <FS.h>\n\n#define FS_CONFIG_DIRECTORY \"/config\"\n#define FACTORY_RESET_SERVICE_PATH \"/rest/factoryReset\"\n\nclass FactoryResetService\n{\n    FS *fs;\n\npublic:\n    FactoryResetService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);\n\n    void begin();\n    void factoryReset();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    esp_err_t handleRequest(PsychicRequest *request);\n};\n\n#endif // end FactoryResetService_h\n"
  },
  {
    "path": "lib/framework/Features.h",
    "content": "#ifndef Features_h\n#define Features_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#define FT_ENABLED(feature) feature\n\n// security feature on by default\n#ifndef FT_SECURITY\n#define FT_SECURITY 1\n#endif\n\n// mqtt feature on by default\n#ifndef FT_MQTT\n#define FT_MQTT 1\n#endif\n\n// ntp feature on by default\n#ifndef FT_NTP\n#define FT_NTP 1\n#endif\n\n// upload firmware feature off by default\n#ifndef FT_UPLOAD_FIRMWARE\n#define FT_UPLOAD_FIRMWARE 0\n#endif\n\n// download firmware feature off by default\n#ifndef FT_DOWNLOAD_FIRMWARE\n#define FT_DOWNLOAD_FIRMWARE 0\n#endif\n\n// ESP32 sleep states off by default\n#ifndef FT_SLEEP\n#define FT_SLEEP 0\n#endif\n\n// ESP32 battery state off by default\n#ifndef FT_BATTERY\n#define FT_BATTERY 0\n#endif\n\n// ESP32 analytics on by default\n#ifndef FT_ANALYTICS\n#define FT_ANALYTICS 1\n#endif\n\n// Use JSON for events. Default, use MessagePack for events\n#ifndef EVENT_USE_JSON\n#define EVENT_USE_JSON 0\n#endif\n\n// Endpoint for Core Dump, off by default\n#ifndef FT_COREDUMP\n#define FT_COREDUMP 0\n#endif\n\n// Ethernet feature off by default\n#ifndef FT_ETHERNET\n#define FT_ETHERNET 0\n#endif\n\n#endif\n"
  },
  {
    "path": "lib/framework/FeaturesService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <FeaturesService.h>\n\nFeaturesService::FeaturesService(PsychicHttpServer *server, EventSocket *eventsocket) : _server(server), _socket(eventsocket)\n{\n}\n\nvoid FeaturesService::begin()\n{\n    _server->on(FEATURES_SERVICE_PATH, HTTP_GET, [&](PsychicRequest *request)\n                {\n                    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n                    JsonObject root = response.getRoot();\n                    createJSON(root);\n                    return response.send(); });\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", FEATURES_SERVICE_PATH);\n\n    _socket->registerEvent(FEATURES_SERVICE_EVENT);\n\n    _socket->onSubscribe(FEATURES_SERVICE_EVENT, [&](const String &originId)\n                         {\n                             ESP_LOGV(SVK_TAG, \"Sending features to %s\", originId.c_str());\n                             JsonDocument doc;\n                             JsonObject root = doc.as<JsonObject>();\n                             createJSON(root);\n                             _socket->emitEvent(FEATURES_SERVICE_EVENT, root); });\n}\n\nvoid FeaturesService::addFeature(String feature, bool enabled)\n{\n    UserFeature newFeature;\n    newFeature.feature = feature;\n    newFeature.enabled = enabled;\n\n    bool featureExists = false;\n\n    // Check if the feature already exists\n    for (auto &existingFeature : userFeatures)\n    {\n        if (existingFeature.feature == feature)\n        {\n            // Update the existing feature\n            existingFeature.enabled = enabled;\n            featureExists = true;\n            break;\n        }\n    }\n\n    if (!featureExists)\n    {\n        // If the feature does not exist, add it\n        userFeatures.push_back(newFeature);\n    }\n\n    JsonDocument doc;\n    JsonObject root = doc.as<JsonObject>();\n    createJSON(root);\n    _socket->emitEvent(FEATURES_SERVICE_EVENT, root);\n}\n\nvoid FeaturesService::createJSON(JsonObject &root)\n{\n\n#if FT_ENABLED(FT_SECURITY)\n    root[\"security\"] = true;\n#else\n    root[\"security\"] = false;\n#endif\n#if FT_ENABLED(FT_MQTT)\n    root[\"mqtt\"] = true;\n#else\n    root[\"mqtt\"] = false;\n#endif\n#if FT_ENABLED(FT_NTP)\n    root[\"ntp\"] = true;\n#else\n    root[\"ntp\"] = false;\n#endif\n#if FT_ENABLED(FT_UPLOAD_FIRMWARE)\n    root[\"upload_firmware\"] = true;\n#else\n    root[\"upload_firmware\"] = false;\n#endif\n#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)\n    root[\"download_firmware\"] = true;\n#else\n    root[\"download_firmware\"] = false;\n#endif\n#if FT_ENABLED(FT_SLEEP)\n    root[\"sleep\"] = true;\n#else\n    root[\"sleep\"] = false;\n#endif\n#if FT_ENABLED(FT_BATTERY)\n    root[\"battery\"] = true;\n#else\n    root[\"battery\"] = false;\n#endif\n#if FT_ENABLED(FT_ANALYTICS)\n    root[\"analytics\"] = true;\n#else\n    root[\"analytics\"] = false;\n#endif\n#if FT_ENABLED(FT_COREDUMP)\n    root[\"coredump\"] = true;\n#else\n    root[\"coredump\"] = false;\n#endif\n\n#if FT_ENABLED(EVENT_USE_JSON)\n    root[\"event_use_json\"] = true;\n#else\n    root[\"event_use_json\"] = false;\n#endif\n\n#if FT_ENABLED(FT_ETHERNET)\n    root[\"ethernet\"] = true;\n#else\n    root[\"ethernet\"] = false;\n#endif\n\n    root[\"firmware_version\"] = APP_VERSION;\n    root[\"firmware_name\"] = APP_NAME;\n    root[\"firmware_built_target\"] = BUILD_TARGET;\n\n    // Iterate over user features\n    for (auto &element : userFeatures)\n    {\n        root[element.feature.c_str()] = element.enabled;\n    }\n}"
  },
  {
    "path": "lib/framework/FeaturesService.h",
    "content": "#ifndef FeaturesService_h\n#define FeaturesService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Features.h>\n\n#include <WiFi.h>\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <EventSocket.h>\n#include <vector>\n\n#define FEATURES_SERVICE_PATH \"/rest/features\"\n#define FEATURES_SERVICE_EVENT \"features\"\n\ntypedef struct\n{\n    String feature;\n    bool enabled;\n} UserFeature;\n\nclass FeaturesService\n{\npublic:\n    FeaturesService(PsychicHttpServer *server, EventSocket *socket);\n\n    void begin();\n\n    void addFeature(String feature, bool enabled);\n\nprivate:\n    PsychicHttpServer *_server;\n    EventSocket *_socket;\n    std::vector<UserFeature> userFeatures;\n\n    void createJSON(JsonObject &root);\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/FirmwareUpdateEvents.h",
    "content": "#ifndef FirmwareUpdateEvents_h\n#define FirmwareUpdateEvents_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2025 hmbacher\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n/**\n * WebSocket event name for OTA firmware updates\n * Used by both UploadFirmwareService and DownloadFirmwareService\n */\n#define EVENT_OTA_UPDATE \"otastatus\"\n\n/**\n * Event JSON Structure:\n * \n * {\n *   \"status\": string,        // Current update status\n *   \"progress\": number,      // Progress percentage (0-100)\n *   \"bytes_written\": number, // Optional: Bytes written so far\n *   \"total_bytes\": number,   // Optional: Total bytes to write\n *   \"error\": string          // Optional: Error message if status is \"error\"\n * }\n * \n * Status Values:\n * - \"none\"      : Frontend-only initial state, no update in progress (never sent by backend)\n * - \"preparing\" : Update started, preparing to receive data\n * - \"progress\"  : Update in progress, progress field contains percentage\n * - \"finished\"  : Update completed successfully, device will restart\n * - \"error\"     : Update failed, error field contains error message\n * \n * Example Events:\n * \n * Preparing:\n * {\"status\":\"preparing\",\"progress\":0}\n * \n * Progress:\n * {\"status\":\"progress\",\"progress\":45,\"bytes_written\":102400,\"total_bytes\":227328}\n * \n * Finished:\n * {\"status\":\"finished\",\"progress\":100}\n * \n * Error:\n * {\"status\":\"error\",\"error\":\"Firmware update failed\"}\n */\n\n#endif\n"
  },
  {
    "path": "lib/framework/HttpEndpoint.h",
    "content": "#ifndef HttpEndpoint_h\n#define HttpEndpoint_h\n\n#include <functional>\n\n#include <PsychicHttp.h>\n\n#include <SecurityManager.h>\n#include <StatefulService.h>\n\n#define HTTP_ENDPOINT_ORIGIN_ID \"http\"\n#define HTTPS_ENDPOINT_ORIGIN_ID \"https\"\n\nusing namespace std::placeholders; // for `_1` etc\n\ntemplate <class T>\nclass HttpEndpoint\n{\nprotected:\n    JsonStateReader<T> _stateReader;\n    JsonStateUpdater<T> _stateUpdater;\n    StatefulService<T> *_statefulService;\n    SecurityManager *_securityManager;\n    AuthenticationPredicate _authenticationPredicate;\n    PsychicHttpServer *_server;\n    const char *_servicePath;\n\npublic:\n    HttpEndpoint(JsonStateReader<T> stateReader,\n                 JsonStateUpdater<T> stateUpdater,\n                 StatefulService<T> *statefulService,\n                 PsychicHttpServer *server,\n                 const char *servicePath,\n                 SecurityManager *securityManager,\n                 AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _stateReader(stateReader),\n                                                                                                         _stateUpdater(stateUpdater),\n                                                                                                         _statefulService(statefulService),\n                                                                                                         _server(server),\n                                                                                                         _servicePath(servicePath),\n                                                                                                         _securityManager(securityManager),\n                                                                                                         _authenticationPredicate(authenticationPredicate)\n    {\n    }\n\n    // register the web server on() endpoints\n    void begin()\n    {\n\n// OPTIONS (for CORS preflight)\n#ifdef ENABLE_CORS\n        _server->on(_servicePath,\n                    HTTP_OPTIONS,\n                    _securityManager->wrapRequest(\n                        [this](PsychicRequest *request)\n                        {\n                            return request->reply(200);\n                        },\n                        AuthenticationPredicates::IS_AUTHENTICATED));\n#endif\n\n        // GET\n        _server->on(_servicePath,\n                    HTTP_GET,\n                    _securityManager->wrapRequest(\n                        [this](PsychicRequest *request)\n                        {\n                            PsychicJsonResponse response = PsychicJsonResponse(request, false);\n                            JsonObject jsonObject = response.getRoot();\n                            _statefulService->read(jsonObject, _stateReader);\n                            return response.send();\n                        },\n                        _authenticationPredicate));\n        ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", _servicePath);\n\n        // POST\n        _server->on(_servicePath,\n                    HTTP_POST,\n                    _securityManager->wrapCallback(\n                        [this](PsychicRequest *request, JsonVariant &json)\n                        {\n                            if (!json.is<JsonObject>())\n                            {\n                                return request->reply(400);\n                            }\n\n                            JsonObject jsonObject = json.as<JsonObject>();\n                            StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater, _servicePath);\n\n                            if (outcome == StateUpdateResult::ERROR)\n                            {\n                                return request->reply(400);\n                            }\n                            else if ((outcome == StateUpdateResult::CHANGED))\n                            {\n                                // persist the changes to the FS\n                                _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID);\n                            }\n\n                            PsychicJsonResponse response = PsychicJsonResponse(request, false);\n                            jsonObject = response.getRoot();\n\n                            _statefulService->read(jsonObject, _stateReader);\n\n                            return response.send();\n                        },\n                        _authenticationPredicate));\n\n        ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", _servicePath);\n    }\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/IPUtils.h",
    "content": "#ifndef IPUtils_h\n#define IPUtils_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <IPAddress.h>\n\nconst IPAddress IP_NOT_SET = IPAddress(INADDR_NONE);\n\nclass IPUtils\n{\npublic:\n    static bool isSet(const IPAddress &ip)\n    {\n        return ip != IP_NOT_SET;\n    }\n    static bool isNotSet(const IPAddress &ip)\n    {\n        return ip == IP_NOT_SET;\n    }\n};\n\n#endif // end IPUtils_h\n"
  },
  {
    "path": "lib/framework/JsonUtils.h",
    "content": "#ifndef JsonUtils_h\n#define JsonUtils_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n#include <IPUtils.h>\n#include <ArduinoJson.h>\n\nclass JsonUtils\n{\npublic:\n    static void readIPStr(JsonObject &root, const String &key, IPAddress &ip, const String &def)\n    {\n        IPAddress defaultIp = {};\n        if (!defaultIp.fromString(def))\n        {\n            defaultIp = INADDR_NONE;\n        }\n        readIP(root, key, ip, defaultIp);\n    }\n\n    static void readIP(JsonObject &root, const String &key, IPAddress &ip, const IPAddress &defaultIp = INADDR_NONE)\n    {\n        if (!root[key].is<String>() || !ip.fromString(root[key].as<String>()))\n        {\n            ip = defaultIp;\n        }\n    }\n\n    static void writeIP(JsonObject &root, const String &key, const IPAddress &ip)\n    {\n        if (IPUtils::isSet(ip))\n        {\n            root[key] = ip.toString();\n        }\n    }\n};\n\n#endif // end JsonUtils\n"
  },
  {
    "path": "lib/framework/LICENSE",
    "content": "ESP32-SvelteKit is distributed with two licenses for different sections of the\ncode. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3\nand is therefore distributed said license. The front end code is distributed\nunder the MIT License.\n\n                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\nThis version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n0. Additional Definitions.\n\nAs used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n\"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\nAn \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\nA \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library. The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\nThe \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\nThe \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n1. Exception to Section 3 of the GNU GPL.\n\nYou may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n2. Conveying Modified Versions.\n\nIf you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\na) under this License, provided that you make a good faith effort to\nensure that, in the event an Application does not supply the\nfunction or data, the facility still operates, and performs\nwhatever part of its purpose remains meaningful, or\n\nb) under the GNU GPL, with none of the additional permissions of\nthis License applicable to that copy.\n\n3. Object Code Incorporating Material from Library Header Files.\n\nThe object code form of an Application may incorporate material from\na header file that is part of the Library. You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\na) Give prominent notice with each copy of the object code that the\nLibrary is used in it and that the Library and its use are\ncovered by this License.\n\nb) Accompany the object code with a copy of the GNU GPL and this license\ndocument.\n\n4. Combined Works.\n\nYou may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\na) Give prominent notice with each copy of the Combined Work that\nthe Library is used in it and that the Library and its use are\ncovered by this License.\n\nb) Accompany the Combined Work with a copy of the GNU GPL and this license\ndocument.\n\nc) For a Combined Work that displays copyright notices during\nexecution, include the copyright notice for the Library among\nthese notices, as well as a reference directing the user to the\ncopies of the GNU GPL and this license document.\n\nd) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\ne) Provide Installation Information, but only if you would otherwise\nbe required to provide such information under section 6 of the\nGNU GPL, and only to the extent that such information is\nnecessary to install and execute a modified version of the\nCombined Work produced by recombining or relinking the\nApplication with a modified version of the Linked Version. (If\nyou use option 4d0, the Installation Information must accompany\nthe Minimal Corresponding Source and Corresponding Application\nCode. If you use option 4d1, you must provide the Installation\nInformation in the manner specified by section 6 of the GNU GPL\nfor conveying Corresponding Source.)\n\n5. Combined Libraries.\n\nYou may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\na) Accompany the combined library with a copy of the same work based\non the Library, uncombined with any other library facilities,\nconveyed under the terms of this License.\n\nb) Give prominent notice with the combined library that part of it\nis a work based on the Library, and explaining where to find the\naccompanying uncombined form of the same work.\n\n6. Revised Versions of the GNU Lesser General Public License.\n\nThe Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\nIf the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "lib/framework/MqttEndpoint.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n#include <vector>\n#include <MqttEndpoint.h>\n#include <MqttSettingsService.h>\n\nstd::vector<MqttCommitHandler *> MqttCommitHandler::_instances;\nTimerHandle_t MqttCommitHandler::_sendTimer = nullptr;\nuint32_t MqttCommitHandler::_timerIntervalMs = FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS; // default"
  },
  {
    "path": "lib/framework/MqttEndpoint.h",
    "content": "#ifndef MqttEndpoint_h\n#define MqttEndpoint_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <StatefulService.h>\n#include <PsychicMqttClient.h>\n#include <SecurityManager.h>\n#include <vector>\n\n#define MQTT_ORIGIN_ID \"mqtt\"\n\n// Commit interface, needed to ensure that the MqttEndpoint can be used in a commit pattern without template\nclass MqttCommitHandler\n{\npublic:\n    MqttCommitHandler()\n    {\n        if (_instances.size() == 0)\n        {\n            _sendTimer = xTimerCreate(\"MqttSendTimer\",\n                                      pdMS_TO_TICKS(500),\n                                      pdTRUE,\n                                      nullptr,\n                                      commitPending);\n            setTimerInterval(_timerIntervalMs);\n        }\n        _instances.push_back(this);\n    }\n    virtual void commit() {};\n    virtual ~MqttCommitHandler() = default;\n    static void setTimerInterval(uint32_t intervalMs)\n    {\n        _timerIntervalMs = intervalMs;\n        if (_sendTimer)\n        {\n            if (intervalMs == 0)\n            {\n                xTimerStop(_sendTimer, 0); // Disable timer (no throttling)\n            }\n            else\n            {\n                xTimerChangePeriod(_sendTimer, pdMS_TO_TICKS(intervalMs), 0); // Update interval\n                xTimerStart(_sendTimer, 0);\n            }\n        }\n    }\n    static uint32_t getTimerInterval()\n    {\n        return _timerIntervalMs;\n    }\n\nprotected:\n    static std::vector<MqttCommitHandler *> _instances;\n    static void commitPending(TimerHandle_t xTimer)\n    {\n        ESP_LOGV(SVK_TAG, \"Publishing pending MQTT messages\");\n        for (auto instance : _instances)\n        {\n            instance->commit();\n        }\n    }\n    static TimerHandle_t _sendTimer;\n    static uint32_t _timerIntervalMs;\n};\n\ntemplate <class T>\nclass MqttEndpoint : public MqttCommitHandler\n{\npublic:\n    MqttEndpoint(JsonStateReader<T> stateReader,\n                 JsonStateUpdater<T> stateUpdater,\n                 StatefulService<T> *statefulService,\n                 PsychicMqttClient *mqttClient,\n                 const String &pubTopic = \"\",\n                 const String &subTopic = \"\",\n                 int QoS = 0,\n                 bool retain = false) : _stateReader(stateReader),\n                                        _stateUpdater(stateUpdater),\n                                        _statefulService(statefulService),\n                                        _mqttClient(mqttClient),\n                                        _pubTopic(pubTopic),\n                                        _subTopic(subTopic),\n                                        _qos(QoS),\n                                        _retain(retain),\n                                        _pendingCommit(false)\n\n    {\n        _statefulService->addUpdateHandler([&](const String &originId)\n                                           { publish(); },\n                                           false);\n\n        _mqttClient->onConnect(std::bind(&MqttEndpoint::onConnect, this));\n\n        _mqttClient->onMessage(std::bind(&MqttEndpoint::onMqttMessage,\n                                         this,\n                                         std::placeholders::_1,\n                                         std::placeholders::_2,\n                                         std::placeholders::_3,\n                                         std::placeholders::_4,\n                                         std::placeholders::_5));\n    }\n\n    void configureTopics(const String &pubTopic, const String &subTopic)\n    {\n        setSubTopic(subTopic);\n        setPubTopic(pubTopic);\n    }\n\n    void setSubTopic(const String &subTopic)\n    {\n        if (!_subTopic.equals(subTopic))\n        {\n            // unsubscribe from the existing topic if one was set\n            if (_subTopic.length() > 0)\n            {\n                _mqttClient->unsubscribe(_subTopic.c_str());\n            }\n            // set the new topic and re-configure the subscription\n            _subTopic = subTopic;\n            subscribe();\n        }\n    }\n\n    void setPubTopic(const String &pubTopic)\n    {\n        _pubTopic = pubTopic;\n        publish();\n    }\n\n    void setRetain(const bool retain)\n    {\n        _retain = retain;\n        publish();\n    }\n\n    void commit() override\n    {\n        if (!_pendingCommit)\n        {\n            return; // nothing to do\n        }\n        if (_pubTopic.length() > 0 && _mqttClient->connected())\n        {\n            // serialize to json doc\n            JsonDocument json;\n            JsonObject jsonObject = json.to<JsonObject>();\n            _statefulService->read(jsonObject, _stateReader);\n\n            // serialize to string\n            String payload;\n            serializeJson(json, payload);\n\n            // publish the payload\n            _mqttClient->publish(_pubTopic.c_str(), _qos, _retain, payload.c_str(), 0, false);\n        }\n        _pendingCommit = false;\n    }\n\n    void publish()\n    {\n        _pendingCommit = true;\n        if (MqttCommitHandler::getTimerInterval() == 0)\n        {\n            commit(); // No throttling—send immediately\n        }\n    }\n\n    PsychicMqttClient *getMqttClient()\n    {\n        return _mqttClient;\n    }\n\nprotected:\n    StatefulService<T> *_statefulService;\n    PsychicMqttClient *_mqttClient;\n    JsonStateUpdater<T> _stateUpdater;\n    JsonStateReader<T> _stateReader;\n    String _subTopic;\n    String _pubTopic;\n    int _qos;\n    bool _retain;\n    bool _pendingCommit;\n\n    void onMqttMessage(char *topic,\n                       char *payload,\n                       int retain,\n                       int qos,\n                       bool dup)\n    {\n        // we only care about the topic we are watching in this class\n        if (strcmp(_subTopic.c_str(), topic))\n        {\n            return;\n        }\n\n        // deserialize from string\n        JsonDocument json;\n        DeserializationError error = deserializeJson(json, payload);\n        if (!error && json.is<JsonObject>())\n        {\n            JsonObject jsonObject = json.as<JsonObject>();\n            _statefulService->update(jsonObject, _stateUpdater, MQTT_ORIGIN_ID);\n        }\n    }\n\n    void onConnect()\n    {\n        subscribe();\n        publish();\n    }\n\n    void subscribe()\n    {\n        if (_subTopic.length() > 0)\n        {\n            _mqttClient->subscribe(_subTopic.c_str(), 2);\n        }\n    }\n};\n\n#endif // end MqttEndpoint\n"
  },
  {
    "path": "lib/framework/MqttSettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <MqttSettingsService.h>\n\n/**\n * Load the root certificate bundle embedded by the build process\n */\nextern const uint8_t rootca_crt_bundle_start[] asm(\"_binary_src_certs_x509_crt_bundle_bin_start\");\nextern const uint8_t rootca_crt_bundle_end[] asm(\"_binary_src_certs_x509_crt_bundle_bin_end\");\n\n/**\n * Retains a copy of the cstr provided in the pointer provided using dynamic allocation.\n *\n * Frees the pointer before allocation and leaves it as nullptr if cstr == nullptr.\n */\nstatic char *retainCstr(const char *cstr, char **ptr)\n{\n    // free up previously retained value if exists\n    free(*ptr);\n    *ptr = nullptr;\n\n    // dynamically allocate and copy cstr (if non null)\n    if (cstr != nullptr)\n    {\n        *ptr = (char *)malloc(strlen(cstr) + 1);\n        strcpy(*ptr, cstr);\n    }\n\n    // return reference to pointer for convenience\n    return *ptr;\n}\n\nMqttSettingsService::MqttSettingsService(PsychicHttpServer *server,\n                                         FS *fs,\n                                         SecurityManager *securityManager) : _server(server),\n                                                                             _securityManager(securityManager),\n                                                                             _httpEndpoint(MqttSettings::read, MqttSettings::update, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager),\n                                                                             _fsPersistence(MqttSettings::read, MqttSettings::update, this, fs, MQTT_SETTINGS_FILE),\n                                                                             _retainedHost(nullptr),\n                                                                             _retainedClientId(nullptr),\n                                                                             _retainedUsername(nullptr),\n                                                                             _retainedPassword(nullptr),\n                                                                             _reconfigureMqtt(false),\n                                                                             _mqttClient(),\n                                                                             _lastError(\"None\")\n{\n    String status_topic = SettingValue::format(FACTORY_MQTT_STATUS_TOPIC);\n    retainCstr(status_topic.c_str(), &_retainedWillTopic);\n    addUpdateHandler([&](const String &originId)\n                     { onConfigUpdated(); },\n                     false);\n\n#if ESP_ARDUINO_VERSION_MAJOR == 3\n    _mqttClient.setCACertBundle(rootca_crt_bundle_start, rootca_crt_bundle_end - rootca_crt_bundle_start);\n#else\n    _mqttClient.setCACertBundle(rootca_crt_bundle_start);\n#endif\n}\n\nMqttSettingsService::~MqttSettingsService()\n{\n}\n\nvoid MqttSettingsService::begin()\n{\n    WiFi.onEvent(\n        std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),\n        WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);\n    WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2),\n                 WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);\n    _mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, std::placeholders::_1));\n    _mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, std::placeholders::_1));\n    _mqttClient.onError(std::bind(&MqttSettingsService::onMqttError, this, std::placeholders::_1));\n\n    _httpEndpoint.begin();\n    _fsPersistence.readFromFS();\n}\n\nvoid MqttSettingsService::loop()\n{\n    if (_reconfigureMqtt)\n    {\n        // reconfigure MQTT client\n        configureMqtt();\n\n        // clear the reconnection flags\n        _reconfigureMqtt = false;\n    }\n}\n\nbool MqttSettingsService::isEnabled()\n{\n    return _state.enabled;\n}\n\nbool MqttSettingsService::isConnected()\n{\n    return _mqttClient.connected();\n}\n\nconst char *MqttSettingsService::getClientId()\n{\n    // return _mqttClient.getClientId();\n    return _state.clientId.c_str();\n}\n\nPsychicMqttClient *MqttSettingsService::getMqttClient()\n{\n    return &_mqttClient;\n}\n\nString MqttSettingsService::getLastError()\n{\n    return _lastError;\n}\n\nvoid MqttSettingsService::onMqttConnect(bool sessionPresent)\n{\n\n#if ESP_IDF_VERSION_MAJOR == 5\n    String uri = _mqttClient.getMqttConfig()->broker.address.uri;\n#else\n    String uri = _mqttClient.getMqttConfig()->uri;\n#endif\n\n    ESP_LOGI(SVK_TAG, \"Connected to MQTT: %s\", uri.c_str());\n#ifdef SERIAL_INFO\n    Serial.printf(\"Connected to MQTT: %s\\n\", uri.c_str());\n#endif\n    _lastError = \"None\";\n    // publish status message\n    _mqttClient.publish(_retainedWillTopic, 1, true, \"online\");\n}\n\nvoid MqttSettingsService::onMqttDisconnect(bool sessionPresent)\n{\n    ESP_LOGI(SVK_TAG, \"Disconnected from MQTT.\");\n#ifdef SERIAL_INFO\n    Serial.println(\"Disconnected from MQTT.\");\n#endif\n}\n\nvoid MqttSettingsService::onMqttError(esp_mqtt_error_codes_t error)\n{\n    if (error.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT)\n    {\n        _lastError = strerror(error.esp_transport_sock_errno);\n        ESP_LOGE(SVK_TAG, \"MQTT TCP error: %s\", _lastError.c_str());\n    }\n}\n\nvoid MqttSettingsService::onConfigUpdated()\n{\n    _reconfigureMqtt = true;\n}\n\nvoid MqttSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    if (_state.enabled)\n    {\n        ESP_LOGI(SVK_TAG, \"WiFi connection established, starting MQTT client.\");\n        onConfigUpdated();\n    }\n}\n\nvoid MqttSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    if (_state.enabled)\n    {\n        ESP_LOGI(SVK_TAG, \"WiFi connection dropped, stopping MQTT client.\");\n        onConfigUpdated();\n    }\n}\n\nvoid MqttSettingsService::configureMqtt()\n{\n    disconnect();\n\n    // only connect if WiFi is connected and MQTT is enabled\n    if (_state.enabled && WiFi.isConnected())\n    {\n#ifdef SERIAL_INFO\n        Serial.println(\"Connecting to MQTT...\");\n#endif\n        _mqttClient.setServer(retainCstr(_state.uri.c_str(), &_retainedHost));\n        if (_state.username.length() > 0)\n        {\n            _mqttClient.setCredentials(\n                retainCstr(_state.username.c_str(), &_retainedUsername),\n                retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword));\n        }\n        else\n        {\n            _mqttClient.setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword));\n        }\n        _mqttClient.setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId));\n        _mqttClient.setKeepAlive(_state.keepAlive);\n        _mqttClient.setWill(_retainedWillTopic, 1, true, _retainedWillPayload);\n        _mqttClient.setCleanSession(_state.cleanSession);\n        _mqttClient.connect();\n\n        MqttCommitHandler::setTimerInterval(_state.messageIntervalMs);\n    }\n}\n\nvoid MqttSettingsService::setStatusTopic(String statusTopic)\n{\n    // check if the status topic is different from the current one\n    if (statusTopic.equals(_retainedWillTopic))\n    {\n        return; // no change\n    }\n\n    // copy the new status topic to the retained pointer\n    retainCstr(statusTopic.c_str(), &_retainedWillTopic);\n\n    // update the state\n    _reconfigureMqtt = true; // mark for reconfiguration\n\n    ESP_LOGI(SVK_TAG, \"Status topic updated to: %s\", _retainedWillTopic);\n}\n\nString MqttSettingsService::getStatusTopic()\n{\n    return String(_retainedWillTopic);\n}\n\nvoid MqttSettingsService::disconnect()\n{\n    // disable MQTT message commit timer, if disconnected\n    MqttCommitHandler::setTimerInterval(0);\n\n    // disconnect if currently connected\n    if (_mqttClient.connected())\n    {\n        ESP_LOGI(SVK_TAG, \"Disconnecting from MQTT client.\");\n        _mqttClient.publish(_retainedWillTopic, 1, true, \"offline\", 0, false);\n        _mqttClient.disconnect();\n    }\n}\n"
  },
  {
    "path": "lib/framework/MqttSettingsService.h",
    "content": "#ifndef MqttSettingsService_h\n#define MqttSettingsService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <StatefulService.h>\n#include <HttpEndpoint.h>\n#include <FSPersistence.h>\n#include <PsychicMqttClient.h>\n#include <SettingValue.h>\n#include <WiFi.h>\n#include <MqttEndpoint.h>\n\n#ifndef FACTORY_MQTT_ENABLED\n#define FACTORY_MQTT_ENABLED false\n#endif\n\n#ifndef FACTORY_MQTT_HOST\n#define FACTORY_MQTT_HOST \"test.mosquitto.org\"\n#endif\n\n#ifndef FACTORY_MQTT_PORT\n#define FACTORY_MQTT_PORT 1883\n#endif\n\n#ifndef FACTORY_MQTT_USERNAME\n#define FACTORY_MQTT_USERNAME \"\"\n#endif\n\n#ifndef FACTORY_MQTT_PASSWORD\n#define FACTORY_MQTT_PASSWORD \"\"\n#endif\n\n#ifndef FACTORY_MQTT_CLIENT_ID\n#define FACTORY_MQTT_CLIENT_ID \"#{platform}-#{unique_id}\"\n#endif\n\n#ifndef FACTORY_MQTT_STATUS_TOPIC\n#define FACTORY_MQTT_STATUS_TOPIC \"#{platform}/#{unique_id}/status\"\n#endif\n\n#ifndef FACTORY_MQTT_KEEP_ALIVE\n#define FACTORY_MQTT_KEEP_ALIVE 16\n#endif\n\n#ifndef FACTORY_MQTT_CLEAN_SESSION\n#define FACTORY_MQTT_CLEAN_SESSION true\n#endif\n\n#ifndef FACTORY_MQTT_MAX_TOPIC_LENGTH\n#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128\n#endif\n\n#ifndef FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS\n#define FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS 500\n#endif\n\n#define MQTT_SETTINGS_FILE \"/config/mqttSettings.json\"\n#define MQTT_SETTINGS_SERVICE_PATH \"/rest/mqttSettings\"\n\n#define MQTT_RECONNECTION_DELAY 5000\n\nclass MqttSettings\n{\npublic:\n    // host and port - if enabled\n    bool enabled;\n    String uri;\n\n    // username and password\n    String username;\n    String password;\n\n    // client id settings\n    String clientId;\n\n    // connection settings\n    uint16_t keepAlive;\n    bool cleanSession;\n\n    // Publish rate limiting\n    uint32_t messageIntervalMs;\n\n    static void\n    read(MqttSettings &settings, JsonObject &root)\n    {\n        root[\"enabled\"] = settings.enabled;\n        root[\"uri\"] = settings.uri;\n        root[\"username\"] = settings.username;\n        root[\"password\"] = settings.password;\n        root[\"client_id\"] = settings.clientId;\n        root[\"keep_alive\"] = settings.keepAlive;\n        root[\"clean_session\"] = settings.cleanSession;\n        root[\"message_interval_ms\"] = settings.messageIntervalMs;\n    }\n\n    static StateUpdateResult update(JsonObject &root, MqttSettings &settings, const String &originId)\n    {\n        settings.enabled = root[\"enabled\"] | FACTORY_MQTT_ENABLED;\n        settings.uri = root[\"uri\"] | FACTORY_MQTT_URI;\n        settings.username = root[\"username\"] | SettingValue::format(FACTORY_MQTT_USERNAME);\n        settings.password = root[\"password\"] | FACTORY_MQTT_PASSWORD;\n        settings.clientId = root[\"client_id\"] | SettingValue::format(FACTORY_MQTT_CLIENT_ID);\n        settings.keepAlive = root[\"keep_alive\"] | FACTORY_MQTT_KEEP_ALIVE;\n        settings.cleanSession = root[\"clean_session\"] | FACTORY_MQTT_CLEAN_SESSION;\n        settings.messageIntervalMs = root[\"message_interval_ms\"] | FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS;\n        return StateUpdateResult::CHANGED;\n    }\n};\n\nclass MqttSettingsService : public StatefulService<MqttSettings>\n{\npublic:\n    MqttSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);\n    ~MqttSettingsService();\n\n    void begin();\n    void loop();\n    bool isEnabled();\n    bool isConnected();\n    const char *getClientId();\n    String getLastError();\n    void setStatusTopic(String statusTopic);\n    String getStatusTopic();\n    PsychicMqttClient *getMqttClient();\n    void disconnect();\n\nprotected:\n    void onConfigUpdated();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    HttpEndpoint<MqttSettings> _httpEndpoint;\n    FSPersistence<MqttSettings> _fsPersistence;\n\n    // Pointers to hold retained copies of the mqtt client connection strings.\n    // This is required as AsyncMqttClient holds references to the supplied connection strings.\n    char *_retainedHost;\n    char *_retainedClientId;\n    char *_retainedUsername;\n    char *_retainedPassword;\n    char *_retainedWillTopic = nullptr;\n    const char *_retainedWillPayload = \"offline\";\n\n    // variable to help manage connection\n    bool _reconfigureMqtt;\n    String _lastError;\n\n    // the MQTT client instance\n    PsychicMqttClient _mqttClient;\n\n    void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);\n    void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);\n\n    void onMqttConnect(bool sessionPresent);\n    void onMqttDisconnect(bool sessionPresent);\n    void onMqttError(esp_mqtt_error_codes_t error);\n    void configureMqtt();\n};\n\n#endif // end MqttSettingsService_h\n"
  },
  {
    "path": "lib/framework/MqttStatus.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <MqttStatus.h>\n\nMqttStatus::MqttStatus(PsychicHttpServer *server,\n                       MqttSettingsService *mqttSettingsService,\n                       SecurityManager *securityManager) : _server(server),\n                                                           _securityManager(securityManager),\n                                                           _mqttSettingsService(mqttSettingsService)\n{\n}\n\nvoid MqttStatus::begin()\n{\n    _server->on(MQTT_STATUS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", MQTT_STATUS_SERVICE_PATH);\n}\n\nesp_err_t MqttStatus::mqttStatus(PsychicRequest *request)\n{\n    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n    JsonObject root = response.getRoot();\n\n    root[\"enabled\"] = _mqttSettingsService->isEnabled();\n    root[\"connected\"] = _mqttSettingsService->isConnected();\n    root[\"client_id\"] = _mqttSettingsService->getClientId();\n    root[\"last_error\"] = _mqttSettingsService->getLastError();\n\n    return response.send();\n}\n\nbool MqttStatus::isConnected()\n{\n    return _mqttSettingsService->isConnected();\n}\n"
  },
  {
    "path": "lib/framework/MqttStatus.h",
    "content": "#ifndef MqttStatus_h\n#define MqttStatus_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <MqttSettingsService.h>\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n\n#define MQTT_STATUS_SERVICE_PATH \"/rest/mqttStatus\"\n\nclass MqttStatus\n{\npublic:\n    MqttStatus(PsychicHttpServer *server, MqttSettingsService *mqttSettingsService, SecurityManager *securityManager);\n\n    void begin();\n\n    bool isConnected();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    MqttSettingsService *_mqttSettingsService;\n\n    esp_err_t mqttStatus(PsychicRequest *request);\n};\n\n#endif // end MqttStatus_h\n"
  },
  {
    "path": "lib/framework/NTPSettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <NTPSettingsService.h>\n#if FT_ENABLED(FT_ETHERNET)\n#include <ETH.h>\n#endif\n\nNTPSettingsService::NTPSettingsService(PsychicHttpServer *server,\n                                       FS *fs,\n                                       SecurityManager *securityManager) : _server(server),\n                                                                           _securityManager(securityManager),\n                                                                           _httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager),\n                                                                           _fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE)\n{\n    addUpdateHandler([&](const String &originId)\n                     { configureNTP(); },\n                     false);\n}\n\nvoid NTPSettingsService::begin()\n{\n    WiFi.onEvent(\n        std::bind(&NTPSettingsService::onNetworkDisconnected, this, std::placeholders::_1, std::placeholders::_2),\n        WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);\n    WiFi.onEvent(std::bind(&NTPSettingsService::onNetworkGotIP, this, std::placeholders::_1, std::placeholders::_2),\n                 WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);\n\n#if FT_ENABLED(FT_ETHERNET)\n    WiFi.onEvent(\n        std::bind(&NTPSettingsService::onNetworkDisconnected, this, std::placeholders::_1, std::placeholders::_2),\n        WiFiEvent_t::ARDUINO_EVENT_ETH_DISCONNECTED);\n    WiFi.onEvent(std::bind(&NTPSettingsService::onNetworkGotIP, this, std::placeholders::_1, std::placeholders::_2),\n                 WiFiEvent_t::ARDUINO_EVENT_ETH_GOT_IP);\n#endif\n\n    _httpEndpoint.begin();\n    _server->on(TIME_PATH,\n                HTTP_POST,\n                _securityManager->wrapCallback(\n                    std::bind(&NTPSettingsService::configureTime, this, std::placeholders::_1, std::placeholders::_2),\n                    AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", TIME_PATH);\n\n    _fsPersistence.readFromFS();\n    configureNTP();\n}\n\nvoid NTPSettingsService::onNetworkGotIP(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n#ifdef SERIAL_INFO\n    Serial.println(F(\"Got IP address, starting NTP Synchronization\"));\n#endif\n    configureNTP();\n}\n\nvoid NTPSettingsService::onNetworkDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n#ifdef SERIAL_INFO\n    Serial.println(F(\"Network connection dropped, stopping NTP.\"));\n#endif\n    configureNTP();\n}\n\nvoid NTPSettingsService::configureNTP()\n{\n    bool networkConnected = WiFi.isConnected();\n#if FT_ENABLED(FT_ETHERNET)\n    networkConnected = networkConnected || ETH.connected();\n#endif\n    if (networkConnected && _state.enabled)\n    {\n#ifdef SERIAL_INFO\n        Serial.println(F(\"Starting NTP...\"));\n#endif\n        configTzTime(_state.tzFormat.c_str(), _state.server.c_str());\n    }\n    else\n    {\n\n#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING\n        if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER))\n        {\n            LOCK_TCPIP_CORE();\n        }\n#endif\n\n        setenv(\"TZ\", _state.tzFormat.c_str(), 1);\n        tzset();\n        sntp_stop();\n\n#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING\n        if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER))\n        {\n            UNLOCK_TCPIP_CORE();\n        }\n#endif\n    }\n}\n\nesp_err_t NTPSettingsService::configureTime(PsychicRequest *request, JsonVariant &json)\n{\n    if (!sntp_enabled() && json.is<JsonObject>())\n    {\n        struct tm tm = {0};\n        String timeLocal = json[\"local_time\"];\n        char *s = strptime(timeLocal.c_str(), \"%Y-%m-%dT%H:%M:%S\", &tm);\n        if (s != nullptr)\n        {\n            time_t time = mktime(&tm);\n            struct timeval now = {.tv_sec = time};\n            settimeofday(&now, nullptr);\n            return request->reply(200);\n        }\n    }\n    return request->reply(400);\n}\n"
  },
  {
    "path": "lib/framework/NTPSettingsService.h",
    "content": "#ifndef NTPSettingsService_h\n#define NTPSettingsService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <HttpEndpoint.h>\n#include <FSPersistence.h>\n#include <WiFi.h>\n\n#include <time.h>\n#include <lwip/apps/sntp.h>\n\n#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING\n#include \"lwip/priv/tcpip_priv.h\"\n#endif\n\n#ifndef FACTORY_NTP_ENABLED\n#define FACTORY_NTP_ENABLED true\n#endif\n\n#ifndef FACTORY_NTP_TIME_ZONE_LABEL\n#define FACTORY_NTP_TIME_ZONE_LABEL \"Europe/London\"\n#endif\n\n#ifndef FACTORY_NTP_TIME_ZONE_FORMAT\n#define FACTORY_NTP_TIME_ZONE_FORMAT \"GMT0BST,M3.5.0/1,M10.5.0\"\n#endif\n\n#ifndef FACTORY_NTP_SERVER\n#define FACTORY_NTP_SERVER \"time.google.com\"\n#endif\n\n#define NTP_SETTINGS_FILE \"/config/ntpSettings.json\"\n#define NTP_SETTINGS_SERVICE_PATH \"/rest/ntpSettings\"\n\n#define TIME_PATH \"/rest/time\"\n\nclass NTPSettings\n{\npublic:\n    bool enabled;\n    String tzLabel;\n    String tzFormat;\n    String server;\n\n    static void read(NTPSettings &settings, JsonObject &root)\n    {\n        root[\"enabled\"] = settings.enabled;\n        root[\"server\"] = settings.server;\n        root[\"tz_label\"] = settings.tzLabel;\n        root[\"tz_format\"] = settings.tzFormat;\n    }\n\n    static StateUpdateResult update(JsonObject &root, NTPSettings &settings, const String &originId)\n    {\n        settings.enabled = root[\"enabled\"] | FACTORY_NTP_ENABLED;\n        settings.server = root[\"server\"] | FACTORY_NTP_SERVER;\n        settings.tzLabel = root[\"tz_label\"] | FACTORY_NTP_TIME_ZONE_LABEL;\n        settings.tzFormat = root[\"tz_format\"] | FACTORY_NTP_TIME_ZONE_FORMAT;\n        return StateUpdateResult::CHANGED;\n    }\n};\n\nclass NTPSettingsService : public StatefulService<NTPSettings>\n{\npublic:\n    NTPSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);\n\n    void begin();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    HttpEndpoint<NTPSettings> _httpEndpoint;\n    FSPersistence<NTPSettings> _fsPersistence;\n\n    void onNetworkGotIP(WiFiEvent_t event, WiFiEventInfo_t info);\n    void onNetworkDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);\n    void configureNTP();\n    esp_err_t configureTime(PsychicRequest *request, JsonVariant &json);\n};\n\n#endif // end NTPSettingsService_h\n"
  },
  {
    "path": "lib/framework/NTPStatus.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <NTPStatus.h>\n\nNTPStatus::NTPStatus(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server),\n                                                                                    _securityManager(securityManager)\n{\n}\n\nvoid NTPStatus::begin()\n{\n    _server->on(NTP_STATUS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", NTP_STATUS_SERVICE_PATH);\n}\n\n/*\n * Formats the time using the format provided.\n *\n * Uses a 25 byte buffer, large enough to fit an ISO time string with offset.\n */\nString formatTime(tm *time, const char *format)\n{\n    char time_string[25];\n    strftime(time_string, 25, format, time);\n    return String(time_string);\n}\n\nString toUTCTimeString(tm *time)\n{\n    return formatTime(time, \"%FT%TZ\");\n}\n\nString toLocalTimeString(tm *time)\n{\n    return formatTime(time, \"%FT%T\");\n}\n\nesp_err_t NTPStatus::ntpStatus(PsychicRequest *request)\n{\n    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n    JsonObject root = response.getRoot();\n\n    // grab the current instant in unix seconds\n    time_t now = time(nullptr);\n\n    // only provide enabled/disabled status for now\n    root[\"status\"] = sntp_enabled() ? 1 : 0;\n\n    // the current time in UTC\n    root[\"utc_time\"] = toUTCTimeString(gmtime(&now));\n\n    // local time with offset\n    root[\"local_time\"] = toLocalTimeString(localtime(&now));\n\n    // the sntp server name\n    root[\"server\"] = sntp_getservername(0);\n\n    // device uptime in seconds\n    root[\"uptime\"] = millis() / 1000;\n\n    return response.send();\n}\n"
  },
  {
    "path": "lib/framework/NTPStatus.h",
    "content": "#ifndef NTPStatus_h\n#define NTPStatus_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <time.h>\n#include <WiFi.h>\n#include <lwip/apps/sntp.h>\n\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n\n#define NTP_STATUS_SERVICE_PATH \"/rest/ntpStatus\"\n\nclass NTPStatus\n{\npublic:\n    NTPStatus(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    esp_err_t ntpStatus(PsychicRequest *request);\n};\n\n#endif // end NTPStatus_h\n"
  },
  {
    "path": "lib/framework/NotificationService.cpp",
    "content": "#include <NotificationService.h>\n\n// array translating pushType into strings\nconst char *pushTypeStrings[] = {\"error\", \"warning\", \"info\", \"success\"};\n\nNotificationService::NotificationService(EventSocket *eventSocket) : _eventSocket(eventSocket)\n{\n}\n\nvoid NotificationService::begin()\n{\n    _eventSocket->registerEvent(NOTIFICATION_EVENT);\n}\n\nvoid NotificationService::pushNotification(String message, pushType event)\n{\n    JsonDocument doc;\n    doc[\"type\"] = pushTypeStrings[event];\n    doc[\"message\"] = message;\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _eventSocket->emitEvent(NOTIFICATION_EVENT, jsonObject);\n}\n"
  },
  {
    "path": "lib/framework/NotificationService.h",
    "content": "#ifndef NotificationService_h\n#define NotificationService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <EventSocket.h>\n\n#define NOTIFICATION_EVENT \"notification\"\n\nenum pushType\n{\n    PUSHERROR,\n    PUSHWARNING,\n    PUSHINFO,\n    PUSHSUCCESS\n};\n\nclass NotificationService\n{\npublic:\n    NotificationService(EventSocket *eventSocket);\n\n    void begin();\n\n    void pushNotification(String message, pushType event);\n\nprivate:\n    EventSocket *_eventSocket;\n};\n\n#endif // NotificationService_h"
  },
  {
    "path": "lib/framework/RestartService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <RestartService.h>\n\nRestartService::RestartService(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server),\n                                                                                              _securityManager(securityManager)\n{\n}\n\nvoid RestartService::begin()\n{\n    _server->on(RESTART_SERVICE_PATH,\n                HTTP_POST,\n                _securityManager->wrapRequest(std::bind(&RestartService::restart, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", RESTART_SERVICE_PATH);\n}\n\nesp_err_t RestartService::restart(PsychicRequest *request)\n{\n    request->reply(200);\n    restartNow();\n    return ESP_OK;\n}\n"
  },
  {
    "path": "lib/framework/RestartService.h",
    "content": "#ifndef RestartService_h\n#define RestartService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <ESPmDNS.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n\n#define RESTART_SERVICE_PATH \"/rest/restart\"\n\nclass RestartService\n{\npublic:\n    RestartService(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\n    static void restartNow()\n    {\n        delay(250);\n        MDNS.end();\n        delay(100);\n        WiFi.disconnect(true);\n        delay(200);\n        ESP.restart();\n    }\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    esp_err_t restart(PsychicRequest *request);\n};\n\n#endif // end RestartService_h\n"
  },
  {
    "path": "lib/framework/SecurityManager.h",
    "content": "#ifndef SecurityManager_h\n#define SecurityManager_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Features.h>\n#include <ArduinoJsonJWT.h>\n#include <PsychicHttp.h>\n#include <list>\n\n#define SVK_TAG \"🐼\"\n\n#define ACCESS_TOKEN_PARAMATER \"access_token\"\n\n#define AUTHORIZATION_HEADER \"Authorization\"\n#define AUTHORIZATION_HEADER_PREFIX \"Bearer \"\n#define AUTHORIZATION_HEADER_PREFIX_LEN 7\n\nclass User\n{\npublic:\n    String username;\n    String password;\n    bool admin;\n\npublic:\n    User(String username, String password, bool admin) : username(username), password(password), admin(admin)\n    {\n    }\n};\n\nclass Authentication\n{\npublic:\n    User *user;\n    boolean authenticated;\n\npublic:\n    Authentication(User &user) : user(new User(user)), authenticated(true)\n    {\n    }\n    Authentication() : user(nullptr), authenticated(false)\n    {\n    }\n    ~Authentication()\n    {\n        delete (user);\n    }\n};\n\ntypedef std::function<boolean(Authentication &authentication)> AuthenticationPredicate;\n\nclass AuthenticationPredicates\n{\npublic:\n    static bool NONE_REQUIRED(Authentication &authentication)\n    {\n        return true;\n    };\n    static bool IS_AUTHENTICATED(Authentication &authentication)\n    {\n        return authentication.authenticated;\n    };\n    static bool IS_ADMIN(Authentication &authentication)\n    {\n        return authentication.authenticated && authentication.user->admin;\n    };\n};\n\nclass SecurityManager\n{\npublic:\n#if FT_ENABLED(FT_SECURITY)\n    /*\n     * Authenticate, returning the user if found\n     */\n    virtual Authentication authenticate(const String &username, const String &password) = 0;\n\n    /*\n     * Generate a JWT for the user provided\n     */\n    virtual String generateJWT(User *user) = 0;\n\n#endif\n\n    /*\n     * Check the request header for the Authorization token\n     */\n    virtual Authentication authenticateRequest(PsychicRequest *request) = 0;\n\n    /**\n     * Filter a request with the provided predicate, only returning true if the predicate matches.\n     */\n    virtual PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0;\n\n    /**\n     * Wrap the provided request to provide validation against an AuthenticationPredicate.\n     */\n    virtual PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate) = 0;\n\n    /**\n     * Wrap the provided json request callback to provide validation against an AuthenticationPredicate.\n     */\n    virtual PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate) = 0;\n};\n\n#endif // end SecurityManager_h\n"
  },
  {
    "path": "lib/framework/SecuritySettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <SecuritySettingsService.h>\n\n#if FT_ENABLED(FT_SECURITY)\n\nSecuritySettingsService::SecuritySettingsService(PsychicHttpServer *server, FS *fs) : _server(server),\n                                                                                      _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this),\n                                                                                      _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE),\n                                                                                      _jwtHandler(FACTORY_JWT_SECRET)\n{\n    addUpdateHandler([&](const String &originId)\n                     { configureJWTHandler(); },\n                     false);\n}\n\nvoid SecuritySettingsService::begin()\n{\n    _server->on(GENERATE_TOKEN_PATH,\n                HTTP_GET,\n                wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1),\n                            AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", GENERATE_TOKEN_PATH);\n\n    _httpEndpoint.begin();\n    _fsPersistence.readFromFS();\n    configureJWTHandler();\n}\n\nAuthentication SecuritySettingsService::authenticateRequest(PsychicRequest *request)\n{\n    // Load the parameters from the request, as they are only loaded later with the regular handler\n    if (request->hasHeader(AUTHORIZATION_HEADER))\n    {\n        auto value = request->header(AUTHORIZATION_HEADER);\n        // ESP_LOGV(SVK_TAG, \"Authorization header: %s\", value.c_str());\n        if (value.startsWith(AUTHORIZATION_HEADER_PREFIX))\n        {\n            value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN);\n            return authenticateJWT(value);\n        }\n    }\n    else if (request->hasParam(ACCESS_TOKEN_PARAMATER))\n    {\n        String value = request->getParam(ACCESS_TOKEN_PARAMATER)->value();\n        // ESP_LOGV(SVK_TAG, \"Access token parameter: %s\", value.c_str());\n        return authenticateJWT(value);\n    }\n    return Authentication();\n}\n\nvoid SecuritySettingsService::configureJWTHandler()\n{\n    _jwtHandler.setSecret(_state.jwtSecret);\n}\n\nAuthentication SecuritySettingsService::authenticateJWT(String &jwt)\n{\n    JsonDocument payloadDocument;\n    _jwtHandler.parseJWT(jwt, payloadDocument);\n    if (payloadDocument.is<JsonObject>())\n    {\n        JsonObject parsedPayload = payloadDocument.as<JsonObject>();\n        String username = parsedPayload[\"username\"];\n        for (User _user : _state.users)\n        {\n            if (_user.username == username && validatePayload(parsedPayload, &_user))\n            {\n                return Authentication(_user);\n            }\n        }\n    }\n    return Authentication();\n}\n\nAuthentication SecuritySettingsService::authenticate(const String &username, const String &password)\n{\n    for (User _user : _state.users)\n    {\n        if (_user.username == username && _user.password == password)\n        {\n            return Authentication(_user);\n        }\n    }\n    return Authentication();\n}\n\ninline void populateJWTPayload(JsonObject &payload, User *user)\n{\n    payload[\"username\"] = user->username;\n    payload[\"admin\"] = user->admin;\n}\n\nboolean SecuritySettingsService::validatePayload(JsonObject &parsedPayload, User *user)\n{\n    JsonDocument jsonDocument;\n    JsonObject payload = jsonDocument.to<JsonObject>();\n    populateJWTPayload(payload, user);\n    return payload == parsedPayload;\n}\n\nString SecuritySettingsService::generateJWT(User *user)\n{\n    JsonDocument jsonDocument;\n    JsonObject payload = jsonDocument.to<JsonObject>();\n    populateJWTPayload(payload, user);\n    return _jwtHandler.buildJWT(payload);\n}\n\nPsychicRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate)\n{\n    return [this, predicate](PsychicRequest *request)\n    {\n        // ESP_LOGV(SVK_TAG, \"Authenticating filter request: %s\", request->uri().c_str());\n        // ESP_LOGV(SVK_TAG, \"Request Method: %s\", request->methodStr().c_str());\n\n        // TODO: This is a hack to allow bogus websocket filter requests to pass through\n        // This is a temporary fix until the PsychicHttp websocket handler is fixed to not send a bogus filter request\n\n        // Check if we have a bogus filter request and return true\n        if (request->uri().isEmpty() && request->method() == HTTP_DELETE)\n        {\n            // ESP_LOGV(SVK_TAG, \"Bogus filter request - allowing\");\n            return true;\n        }\n        else\n            request->loadParams();\n\n        Authentication authentication = authenticateRequest(request);\n        bool result = predicate(authentication);\n        // ESP_LOGV(SVK_TAG, \"Filter Request %s\", result ? \"allowed\" : \"denied\");\n        return result;\n    };\n}\n\nPsychicHttpRequestCallback SecuritySettingsService::wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate)\n{\n    return [this, onRequest, predicate](PsychicRequest *request)\n    {\n        Authentication authentication = authenticateRequest(request);\n        if (!predicate(authentication))\n        {\n            return request->reply(401);\n        }\n        return onRequest(request);\n    };\n}\n\nPsychicJsonRequestCallback SecuritySettingsService::wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate)\n{\n    return [this, onRequest, predicate](PsychicRequest *request, JsonVariant &json)\n    {\n        Authentication authentication = authenticateRequest(request);\n        if (!predicate(authentication))\n        {\n            return request->reply(401);\n        }\n        return onRequest(request, json);\n    };\n}\n\nesp_err_t SecuritySettingsService::generateToken(PsychicRequest *request)\n{\n    String usernameParam = request->getParam(\"username\")->value();\n    for (User _user : _state.users)\n    {\n        if (_user.username == usernameParam)\n        {\n            PsychicJsonResponse response = PsychicJsonResponse(request, false);\n            JsonObject root = response.getRoot();\n            root[\"token\"] = generateJWT(&_user);\n            return response.send();\n        }\n    }\n    return request->reply(401);\n}\n\n#else\n\nUser ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);\n\nSecuritySettingsService::SecuritySettingsService(PsychicHttpServer *server, FS *fs) : SecurityManager()\n{\n}\nSecuritySettingsService::~SecuritySettingsService()\n{\n}\n\nPsychicRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate)\n{\n    return [this, predicate](PsychicRequest *request)\n    {\n        // ESP_LOGV(SVK_TAG, \"Security disabled - all requests are allowed\");\n        return true;\n    };\n}\n\n// Return the admin user on all request - disabling security features\nAuthentication SecuritySettingsService::authenticateRequest(PsychicRequest *request)\n{\n    return Authentication(ADMIN_USER);\n}\n\n// Return the function unwrapped\nPsychicHttpRequestCallback SecuritySettingsService::wrapRequest(PsychicHttpRequestCallback onRequest,\n                                                                AuthenticationPredicate predicate)\n{\n    return onRequest;\n}\n\nPsychicJsonRequestCallback SecuritySettingsService::wrapCallback(PsychicJsonRequestCallback onRequest,\n                                                                 AuthenticationPredicate predicate)\n{\n    return onRequest;\n}\n\n#endif\n"
  },
  {
    "path": "lib/framework/SecuritySettingsService.h",
    "content": "#ifndef SecuritySettingsService_h\n#define SecuritySettingsService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <SettingValue.h>\n#include <Features.h>\n#include <SecurityManager.h>\n#include <HttpEndpoint.h>\n#include <FSPersistence.h>\n\n#ifndef FACTORY_JWT_SECRET\n#define FACTORY_JWT_SECRET \"#{random}-#{random}\"\n#endif\n\n#ifndef FACTORY_ADMIN_USERNAME\n#define FACTORY_ADMIN_USERNAME \"admin\"\n#endif\n\n#ifndef FACTORY_ADMIN_PASSWORD\n#define FACTORY_ADMIN_PASSWORD \"admin\"\n#endif\n\n#ifndef FACTORY_GUEST_USERNAME\n#define FACTORY_GUEST_USERNAME \"guest\"\n#endif\n\n#ifndef FACTORY_GUEST_PASSWORD\n#define FACTORY_GUEST_PASSWORD \"guest\"\n#endif\n\n#define SECURITY_SETTINGS_FILE \"/config/securitySettings.json\"\n#define SECURITY_SETTINGS_PATH \"/rest/securitySettings\"\n\n#define GENERATE_TOKEN_PATH \"/rest/generateToken\"\n\n#if FT_ENABLED(FT_SECURITY)\n\nclass SecuritySettings\n{\npublic:\n    String jwtSecret;\n    std::list<User> users;\n\n    static void read(SecuritySettings &settings, JsonObject &root)\n    {\n        // secret\n        root[\"jwt_secret\"] = settings.jwtSecret;\n\n        // users\n        JsonArray users = root[\"users\"].to<JsonArray>();\n        for (User user : settings.users)\n        {\n            JsonObject userRoot = users.add<JsonObject>();\n            userRoot[\"username\"] = user.username;\n            userRoot[\"password\"] = user.password;\n            userRoot[\"admin\"] = user.admin;\n        }\n    }\n\n    static StateUpdateResult update(JsonObject &root, SecuritySettings &settings, const String& originID)\n    {\n        // secret\n        settings.jwtSecret = root[\"jwt_secret\"] | SettingValue::format(FACTORY_JWT_SECRET);\n\n        // users\n        settings.users.clear();\n        if (root[\"users\"].is<JsonArray>())\n        {\n            for (JsonVariant user : root[\"users\"].as<JsonArray>())\n            {\n                settings.users.push_back(User(user[\"username\"], user[\"password\"], user[\"admin\"]));\n            }\n        }\n        else\n        {\n            settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true));\n            settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false));\n        }\n        return StateUpdateResult::CHANGED;\n    }\n};\n\nclass SecuritySettingsService : public StatefulService<SecuritySettings>, public SecurityManager\n{\npublic:\n    SecuritySettingsService(PsychicHttpServer *server, FS *fs);\n\n    void begin();\n\n    // Functions to implement SecurityManager\n    Authentication authenticate(const String &username, const String &password);\n    Authentication authenticateRequest(PsychicRequest *request);\n    String generateJWT(User *user);\n\n    PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate);\n    PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate);\n    PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate);\n\nprivate:\n    PsychicHttpServer *_server;\n\n    HttpEndpoint<SecuritySettings> _httpEndpoint;\n    FSPersistence<SecuritySettings> _fsPersistence;\n    ArduinoJsonJWT _jwtHandler;\n\n    esp_err_t generateToken(PsychicRequest *request);\n\n    void configureJWTHandler();\n\n    /*\n     * Lookup the user by JWT\n     */\n    Authentication authenticateJWT(String &jwt);\n\n    /*\n     * Verify the payload is correct\n     */\n    boolean validatePayload(JsonObject &parsedPayload, User *user);\n};\n\n#else\n\nclass SecuritySettingsService : public SecurityManager\n{\npublic:\n    SecuritySettingsService(PsychicHttpServer *server, FS *fs);\n    ~SecuritySettingsService();\n\n    // minimal set of functions to support framework with security settings disabled\n    Authentication authenticateRequest(PsychicRequest *request);\n    PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate);\n    PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate);\n    PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate);\n};\n\n#endif // end FT_ENABLED(FT_SECURITY)\n#endif // end SecuritySettingsService_h\n"
  },
  {
    "path": "lib/framework/SettingValue.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <SettingValue.h>\n\nnamespace SettingValue\n{\n    const String PLATFORM = \"esp32\";\n\n    /**\n     * Returns a new string after replacing each instance of the pattern with a value generated by calling the provided\n     * callback.\n     */\n    String replaceEach(String value, String pattern, String (*generateReplacement)())\n    {\n        while (true)\n        {\n            int index = value.indexOf(pattern);\n            if (index == -1)\n            {\n                break;\n            }\n            value = value.substring(0, index) + generateReplacement() + value.substring(index + pattern.length());\n        }\n        return value;\n    }\n\n    /**\n     * Generates a random number, encoded as a hex string.\n     */\n    String getRandom()\n    {\n        return String(random(2147483647), HEX);\n    }\n\n    /**\n     * Uses the station's MAC address to create a unique id for each device.\n     */\n    String getUniqueId()\n    {\n        uint8_t mac[6];\n        esp_read_mac(mac, ESP_MAC_WIFI_STA);\n        char macStr[13] = {0};\n        sprintf(macStr, \"%02x%02x%02x%02x%02x%02x\", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);\n        return String(macStr);\n    }\n\n    String format(String value)\n    {\n        value = replaceEach(value, \"#{random}\", getRandom);\n        value.replace(\"#{unique_id}\", getUniqueId());\n        value.replace(\"#{platform}\", PLATFORM);\n        return value;\n    }\n\n}; // end namespace SettingValue\n"
  },
  {
    "path": "lib/framework/SettingValue.h",
    "content": "#ifndef SettingValue_h\n#define SettingValue_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n\n#if ESP_ARDUINO_VERSION_MAJOR == 3\n#include <esp_mac.h>\n#endif\n\nnamespace SettingValue\n{\n    String format(String value);\n};\n\n#endif // end SettingValue\n"
  },
  {
    "path": "lib/framework/SleepService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <SleepService.h>\n\n// Definition of static member variables\nstd::vector<sleepCallback> SleepService::_sleepCallbacks;\nu_int64_t _wakeUpPin = WAKEUP_PIN_NUMBER;\nbool _wakeUpSignal = WAKEUP_SIGNAL;\npinTermination _wakeUpTermination = pinTermination::FLOATING;\n\nSleepService::SleepService(PsychicHttpServer *server,\n                           SecurityManager *securityManager) : _server(server),\n                                                               _securityManager(securityManager)\n{\n}\n\nvoid SleepService::begin()\n{\n// OPTIONS (for CORS preflight)\n#ifdef ENABLE_CORS\n    _server->on(SLEEP_SERVICE_PATH,\n                HTTP_OPTIONS,\n                _securityManager->wrapRequest(\n                    [this](PsychicRequest *request)\n                    {\n                        return request->reply(200);\n                    },\n                    AuthenticationPredicates::IS_AUTHENTICATED));\n#endif\n\n    _server->on(SLEEP_SERVICE_PATH,\n                HTTP_POST,\n                _securityManager->wrapRequest(std::bind(&SleepService::sleep, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", SLEEP_SERVICE_PATH);\n}\n\nesp_err_t SleepService::sleep(PsychicRequest *request)\n{\n    request->reply(200);\n    sleepNow();\n\n    return ESP_OK;\n}\n\nvoid SleepService::sleepNow()\n{\n#ifdef SERIAL_INFO\n    Serial.println(\"Going into deep sleep now\");\n#endif\n    ESP_LOGI(SVK_TAG, \"Going into deep sleep now\");\n\n    // Callback for main code sleep preparation\n    for (auto callback : _sleepCallbacks)\n    {\n        callback();\n    }\n\n    MDNS.end();\n    delay(100);\n    WiFi.disconnect(true);\n    delay(200);\n\n    // set pin function of _wakeUpPin\n    pinMode(_wakeUpPin, INPUT);\n\n    ESP_LOGD(SVK_TAG, \"Enabling GPIO wakeup on pin GPIO%d with level %d\\n\", _wakeUpPin, _wakeUpSignal);\n    ESP_LOGD(SVK_TAG, \"Current level on GPIO%d: %d\\n\", _wakeUpPin, digitalRead(_wakeUpPin));\n\n// special treatment for ESP32-C3 / C6 because of the RISC-V architecture\n#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)\n    esp_deep_sleep_enable_gpio_wakeup(BIT(_wakeUpPin), (esp_deepsleep_gpio_wake_up_mode_t)_wakeUpSignal);\n#else\n    esp_sleep_enable_ext1_wakeup(BIT(_wakeUpPin), (esp_sleep_ext1_wakeup_mode_t)_wakeUpSignal);\n\n    switch (_wakeUpTermination)\n    {\n    case pinTermination::PULL_DOWN:\n        esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);\n        rtc_gpio_init((gpio_num_t)_wakeUpPin);\n        rtc_gpio_pullup_dis((gpio_num_t)_wakeUpPin);\n        rtc_gpio_pulldown_en((gpio_num_t)_wakeUpPin);\n        break;\n    case pinTermination::PULL_UP:\n        esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);\n        rtc_gpio_init((gpio_num_t)_wakeUpPin);\n        rtc_gpio_pullup_en((gpio_num_t)_wakeUpPin);\n        rtc_gpio_pulldown_dis((gpio_num_t)_wakeUpPin);\n        break;\n    default:\n        esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_AUTO);\n    }\n#endif\n\n#ifdef SERIAL_INFO\n    Serial.println(\"Good by!\");\n#endif\n\n    esp_deep_sleep_start();\n}\n\nvoid SleepService::setWakeUpPin(int pin, bool level, pinTermination termination)\n{\n    _wakeUpPin = (u_int64_t)pin;\n    _wakeUpSignal = level;\n    _wakeUpTermination = termination;\n}\n"
  },
  {
    "path": "lib/framework/SleepService.h",
    "content": "#pragma once\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n#include <ESPmDNS.h>\n\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include \"driver/rtc_io.h\"\n#include <vector>\n\n#define SLEEP_SERVICE_PATH \"/rest/sleep\"\n\n#ifndef WAKEUP_PIN_NUMBER\n#define WAKEUP_PIN_NUMBER 0\n#endif\n\n#ifndef WAKEUP_SIGNAL\n#define WAKEUP_SIGNAL 0\n#endif\n\nenum class pinTermination\n{\n    FLOATING,\n    PULL_UP,\n    PULL_DOWN\n};\n\n// typdef for sleep service callback\ntypedef std::function<void()> sleepCallback;\n\nclass SleepService\n{\npublic:\n    SleepService(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\n    static void sleepNow();\n\n    void attachOnSleepCallback(sleepCallback callbackSleep)\n    {\n        _sleepCallbacks.push_back(callbackSleep);\n    }\n\n    void setWakeUpPin(int pin, bool level, pinTermination termination = pinTermination::FLOATING);\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    esp_err_t sleep(PsychicRequest *request);\n\nprotected:\n    static std::vector<sleepCallback> _sleepCallbacks;\n};\n"
  },
  {
    "path": "lib/framework/StatefulService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <StatefulService.h>\n\nupdate_handler_id_t StateUpdateHandlerInfo::currentUpdatedHandlerId = 0;\nhook_handler_id_t StateHookHandlerInfo::currentHookHandlerId = 0;\n"
  },
  {
    "path": "lib/framework/StatefulService.h",
    "content": "#ifndef StatefulService_h\n#define StatefulService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n\n#include <list>\n#include <functional>\n#include <freertos/FreeRTOS.h>\n#include <freertos/semphr.h>\n\nenum class StateUpdateResult\n{\n    CHANGED = 0, // The update changed the state and propagation should take place if required\n    UNCHANGED,   // The state was unchanged, propagation should not take place\n    ERROR        // There was a problem updating the state, propagation should not take place\n};\n\ntemplate <typename T>\nusing JsonStateUpdater = std::function<StateUpdateResult(JsonObject &root, T &settings, const String &originId)>;\n\ntemplate <typename T>\nusing JsonStateReader = std::function<void(T &settings, JsonObject &root)>;\n\ntypedef size_t update_handler_id_t;\ntypedef size_t hook_handler_id_t;\ntypedef std::function<void(const String &originId)> StateUpdateCallback;\ntypedef std::function<void(const String &originId, StateUpdateResult &result)> StateHookCallback;\n\ntypedef struct StateUpdateHandlerInfo\n{\n    static update_handler_id_t currentUpdatedHandlerId;\n    update_handler_id_t _id;\n    StateUpdateCallback _cb;\n    bool _allowRemove;\n    StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) : _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove) {};\n} StateUpdateHandlerInfo_t;\n\ntypedef struct StateHookHandlerInfo\n{\n    static hook_handler_id_t currentHookHandlerId;\n    hook_handler_id_t _id;\n    StateHookCallback _cb;\n    bool _allowRemove;\n    StateHookHandlerInfo(StateHookCallback cb, bool allowRemove) : _id(++currentHookHandlerId), _cb(cb), _allowRemove(allowRemove) {};\n} StateHookHandlerInfo_t;\n\ntemplate <class T>\nclass StatefulService\n{\npublic:\n    template <typename... Args>\n    StatefulService(Args &&...args) : _state(std::forward<Args>(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex())\n    {\n    }\n\n    update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true)\n    {\n        if (!cb)\n        {\n            return 0;\n        }\n        StateUpdateHandlerInfo_t updateHandler(cb, allowRemove);\n        _updateHandlers.push_back(updateHandler);\n        return updateHandler._id;\n    }\n\n    void removeUpdateHandler(update_handler_id_t id)\n    {\n        for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();)\n        {\n            if ((*i)._allowRemove && (*i)._id == id)\n            {\n                i = _updateHandlers.erase(i);\n            }\n            else\n            {\n                ++i;\n            }\n        }\n    }\n\n    hook_handler_id_t addHookHandler(StateHookCallback cb, bool allowRemove = true)\n    {\n        if (!cb)\n        {\n            return 0;\n        }\n        StateHookHandlerInfo_t hookHandler(cb, allowRemove);\n        _hookHandlers.push_back(hookHandler);\n        return hookHandler._id;\n    }\n\n    void removeHookHandler(hook_handler_id_t id)\n    {\n        for (auto i = _hookHandlers.begin(); i != _hookHandlers.end();)\n        {\n            if ((*i)._allowRemove && (*i)._id == id)\n            {\n                i = _hookHandlers.erase(i);\n            }\n            else\n            {\n                ++i;\n            }\n        }\n    }\n\n    StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater, const String &originId)\n    {\n        beginTransaction();\n        StateUpdateResult result = stateUpdater(_state);\n        endTransaction();\n        callHookHandlers(originId, result);\n        if (result == StateUpdateResult::CHANGED)\n        {\n            callUpdateHandlers(originId);\n        }\n        return result;\n    }\n\n    StateUpdateResult updateWithoutPropagation(std::function<StateUpdateResult(T &)> stateUpdater, const String &originId)\n    {\n        beginTransaction();\n        StateUpdateResult result = stateUpdater(_state);\n        endTransaction();\n        return result;\n    }\n\n    StateUpdateResult update(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater, const String &originId)\n    {\n        beginTransaction();\n        StateUpdateResult result = stateUpdater(jsonObject, _state, originId);\n        endTransaction();\n        callHookHandlers(originId, result);\n        if (result == StateUpdateResult::CHANGED)\n        {\n            callUpdateHandlers(originId);\n        }\n        return result;\n    }\n\n    StateUpdateResult updateWithoutPropagation(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater, const String &originId)\n    {\n        beginTransaction();\n        StateUpdateResult result = stateUpdater(jsonObject, _state, originId);\n        endTransaction();\n        return result;\n    }\n\n    void read(std::function<void(T &)> stateReader)\n    {\n        beginTransaction();\n        stateReader(_state);\n        endTransaction();\n    }\n\n    void read(JsonObject &jsonObject, JsonStateReader<T> stateReader)\n    {\n        beginTransaction();\n        stateReader(_state, jsonObject);\n        endTransaction();\n    }\n\n    void callUpdateHandlers(const String &originId)\n    {\n        for (const StateUpdateHandlerInfo_t &updateHandler : _updateHandlers)\n        {\n            updateHandler._cb(originId);\n        }\n    }\n\n    void callHookHandlers(const String &originId, StateUpdateResult &result)\n    {\n        for (const StateHookHandlerInfo_t &hookHandler : _hookHandlers)\n        {\n            hookHandler._cb(originId, result);\n        }\n    }\n\nprotected:\n    T _state;\n\n    inline void beginTransaction()\n    {\n        xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);\n    }\n\n    inline void endTransaction()\n    {\n        xSemaphoreGiveRecursive(_accessMutex);\n    }\n\nprivate:\n    SemaphoreHandle_t _accessMutex;\n    std::list<StateUpdateHandlerInfo_t> _updateHandlers;\n    std::list<StateHookHandlerInfo_t> _hookHandlers;\n};\n\n#endif // end StatefulService_h\n"
  },
  {
    "path": "lib/framework/SystemStatus.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <SystemStatus.h>\n#include <esp32-hal.h>\n\n#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4\n#include \"esp32/rom/rtc.h\"\n#define ESP_TARGET \"ESP32\";\n#elif CONFIG_IDF_TARGET_ESP32S2\n#include \"esp32/rom/rtc.h\"\n#define ESP_TARGET \"ESP32-S2\";\n#elif CONFIG_IDF_TARGET_ESP32C3\n#include \"esp32c3/rom/rtc.h\"\n#define ESP_TARGET \"ESP32-C3\";\n#elif CONFIG_IDF_TARGET_ESP32S3\n#include \"esp32s3/rom/rtc.h\"\n#define ESP_TARGET \"ESP32-S3\";\n#elif CONFIG_IDF_TARGET_ESP32C6\n#include \"esp32c6/rom/rtc.h\"\n#define ESP_TARGET \"ESP32-C6\";\n#else\n#error Target CONFIG_IDF_TARGET is not supported\n#endif\n\n#ifndef ARDUINO_VERSION\n#ifndef STRINGIZE\n#define STRINGIZE(s) #s\n#endif\n#define ARDUINO_VERSION_STR(major, minor, patch) \"v\" STRINGIZE(major) \".\" STRINGIZE(minor) \".\" STRINGIZE(patch)\n#define ARDUINO_VERSION ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)\n#endif\n\nString verbosePrintResetReason(int reason)\n{\n    switch (reason)\n    {\n    case ESP_RST_UNKNOWN:\n        return (\"Reset reason can not be determined\");\n        break;\n    case ESP_RST_POWERON:\n        return (\"Reset due to power-on event\");\n        break;\n    case ESP_RST_EXT:\n        return (\"Reset by external pin (not applicable for ESP32)\");\n        break;\n    case ESP_RST_SW:\n        return (\"Software reset via esp_restart\");\n        break;\n    case ESP_RST_PANIC:\n        return (\"Software reset due to exception/panic\");\n        break;\n    case ESP_RST_INT_WDT:\n        return (\"Reset (software or hardware) due to interrupt watchdog\");\n        break;\n    case ESP_RST_TASK_WDT:\n        return (\"Reset due to task watchdog\");\n        break;\n    case ESP_RST_WDT:\n        return (\"Reset due to other watchdogs\");\n        break;\n    case ESP_RST_DEEPSLEEP:\n        return (\"Reset after exiting deep sleep mode\");\n        break;\n    case ESP_RST_BROWNOUT:\n        return (\"Brownout reset (software or hardware)\");\n        break;\n    case ESP_RST_SDIO:\n        return (\"Reset over SDIO\");\n        break;\n#ifdef ESP_RST_USB\n    case ESP_RST_USB:\n        return (\"Reset by USB peripheral\");\n        break;\n#endif\n#ifdef ESP_RST_JSVK_TAG\n    case ESP_RST_JSVK_TAG:\n        return (\"Reset by JSVK_TAG\");\n        break;\n#endif\n#ifdef ESP_RST_EFUSE\n    case ESP_RST_EFUSE:\n        return (\"Reset due to efuse error\");\n        break;\n#endif\n#ifdef ESP_RST_PWR_GLITCH\n    case ESP_RST_PWR_GLITCH:\n        return (\"Reset due to power glitch detected\");\n        break;\n#endif\n#ifdef ESP_RST_CPU_LOCKUP\n    case ESP_RST_CPU_LOCKUP:\n        return (\"Reset due to CPU lock up (double exception)\");\n        break;\n#endif\n    default:\n        char buffer[50];\n        snprintf(buffer, sizeof(buffer), \"Unknown reset reason (%d)\", reason);\n        return String(buffer);\n        break;\n    }\n}\n\nSystemStatus::SystemStatus(PsychicHttpServer *server,\n                           SecurityManager *securityManager) : _server(server),\n                                                               _securityManager(securityManager)\n{\n}\n\nvoid SystemStatus::begin()\n{\n    _server->on(SYSTEM_STATUS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", SYSTEM_STATUS_SERVICE_PATH);\n}\n\nesp_err_t SystemStatus::systemStatus(PsychicRequest *request)\n{\n    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n    JsonObject root = response.getRoot();\n\n    root[\"esp_platform\"] = ESP_TARGET;\n    root[\"firmware_version\"] = APP_VERSION;\n    root[\"max_alloc_heap\"] = ESP.getMaxAllocHeap();\n    if (psramFound())\n    {\n        root[\"free_psram\"] = ESP.getFreePsram();\n        root[\"used_psram\"] = ESP.getPsramSize() - ESP.getFreePsram();\n        root[\"psram_size\"] = ESP.getPsramSize();\n    }\n    root[\"cpu_freq_mhz\"] = ESP.getCpuFreqMHz();\n    root[\"cpu_type\"] = ESP.getChipModel();\n    root[\"cpu_rev\"] = ESP.getChipRevision();\n    root[\"cpu_cores\"] = ESP.getChipCores();\n    root[\"free_heap\"] = ESP.getFreeHeap();\n    root[\"used_heap\"] = ESP.getHeapSize() - ESP.getFreeHeap();\n    root[\"total_heap\"] = ESP.getHeapSize();\n    root[\"min_free_heap\"] = ESP.getMinFreeHeap();\n    root[\"sketch_size\"] = ESP.getSketchSize();\n    root[\"free_sketch_space\"] = ESP.getFreeSketchSpace();\n    root[\"sdk_version\"] = ESP.getSdkVersion();\n    root[\"arduino_version\"] = ARDUINO_VERSION;\n    root[\"flash_chip_size\"] = ESP.getFlashChipSize();\n    root[\"flash_chip_speed\"] = ESP.getFlashChipSpeed();\n    root[\"fs_total\"] = ESPFS.totalBytes();\n    root[\"fs_used\"] = ESPFS.usedBytes();\n    root[\"core_temp\"] = temperatureRead();\n    root[\"cpu_reset_reason\"] = verbosePrintResetReason(esp_reset_reason());\n    root[\"uptime\"] = millis() / 1000;\n\n    return response.send();\n}\n"
  },
  {
    "path": "lib/framework/SystemStatus.h",
    "content": "#ifndef SystemStatus_h\n#define SystemStatus_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <ESPFS.h>\n\n#define SYSTEM_STATUS_SERVICE_PATH \"/rest/systemStatus\"\n\nclass SystemStatus\n{\npublic:\n    SystemStatus(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    esp_err_t systemStatus(PsychicRequest *request);\n};\n\n#endif // end SystemStatus_h\n"
  },
  {
    "path": "lib/framework/UploadFirmwareService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *   Copyright (C) 2025 hmbacher\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <UploadFirmwareService.h>\n#include <esp_ota_ops.h>\n#include <esp_partition.h>\n#include <esp_app_format.h>\n#include <strings.h>\n#include <ArduinoJson.h>\n\nusing namespace std::placeholders; // for `_1` etc\n\nUploadFirmwareService::UploadFirmwareService(PsychicHttpServer *server,\n                                             SecurityManager *securityManager,\n                                             EventSocket *socket) : _server(server),\n                                                                    _securityManager(securityManager),\n                                                                    _socket(socket)\n{\n    _md5[0] = '\\0';  // Initialize MD5 buffer\n}\n\nvoid UploadFirmwareService::begin()\n{\n    if (!_socket->isEventValid(EVENT_OTA_UPDATE))\n    {\n        _socket->registerEvent(EVENT_OTA_UPDATE);\n    }\n    \n    // Set PsychicHttp's limit to max to avoid connection reset on oversized files\n    // We'll validate the size ourselves in handleUpload() to provide proper error handling\n    _server->maxUploadSize = SIZE_MAX;\n    _maxFirmwareSize = getMaxFirmwareSize();\n\n    // Setup progress callback for Update library\n    Update.onProgress([this](size_t progress, size_t total) {\n        if (_socket && total > 0) {\n            int percentComplete = (progress * 100) / total;\n            if (percentComplete > _previousProgress || progress == total) {\n                JsonDocument doc;\n                doc[\"status\"] = \"progress\";\n                doc[\"progress\"] = percentComplete;\n                doc[\"bytes_written\"] = progress;\n                doc[\"total_bytes\"] = total;\n                \n                JsonObject jsonObject = doc.as<JsonObject>();\n                _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n                \n                ESP_LOGV(SVK_TAG, \"Firmware upload process at %d of %d bytes... (%d %%)\", progress, total, percentComplete);\n                \n                _previousProgress = percentComplete;\n            }\n        }\n    });\n\n    PsychicUploadHandler *uploadHandler = new PsychicUploadHandler();\n\n    uploadHandler->onUpload(std::bind(&UploadFirmwareService::handleUpload, this, _1, _2, _3, _4, _5, _6));\n    uploadHandler->onRequest(std::bind(&UploadFirmwareService::uploadComplete, this, _1));  // gets called after upload has been handled\n    uploadHandler->onClose(std::bind(&UploadFirmwareService::handleEarlyDisconnect, this)); // gets called if client disconnects\n    _server->on(UPLOAD_FIRMWARE_PATH, HTTP_POST, uploadHandler);\n\n    ESP_LOGV(SVK_TAG, \"Registered POST endpoint: %s\", UPLOAD_FIRMWARE_PATH);\n}\n\nsize_t UploadFirmwareService::getMaxFirmwareSize()\n{\n    const esp_partition_t* update_partition = esp_ota_get_next_update_partition(NULL);\n    if (update_partition != NULL) {\n        ESP_LOGI(SVK_TAG, \"Max firmware size: %d bytes (from OTA partition)\", update_partition->size);\n        return update_partition->size;\n    }\n    \n    // Fallback if partition query fails (should never happen)\n    ESP_LOGW(SVK_TAG, \"Could not determine OTA partition size, using fallback of 2MB\");\n    return 2097152; // 2 MB fallback\n}\n\nbool UploadFirmwareService::validateChipType(uint8_t *data, size_t len)\n{\n    if (len <= 12)\n    {\n        return false; // Not enough data to validate - firmware is invalid\n    }\n    \n    // Check magic byte at offset 0\n    if (data[0] != ESP_MAGIC_BYTE)\n    {\n        return false;\n    }\n    \n    // Check chip ID at offset 12\n    if (data[12] != ESP_CHIP_ID)\n    {\n        return false;\n    }\n    \n    return true;\n}\n\nesp_err_t UploadFirmwareService::handleUpload(PsychicRequest *request,\n                                              const String &filename,\n                                              uint64_t index,\n                                              uint8_t *data,\n                                              size_t len,\n                                              bool final)\n{\n    // quit if not authorized\n    Authentication authentication = _securityManager->authenticateRequest(request);\n    if (!AuthenticationPredicates::IS_ADMIN(authentication))\n    {\n        return handleError(request, 403, \"Insufficient permissions to upload firmware\");\n    }\n\n    if (index == 0) // Are we at the start of a new upload?\n    {\n        // check details of the file, to see if its a valid bin or md5 file\n        std::string fname(filename.c_str());\n        auto position = fname.find_last_of(\".\");\n        \n        // Check if extension exists to avoid undefined behavior\n        if (position == std::string::npos)\n        {\n            return handleError(request, 406, \"File has no extension\");\n        }\n        \n        std::string extension = fname.substr(position + 1);\n        size_t fsize = request->contentLength();\n\n        _fileType = ft_none;\n        if (strcasecmp(extension.c_str(), \"md5\") == 0)  // Are we processing an MD5 file?\n        {\n            _fileType = ft_md5;\n          \n            if (len == MD5_LENGTH)  // This implicitely checks that fsize is also 32\n            {\n                // Safe: _md5[MD5_LENGTH + 1] has space for 32 bytes + null terminator\n                memcpy(_md5, data, MD5_LENGTH);\n                _md5[MD5_LENGTH] = '\\0';\n\n                return ESP_OK;  // Finished processing MD5 file\n            }\n            else\n            {\n                _md5[0] = '\\0';  // Clear any previously stored MD5 on invalid upload\n                return handleError(request, 422, \"MD5 must be exactly 32 bytes\");\n            }\n        }\n        else if (strcasecmp(extension.c_str(), \"bin\") == 0) // Are we processing a firmware binary?\n        {\n            _fileType = ft_firmware;\n            \n            // Validate file size before processing\n            if (fsize > _maxFirmwareSize)\n            {\n                char errorMsg[64];\n                snprintf(errorMsg, sizeof(errorMsg), \n                        \"Firmware too large: %.2f MB (max: %.2f MB)\", \n                        fsize / 1024.0 / 1024.0,\n                        _maxFirmwareSize / 1024.0 / 1024.0);\n                return handleError(request, 413, errorMsg);\n            }\n            \n            ESP_LOGI(SVK_TAG, \"Starting firmware upload: %s (%d bytes)\", filename.c_str(), fsize);\n#ifdef SERIAL_INFO\n            Serial.printf(\"Starting firmware upload: %s (%d bytes)\\n\", filename.c_str(), fsize);\n#endif\n            \n            // Validate firmware header (magic byte and chip type)\n            if (!validateChipType(data, len))\n            {\n                return handleError(request, 503, \"Wrong firmware for this device\");\n            }\n            \n            if (Update.begin(fsize - sizeof(esp_image_header_t)))\n            {\n                // Emit preparing status after validation succeeds\n                if (_socket)\n                {\n                    JsonDocument doc;\n                    doc[\"status\"] = \"preparing\";\n                    doc[\"progress\"] = 0;\n                    JsonObject jsonObject = doc.as<JsonObject>();\n                    _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n                }\n                \n                if (strlen(_md5) == MD5_LENGTH)\n                {\n                    Update.setMD5(_md5);\n                    ESP_LOGI(SVK_TAG, \"MD5 hash for validation: %s\", _md5);\n#ifdef SERIAL_INFO\n                    Serial.printf(\"MD5 hash for validation: %s\\n\", _md5);\n#endif\n                    _md5[0] = '\\0';  // clear md5 after setting it in Arduino Updater\n                }\n            }\n            else\n            {\n                return handleError(request, 507, \"Insufficient storage space\");\n            }\n        }\n        else // Are we processing an unsupported file type?\n        {\n            return handleError(request, 406, \"File not a firmware binary or MD5 hash\");\n        }\n    }\n    else // we are continuing an existing upload\n    {\n        // Resumable upload: verify that Update was already started\n        if (_fileType == ft_none || !Update.isRunning())\n        {\n            return handleError(request, 400, \"Upload not initialized\");\n        }\n    }\n\n    // if we haven't dealt with an error so far, continue with the firmware update\n    if (!request->_tempObject)\n    {\n        if (_fileType == ft_firmware)\n        {\n            if (Update.write(data, len) != len)\n            {\n                Update.abort();\n                return handleError(request, 500, \"Firmware write failed\");\n            }\n            if (final)\n            {\n                if (!Update.end(true))\n                {\n                    // Get specific error message from Update library (includes MD5 mismatch)\n                    String errorMsg = \"Firmware update failed\";\n                    if (Update.hasError())\n                    {\n                        errorMsg = Update.errorString();\n                    }\n                    Update.abort();\n                    return handleError(request, 500, errorMsg.c_str());\n                }\n            }\n        }\n    }\n\n    return ESP_OK;\n}\n\nesp_err_t UploadFirmwareService::uploadComplete(PsychicRequest *request)\n{\n    // if we already handled an error in handleUpload, do nothing\n    if (request->_tempObject)\n    {\n        return ESP_OK;\n    }\n    \n    // if we completed uploading a md5 file create a JSON response\n    if (_fileType == ft_md5)\n    {\n        if (strlen(_md5) == MD5_LENGTH)\n        {\n            PsychicJsonResponse response = PsychicJsonResponse(request, false);\n            JsonObject root = response.getRoot();\n            root[\"md5\"] = _md5;\n            return response.send();\n        }\n        return ESP_OK;\n    }\n\n    // if no error, send the success response\n    if (_fileType == ft_firmware)\n    {\n        // Emit finished event\n        if (_socket)\n        {\n            JsonDocument doc;\n            doc[\"status\"] = \"finished\";\n            doc[\"progress\"] = 100;\n            JsonObject jsonObject = doc.as<JsonObject>();\n            _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n            vTaskDelay(100 / portTICK_PERIOD_MS); // Give time for event to be sent\n        }\n        \n        ESP_LOGI(SVK_TAG, \"Firmware upload successful - Restarting\");\n#ifdef SERIAL_INFO\n        Serial.println(\"Firmware upload successful - Restarting\");\n#endif\n        \n        // Reset progress tracker for next upload\n        _previousProgress = 0;\n        \n        request->reply(200);\n        RestartService::restartNow();\n        return ESP_OK;\n    }\n\n    // if updated has an error send 500 response and log on Serial\n    if (Update.hasError())\n    {\n        // Get specific error message from Update library\n        String errorMsg = Update.errorString();\n        if (errorMsg.length() == 0)\n        {\n            errorMsg = \"Unknown update error\";\n        }\n        \n        // Reset progress tracker\n        _previousProgress = 0;\n        \n        ESP_LOGE(SVK_TAG, \"Update error: %s\", errorMsg.c_str());\n#ifdef SERIAL_INFO\n        Update.printError(Serial);\n#endif\n        Update.abort();\n        \n        // handleError will emit the WebSocket event and send HTTP response\n        return handleError(request, 500, errorMsg.c_str());\n    }\n\n    return ESP_OK;\n}\n\nesp_err_t UploadFirmwareService::handleError(PsychicRequest *request, int code, const char *message)\n{\n    // if we have had an error already, do nothing\n    if (request->_tempObject)\n    {\n        return ESP_OK;\n    }\n\n    // Emit WebSocket error event for BIN files (skip for MD5 files)\n    if (_fileType == ft_firmware && _socket && message)\n    {\n        JsonDocument doc;\n        doc[\"status\"] = \"error\";\n        doc[\"error\"] = message;\n        JsonObject jsonObject = doc.as<JsonObject>();\n        _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject);\n    }\n    \n    // Log error\n    if (message)\n    {\n        ESP_LOGE(SVK_TAG, \"Firmware upload failed (%d): %s\", code, message);\n#ifdef SERIAL_INFO\n        Serial.printf(\"Firmware upload failed (%d): %s\\n\", code, message);\n#endif\n    }\n    else\n    {\n        ESP_LOGE(SVK_TAG, \"Firmware upload failed with error code: %d\", code);\n#ifdef SERIAL_INFO\n        Serial.printf(\"Firmware upload failed with error code: %d\\n\", code);\n#endif\n    }\n\n    // Reset state to allow new upload attempts\n    _fileType = ft_none;\n    _previousProgress = 0;\n    \n    // Abort any ongoing Update to clear error state\n    Update.abort();\n    \n    // Mark this request as having encountered an error using _tempObject as a flag\n    // (The pointer value itself is not used, only checked for NULL vs non-NULL)\n    // The allocated memory is freed on request destruction (see PsychicRequest::~PsychicRequest())\n    request->_tempObject = malloc(sizeof(int));\n    return request->reply(code);\n}\n\nesp_err_t UploadFirmwareService::handleEarlyDisconnect()\n{\n    // if updated has not ended on connection close, abort it\n    if (!Update.end(true))\n    {\n        ESP_LOGE(SVK_TAG, \"Update error on early disconnect:\");\n#ifdef SERIAL_INFO\n        Update.printError(Serial);\n#endif\n        Update.abort();\n        return ESP_OK;\n    }\n    return ESP_OK;\n}\n"
  },
  {
    "path": "lib/framework/UploadFirmwareService.h",
    "content": "#ifndef UploadFirmwareService_h\n#define UploadFirmwareService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *   Copyright (C) 2025 hmbacher\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <Arduino.h>\n\n#include <Update.h>\n#include <WiFi.h>\n\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n#include <RestartService.h>\n#include <EventSocket.h>\n#include <FirmwareUpdateEvents.h>\n\n#define UPLOAD_FIRMWARE_PATH \"/rest/uploadFirmware\"\n\n// Firmware upload constants\nconstexpr size_t MD5_LENGTH = 32;              // MD5 hash length\nconstexpr uint8_t ESP_MAGIC_BYTE = 0xE9;       // ESP binary magic byte\n\n// ESP32 chip type identifiers (byte offset 12 in firmware)\n#if CONFIG_IDF_TARGET_ESP32\n    constexpr uint8_t ESP_CHIP_ID = 0;\n#elif CONFIG_IDF_TARGET_ESP32S2\n    constexpr uint8_t ESP_CHIP_ID = 2;\n#elif CONFIG_IDF_TARGET_ESP32C3\n    constexpr uint8_t ESP_CHIP_ID = 5;\n#elif CONFIG_IDF_TARGET_ESP32S3\n    constexpr uint8_t ESP_CHIP_ID = 9;\n#else\n    #error \"Unsupported ESP32 target\"\n#endif\n\nenum FileType\n{\n    ft_none = 0,\n    ft_firmware = 1,\n    ft_md5 = 2\n};\n\n/**\n * @brief Service for handling firmware uploads over HTTP with OTA support\n * \n * Supports chunked uploads of .bin firmware files and .md5 hash files for validation.\n * Emits real-time progress updates via WebSocket and validates chip compatibility.\n */\nclass UploadFirmwareService\n{\npublic:\n    /**\n     * @brief Construct firmware upload service\n     * @param server PsychicHttpServer instance for handling HTTP requests\n     * @param securityManager Security manager for authentication\n     * @param socket EventSocket for emitting real-time progress updates\n     */\n    UploadFirmwareService(PsychicHttpServer *server, SecurityManager *securityManager, EventSocket *socket);\n\n    /**\n     * @brief Initialize the service and register HTTP endpoints\n     * Sets up upload handlers and progress callbacks\n     */\n    void begin();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    EventSocket *_socket;\n\n    // Upload state (per-instance to support concurrent uploads)\n    char _md5[MD5_LENGTH + 1];\n    FileType _fileType = ft_none;\n    int _previousProgress = 0;\n    size_t _maxFirmwareSize = 0;\n\n    /**\n     * @brief Get maximum firmware size from OTA partition\n     * @return Size of OTA partition in bytes, or 2MB fallback if not available\n     */\n    size_t getMaxFirmwareSize();\n\n    /**\n     * @brief Validate firmware chip type matches target device\n     * @param data First chunk of firmware data\n     * @param len Length of data chunk\n     * @return true if magic byte and chip ID are valid, false otherwise\n     */\n    bool validateChipType(uint8_t *data, size_t len);\n\n    /**\n     * @brief Handle incoming firmware upload chunks. Called once per chunk.\n     * @param request HTTP request object\n     * @param filename Uploaded file name\n     * @param index Byte offset of this chunk (0 for first chunk)\n     * @param data Chunk data buffer\n     * @param len Chunk length in bytes\n     * @param final true if this is the last chunk\n     * @return ESP_OK on success, error code on failure\n     */\n    esp_err_t handleUpload(PsychicRequest *request,\n                           const String &filename,\n                           uint64_t index,\n                           uint8_t *data,\n                           size_t len,\n                           bool final);\n    \n    /**\n     * @brief Called after upload finished (i.e. all chunks received)\n     * @param request HTTP request object\n     * @return ESP_OK on success, error code on failure\n     */\n    esp_err_t uploadComplete(PsychicRequest *request);\n    \n    /**\n     * @brief Handle upload errors and emit error events\n     * @param request HTTP request object\n     * @param code HTTP status code to return\n     * @param message Optional error message (default: nullptr)\n     * @return ESP_OK (error already handled)\n     */\n    esp_err_t handleError(PsychicRequest *request, int code, const char *message = nullptr);\n    \n    /**\n     * @brief Handle client disconnection during upload\n     * @return ESP_OK on successful cleanup\n     */\n    esp_err_t handleEarlyDisconnect();\n};\n\n#endif // end UploadFirmwareService_h\n"
  },
  {
    "path": "lib/framework/WebSocketServer.h",
    "content": "#ifndef WebSocketServer_h\n#define WebSocketServer_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <StatefulService.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n\n#define WEB_SOCKET_ORIGIN \"wsserver\"\n#define WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX \"wsserver:\"\n\ntemplate <class T>\nclass WebSocketServer\n{\npublic:\n    WebSocketServer(JsonStateReader<T> stateReader,\n                    JsonStateUpdater<T> stateUpdater,\n                    StatefulService<T> *statefulService,\n                    PsychicHttpServer *server,\n                    const char *webSocketPath,\n                    SecurityManager *securityManager,\n                    AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _stateReader(stateReader),\n                                                                                                            _stateUpdater(stateUpdater),\n                                                                                                            _statefulService(statefulService),\n                                                                                                            _server(server),\n                                                                                                            _webSocketPath(webSocketPath),\n                                                                                                            _authenticationPredicate(authenticationPredicate),\n                                                                                                            _securityManager(securityManager)\n    {\n        _statefulService->addUpdateHandler(\n            [&](const String &originId)\n            { transmitData(nullptr, originId); },\n            false);\n    }\n\n    void begin()\n    {\n        _webSocket.setFilter(_securityManager->filterRequest(_authenticationPredicate));\n        _webSocket.onOpen(std::bind(&WebSocketServer::onWSOpen,\n                                    this,\n                                    std::placeholders::_1));\n        _webSocket.onClose(std::bind(&WebSocketServer::onWSClose,\n                                     this,\n                                     std::placeholders::_1));\n        _webSocket.onFrame(std::bind(&WebSocketServer::onWSFrame,\n                                     this,\n                                     std::placeholders::_1,\n                                     std::placeholders::_2));\n        _server->on(_webSocketPath.c_str(), &_webSocket);\n\n        ESP_LOGV(SVK_TAG, \"Registered WebSocket handler: %s\", _webSocketPath.c_str());\n    }\n\n    void onWSOpen(PsychicWebSocketClient *client)\n    {\n\n        // when a client connects, we transmit it's id and the current payload\n        transmitId(client);\n        transmitData(client, WEB_SOCKET_ORIGIN);\n        ESP_LOGI(SVK_TAG, \"ws[%s][%u] connect\", client->remoteIP().toString().c_str(), client->socket());\n    }\n\n    void onWSClose(PsychicWebSocketClient *client)\n    {\n        ESP_LOGI(SVK_TAG, \"ws[%s][%u] disconnect\", client->remoteIP().toString().c_str(), client->socket());\n    }\n\n    esp_err_t onWSFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame)\n    {\n        ESP_LOGV(SVK_TAG, \"ws[%s][%u] opcode[%d]\", request->client()->remoteIP().toString().c_str(), request->client()->socket(), frame->type);\n\n        if (frame->type == HTTPD_WS_TYPE_TEXT)\n        {\n            ESP_LOGV(SVK_TAG, \"ws[%s][%u] request: %s\", request->client()->remoteIP().toString().c_str(), request->client()->socket(), (char *)frame->payload);\n\n            JsonDocument jsonDocument;\n            DeserializationError error = deserializeJson(jsonDocument, (char *)frame->payload, frame->len);\n\n            if (!error && jsonDocument.is<JsonObject>())\n            {\n                JsonObject jsonObject = jsonDocument.as<JsonObject>();\n                _statefulService->update(jsonObject, _stateUpdater, clientId(request->client()));\n                return ESP_OK;\n            }\n        }\n        return ESP_OK;\n    }\n\n    String clientId(PsychicWebSocketClient *client)\n    {\n        return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->socket());\n    }\n\nprivate:\n    JsonStateReader<T> _stateReader;\n    JsonStateUpdater<T> _stateUpdater;\n    StatefulService<T> *_statefulService;\n    AuthenticationPredicate _authenticationPredicate;\n    SecurityManager *_securityManager;\n    PsychicHttpServer *_server;\n    PsychicWebSocketHandler _webSocket;\n    String _webSocketPath;\n\n    void transmitId(PsychicWebSocketClient *client)\n    {\n        JsonDocument jsonDocument;\n        JsonObject root = jsonDocument.to<JsonObject>();\n        root[\"type\"] = \"id\";\n        root[\"id\"] = clientId(client);\n\n        // serialize the json to a string\n        String buffer;\n        serializeJson(jsonDocument, buffer);\n        client->sendMessage(buffer.c_str());\n    }\n\n    /**\n     * Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if\n     * specified.\n     *\n     * Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach\n     * simplifies the client and the server implementation but may not be sufficient for all use-cases.\n     */\n    void transmitData(PsychicWebSocketClient *client, const String &originId)\n    {\n        JsonDocument jsonDocument;\n        JsonObject root = jsonDocument.to<JsonObject>();\n        String buffer;\n\n        _statefulService->read(root, _stateReader);\n\n        // serialize the json to a string\n        serializeJson(jsonDocument, buffer);\n        if (client)\n        {\n            client->sendMessage(buffer.c_str());\n        }\n        else\n        {\n            _webSocket.sendAll(buffer.c_str());\n        }\n    }\n};\n\n#endif\n"
  },
  {
    "path": "lib/framework/WiFiScanner.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFiScanner.h>\n\nWiFiScanner::WiFiScanner(PsychicHttpServer *server,\n                         SecurityManager *securityManager) : _server(server),\n                                                             _securityManager(securityManager)\n{\n}\n\nvoid WiFiScanner::begin()\n{\n    _server->on(SCAN_NETWORKS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", SCAN_NETWORKS_SERVICE_PATH);\n\n    _server->on(LIST_NETWORKS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_ADMIN));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", LIST_NETWORKS_SERVICE_PATH);\n}\n\nesp_err_t WiFiScanner::scanNetworks(PsychicRequest *request)\n{\n    if (WiFi.scanComplete() != -1)\n    {\n        WiFi.scanDelete();\n        WiFi.scanNetworks(true);\n    }\n    return request->reply(202);\n}\n\nesp_err_t WiFiScanner::listNetworks(PsychicRequest *request)\n{\n    int numNetworks = WiFi.scanComplete();\n    if (numNetworks > -1)\n    {\n        PsychicJsonResponse response = PsychicJsonResponse(request, false);\n        JsonObject root = response.getRoot();\n        JsonArray networks = root[\"networks\"].to<JsonArray>();\n        for (int i = 0; i < numNetworks; i++)\n        {\n            JsonObject network = networks.add<JsonObject>();\n            network[\"rssi\"] = WiFi.RSSI(i);\n            network[\"ssid\"] = WiFi.SSID(i);\n            network[\"bssid\"] = WiFi.BSSIDstr(i);\n            network[\"channel\"] = WiFi.channel(i);\n            network[\"encryption_type\"] = (uint8_t)WiFi.encryptionType(i);\n        }\n\n        return response.send();\n    }\n    else if (numNetworks == -1)\n    {\n        return request->reply(202);\n    }\n    else\n    {\n        return scanNetworks(request);\n    }\n}\n"
  },
  {
    "path": "lib/framework/WiFiScanner.h",
    "content": "#ifndef WiFiScanner_h\n#define WiFiScanner_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <SecurityManager.h>\n\n#define SCAN_NETWORKS_SERVICE_PATH \"/rest/scanNetworks\"\n#define LIST_NETWORKS_SERVICE_PATH \"/rest/listNetworks\"\n\nclass WiFiScanner\n{\npublic:\n    WiFiScanner(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n\n    esp_err_t scanNetworks(PsychicRequest *request);\n    esp_err_t listNetworks(PsychicRequest *request);\n};\n\n#endif // end WiFiScanner_h\n"
  },
  {
    "path": "lib/framework/WiFiSettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFiSettingsService.h>\n\nWiFiSettingsService::WiFiSettingsService(PsychicHttpServer *server,\n                                         FS *fs,\n                                         SecurityManager *securityManager,\n                                         EventSocket *socket) : _server(server),\n                                                                _securityManager(securityManager),\n                                                                _httpEndpoint(WiFiSettings::read, WiFiSettings::update, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager,\n                                                                              AuthenticationPredicates::IS_ADMIN),\n                                                                _fsPersistence(WiFiSettings::read, WiFiSettings::update, this, fs, WIFI_SETTINGS_FILE),\n                                                                _lastConnectionAttempt(0),\n                                                                _delayedReconnectTime(0),\n                                                                _delayedReconnectPending(false),\n                                                                _socket(socket)\n{\n    addUpdateHandler([&](const String &originId)\n                     { delayedReconnect(); },\n                     false);\n}\n\nvoid WiFiSettingsService::initWiFi()\n{\n    WiFi.mode(WIFI_MODE_STA); // this is the default.\n\n    // Disable WiFi config persistance and auto reconnect\n    WiFi.persistent(false);\n    WiFi.setAutoReconnect(false);\n\n    WiFi.onEvent(\n        std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),\n        WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);\n    WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeStop, this, std::placeholders::_1, std::placeholders::_2),\n                 WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_STOP);\n\n    _fsPersistence.readFromFS();\n    reconfigureWiFiConnection();\n}\n\nvoid WiFiSettingsService::begin()\n{\n    _socket->registerEvent(EVENT_RSSI);\n    _socket->registerEvent(EVENT_RECONNECT);\n\n    _httpEndpoint.begin();\n}\n\nvoid WiFiSettingsService::delayedReconnect()\n{\n    _delayedReconnectTime = millis() + DELAYED_RECONNECT_MS;\n    _delayedReconnectPending = true;\n    ESP_LOGI(SVK_TAG, \"Delayed WiFi reconnection scheduled in %d ms\", DELAYED_RECONNECT_MS);\n\n    // Emit event to notify clients of impending reconnection\n    JsonDocument doc;\n    doc[\"delay_ms\"] = DELAYED_RECONNECT_MS;\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_RECONNECT, jsonObject);\n}\n\nvoid WiFiSettingsService::reconfigureWiFiConnection()\n{\n    // reset last connection attempt to force loop to reconnect immediately\n    _lastConnectionAttempt = 0;\n\n    String connectionMode;\n\n    switch (_state.staConnectionMode)\n    {\n    case (u_int8_t)STAConnectionMode::OFFLINE:\n        connectionMode = \"OFFLINE\";\n        break;\n    case (u_int8_t)STAConnectionMode::PRIORITY:\n        connectionMode = \"PRIORITY\";\n        break;\n    case (u_int8_t)STAConnectionMode::STRENGTH:\n        connectionMode = \"STRENGTH\";\n        break;\n    default:\n        connectionMode = \"UNKNOWN\";\n        break;\n    }\n\n    ESP_LOGI(SVK_TAG, \"Reconfiguring WiFi connection to: %s\", connectionMode.c_str());\n\n    // disconnect and de-configure wifi\n    if (WiFi.disconnect(true))\n    {\n        _stopping = true;\n    }\n}\n\nvoid WiFiSettingsService::loop()\n{\n    unsigned long currentMillis = millis();\n\n    // Handle delayed reconnection\n    if (_delayedReconnectPending && currentMillis >= _delayedReconnectTime)\n    {\n        _delayedReconnectPending = false;\n        ESP_LOGI(SVK_TAG, \"Executing delayed WiFi reconnection\");\n        reconfigureWiFiConnection();\n    }\n\n    if (!_lastConnectionAttempt || (unsigned long)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY)\n    {\n        _lastConnectionAttempt = currentMillis;\n        manageSTA();\n    }\n\n    if (!_lastRssiUpdate || (unsigned long)(currentMillis - _lastRssiUpdate) >= RSSI_EVENT_DELAY)\n    {\n        _lastRssiUpdate = currentMillis;\n        updateRSSI();\n    }\n}\n\nString WiFiSettingsService::getHostname()\n{\n    return _state.hostname;\n}\n\nString WiFiSettingsService::getIP()\n{\n    if (WiFi.isConnected())\n    {\n        return WiFi.localIP().toString();\n    }\n    return \"Not connected\";\n}\n\nvoid WiFiSettingsService::manageSTA()\n{\n    // Abort if already connected, if we have no SSID, or are in offline mode\n    if (WiFi.isConnected() || _state.wifiSettings.empty() || _state.staConnectionMode == (u_int8_t)STAConnectionMode::OFFLINE)\n    {\n        return;\n    }\n    else\n    {\n#ifdef SERIAL_INFO\n        Serial.println(\"Connecting to WiFi...\");\n#endif\n        connectToWiFi();\n    }\n}\n\nvoid WiFiSettingsService::connectToWiFi()\n{\n    // reset availability flag for all stored networks\n    for (auto &network : _state.wifiSettings)\n    {\n        network.available = false;\n    }\n\n    // scanning for available networks\n    int scanResult = WiFi.scanNetworks();\n    if (scanResult == WIFI_SCAN_FAILED)\n    {\n        ESP_LOGE(SVK_TAG, \"WiFi scan failed.\");\n    }\n    else if (scanResult == 0)\n    {\n        ESP_LOGW(SVK_TAG, \"No networks found.\");\n    }\n    else\n    {\n        ESP_LOGI(SVK_TAG, \"%d networks found.\", scanResult);\n\n        // find the best network to connect\n        wifi_settings_t *bestNetwork = NULL;\n        int bestNetworkDb = FACTORY_WIFI_RSSI_THRESHOLD;\n\n        for (int i = 0; i < scanResult; ++i)\n        {\n            String ssid_scan;\n            int32_t rssi_scan;\n            uint8_t sec_scan;\n            uint8_t *BSSID_scan;\n            int32_t chan_scan;\n\n            WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan);\n            ESP_LOGV(SVK_TAG, \"SSID: %s, BSSID: \" MACSTR \", RSSI: %d dbm, Channel: %d\", ssid_scan.c_str(), MAC2STR(BSSID_scan), rssi_scan, chan_scan);\n\n            for (auto &network : _state.wifiSettings)\n            {\n                if (ssid_scan.equals(network.ssid))\n                { // SSID match\n                    if (rssi_scan > bestNetworkDb)\n                    { // best network\n                        bestNetworkDb = rssi_scan;\n                        ESP_LOGV(SVK_TAG, \"--> New best network SSID: %s, BSSID: \" MACSTR \"\", ssid_scan.c_str(), MAC2STR(BSSID_scan));\n                        network.available = true;\n                        network.channel = chan_scan;\n                        memcpy(network.bssid, BSSID_scan, 6);\n                        bestNetwork = &network;\n                    }\n                    else if (rssi_scan >= FACTORY_WIFI_RSSI_THRESHOLD && network.available == false)\n                    { // available network\n                        network.available = true;\n                        network.channel = chan_scan;\n                        memcpy(network.bssid, BSSID_scan, 6);\n                    }\n                    break;\n                }\n            }\n        }\n\n        // Connection mode PRIORITY: connect to the first available network\n        if (_state.staConnectionMode == (u_int8_t)STAConnectionMode::PRIORITY)\n        {\n            for (auto &network : _state.wifiSettings)\n            {\n                if (network.available == true)\n                {\n                    ESP_LOGI(SVK_TAG, \"Connecting to first available network: %s\", network.ssid.c_str());\n                    configureNetwork(network);\n                    break;\n                }\n            }\n        }\n        // Connection mode STRENGTH: connect to the strongest network\n        else if (_state.staConnectionMode == (u_int8_t)STAConnectionMode::STRENGTH)\n        {\n            if (bestNetwork)\n            {\n                ESP_LOGI(SVK_TAG, \"Connecting to strongest network: %s, BSSID: \" MACSTR \" \", bestNetwork->ssid.c_str(), MAC2STR(bestNetwork->bssid));\n                configureNetwork(*bestNetwork);\n            }\n            else\n            {\n                ESP_LOGI(SVK_TAG, \"No suitable network found.\");\n            }\n        }\n        // Connection mode OFFLINE: do not connect to any network\n        else if (_state.staConnectionMode == (u_int8_t)STAConnectionMode::OFFLINE)\n        {\n            ESP_LOGI(SVK_TAG, \"WiFi connection mode is OFFLINE, not connecting to any network.\");\n        }\n        // Connection mode is unknown: do not connect to any network\n        else\n        {\n            ESP_LOGE(SVK_TAG, \"Unknown connection mode, not connecting to any network.\");\n        }\n\n        // delete scan results\n        WiFi.scanDelete();\n    }\n}\n\nvoid WiFiSettingsService::configureNetwork(wifi_settings_t &network)\n{\n    if (network.staticIPConfig)\n    {\n        // configure for static IP\n        WiFi.config(network.localIP, network.gatewayIP, network.subnetMask, network.dnsIP1, network.dnsIP2);\n    }\n    else\n    {\n        // configure for DHCP\n        WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);\n    }\n    WiFi.setHostname(_state.hostname.c_str());\n\n    // attempt to connect to the network\n    WiFi.begin(network.ssid.c_str(), network.password.c_str(), network.channel, network.bssid);\n    // WiFi.begin(network.ssid.c_str(), network.password.c_str());\n\n#if CONFIG_IDF_TARGET_ESP32C3\n    WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi\n#endif\n}\n\nvoid WiFiSettingsService::updateRSSI()\n{\n    JsonDocument doc;\n    doc[\"rssi\"] = WiFi.RSSI();\n    doc[\"ssid\"] = WiFi.isConnected() ? WiFi.SSID() : \"disconnected\";\n    JsonObject jsonObject = doc.as<JsonObject>();\n    _socket->emitEvent(EVENT_RSSI, jsonObject);\n}\n\nvoid WiFiSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    manageSTA();\n}\n\nvoid WiFiSettingsService::onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    if (_stopping)\n    {\n        _lastConnectionAttempt = 0;\n        _stopping = false;\n    }\n}\n"
  },
  {
    "path": "lib/framework/WiFiSettingsService.h",
    "content": "#ifndef WiFiSettingsService_h\n#define WiFiSettingsService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n#include <WiFiMulti.h>\n#include <SettingValue.h>\n#include <StatefulService.h>\n#include <EventSocket.h>\n#include <FSPersistence.h>\n#include <HttpEndpoint.h>\n#include <JsonUtils.h>\n#include <SecurityManager.h>\n#include <PsychicHttp.h>\n#include <vector>\n\n#ifndef FACTORY_WIFI_SSID\n#define FACTORY_WIFI_SSID \"\"\n#endif\n\n#ifndef FACTORY_WIFI_PASSWORD\n#define FACTORY_WIFI_PASSWORD \"\"\n#endif\n\n#ifndef FACTORY_WIFI_HOSTNAME\n#define FACTORY_WIFI_HOSTNAME \"#{platform}-#{unique_id}\"\n#endif\n\n#ifndef FACTORY_WIFI_RSSI_THRESHOLD\n#define FACTORY_WIFI_RSSI_THRESHOLD -80\n#endif\n\n#define WIFI_SETTINGS_FILE \"/config/wifiSettings.json\"\n#define WIFI_SETTINGS_SERVICE_PATH \"/rest/wifiSettings\"\n\n#define WIFI_RECONNECTION_DELAY 1000 * 5\n#define RSSI_EVENT_DELAY 500\n#define DELAYED_RECONNECT_MS 1000\n\n#define EVENT_RSSI \"rssi\"\n#define EVENT_RECONNECT \"reconnect\"\n\n// Struct defining the wifi settings\ntypedef struct\n{\n    String ssid;\n    uint8_t bssid[6];\n    int32_t channel;\n    String password;\n    bool staticIPConfig;\n    IPAddress localIP;\n    IPAddress gatewayIP;\n    IPAddress subnetMask;\n    IPAddress dnsIP1;\n    IPAddress dnsIP2;\n    bool available;\n} wifi_settings_t;\n\nenum class STAConnectionMode\n{\n    OFFLINE = 0,\n    STRENGTH,\n    PRIORITY\n};\n\nclass WiFiSettings\n{\npublic:\n    // core wifi configuration\n    String hostname;\n    u_int8_t staConnectionMode;\n    std::vector<wifi_settings_t> wifiSettings;\n\n    static void read(WiFiSettings &settings, JsonObject &root)\n    {\n        root[\"hostname\"] = settings.hostname;\n        root[\"connection_mode\"] = settings.staConnectionMode;\n\n        // create JSON array from root\n        JsonArray wifiNetworks = root[\"wifi_networks\"].to<JsonArray>();\n\n        // iterate over the wifiSettings\n        for (auto &wifi : settings.wifiSettings)\n        {\n            // create JSON object for each wifi network\n            JsonObject wifiNetwork = wifiNetworks.add<JsonObject>();\n\n            // add the ssid and password to the JSON object\n            wifiNetwork[\"ssid\"] = wifi.ssid;\n            wifiNetwork[\"password\"] = wifi.password;\n            wifiNetwork[\"static_ip_config\"] = wifi.staticIPConfig;\n\n            // extended settings\n            JsonUtils::writeIP(wifiNetwork, \"local_ip\", wifi.localIP);\n            JsonUtils::writeIP(wifiNetwork, \"gateway_ip\", wifi.gatewayIP);\n            JsonUtils::writeIP(wifiNetwork, \"subnet_mask\", wifi.subnetMask);\n            JsonUtils::writeIP(wifiNetwork, \"dns_ip_1\", wifi.dnsIP1);\n            JsonUtils::writeIP(wifiNetwork, \"dns_ip_2\", wifi.dnsIP2);\n        }\n\n        ESP_LOGV(SVK_TAG, \"WiFi Settings read\");\n    }\n\n    static StateUpdateResult update(JsonObject &root, WiFiSettings &settings, const String &originId)\n    {\n        settings.hostname = root[\"hostname\"] | SettingValue::format(FACTORY_WIFI_HOSTNAME);\n        settings.staConnectionMode = root[\"connection_mode\"] | 1;\n\n        settings.wifiSettings.clear();\n\n        // create JSON array from root\n        JsonArray wifiNetworks = root[\"wifi_networks\"];\n        if (root[\"wifi_networks\"].is<JsonArray>())\n        {\n            // iterate over the wifiSettings\n            int i = 0;\n            for (auto wifiNetwork : wifiNetworks)\n            {\n                // max 5 wifi networks\n                if (i++ >= 5)\n                {\n                    ESP_LOGE(SVK_TAG, \"Too many wifi networks\");\n                    break;\n                }\n\n                // create JSON object for each wifi network\n                JsonObject wifi = wifiNetwork.as<JsonObject>();\n\n                // Check if SSID length is between 1 and 31 characters and password between 0 and 64 characters\n                if (wifi[\"ssid\"].as<String>().length() < 1 || wifi[\"ssid\"].as<String>().length() > 31 || wifi[\"password\"].as<String>().length() > 64)\n                {\n                    ESP_LOGE(SVK_TAG, \"SSID or password length is invalid\");\n                }\n                else\n                {\n                    // add the ssid and password to the JSON object\n                    wifi_settings_t wifiSettings;\n\n                    wifiSettings.ssid = wifi[\"ssid\"].as<String>();\n                    wifiSettings.password = wifi[\"password\"].as<String>();\n                    wifiSettings.staticIPConfig = wifi[\"static_ip_config\"];\n\n                    // extended settings\n                    JsonUtils::readIP(wifi, \"local_ip\", wifiSettings.localIP);\n                    JsonUtils::readIP(wifi, \"gateway_ip\", wifiSettings.gatewayIP);\n                    JsonUtils::readIP(wifi, \"subnet_mask\", wifiSettings.subnetMask);\n                    JsonUtils::readIP(wifi, \"dns_ip_1\", wifiSettings.dnsIP1);\n                    JsonUtils::readIP(wifi, \"dns_ip_2\", wifiSettings.dnsIP2);\n\n                    // Swap around the dns servers if 2 is populated but 1 is not\n                    if (IPUtils::isNotSet(wifiSettings.dnsIP1) && IPUtils::isSet(wifiSettings.dnsIP2))\n                    {\n                        wifiSettings.dnsIP1 = wifiSettings.dnsIP2;\n                        wifiSettings.dnsIP2 = INADDR_NONE;\n                    }\n\n                    // Turning off static ip config if we don't meet the minimum requirements\n                    // of ipAddress, gateway and subnet. This may change to static ip only\n                    // as sensible defaults can be assumed for gateway and subnet\n                    if (wifiSettings.staticIPConfig && (IPUtils::isNotSet(wifiSettings.localIP) || IPUtils::isNotSet(wifiSettings.gatewayIP) ||\n                                                        IPUtils::isNotSet(wifiSettings.subnetMask)))\n                    {\n                        wifiSettings.staticIPConfig = false;\n                    }\n\n                    // reset scan result\n                    wifiSettings.available = false;\n                    settings.wifiSettings.push_back(wifiSettings);\n\n                    // increment the wifi network index\n                    i++;\n                }\n            }\n        }\n        else\n        {\n            // populate with factory defaults if they are present\n            if (String(FACTORY_WIFI_SSID).length() > 0)\n            {\n                settings.wifiSettings.push_back(wifi_settings_t{\n                    .ssid = FACTORY_WIFI_SSID,\n                    .password = FACTORY_WIFI_PASSWORD,\n                    .staticIPConfig = false,\n                    .localIP = INADDR_NONE,\n                    .gatewayIP = INADDR_NONE,\n                    .subnetMask = INADDR_NONE,\n                    .dnsIP1 = INADDR_NONE,\n                    .dnsIP2 = INADDR_NONE,\n                    .available = false,\n                });\n            }\n        }\n        ESP_LOGV(SVK_TAG, \"WiFi Settings updated\");\n\n        return StateUpdateResult::CHANGED;\n    };\n};\n\nclass WiFiSettingsService : public StatefulService<WiFiSettings>\n{\npublic:\n    WiFiSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket);\n\n    void initWiFi();\n    void begin();\n    void loop();\n    void delayedReconnect();\n    String getHostname();\n    String getIP();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n    HttpEndpoint<WiFiSettings> _httpEndpoint;\n    FSPersistence<WiFiSettings> _fsPersistence;\n    EventSocket *_socket;\n    unsigned long _lastConnectionAttempt;\n    unsigned long _lastRssiUpdate;\n    unsigned long _delayedReconnectTime;\n    bool _delayedReconnectPending;\n\n    bool _stopping;\n    void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);\n    void onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info);\n\n    void reconfigureWiFiConnection();\n    void manageSTA();\n    void connectToWiFi();\n    void configureNetwork(wifi_settings_t &network);\n    void updateRSSI();\n};\n\n#endif // end WiFiSettingsService_h\n"
  },
  {
    "path": "lib/framework/WiFiStatus.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFiStatus.h>\n\nWiFiStatus::WiFiStatus(PsychicHttpServer *server,\n                       SecurityManager *securityManager) : _server(server),\n                                                           _securityManager(securityManager)\n{\n}\n\nvoid WiFiStatus::begin()\n{\n    _server->on(WIFI_STATUS_SERVICE_PATH,\n                HTTP_GET,\n                _securityManager->wrapRequest(std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1),\n                                              AuthenticationPredicates::IS_AUTHENTICATED));\n\n    ESP_LOGV(SVK_TAG, \"Registered GET endpoint: %s\", WIFI_STATUS_SERVICE_PATH);\n\n    WiFi.onEvent(onStationModeConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);\n    WiFi.onEvent(onStationModeDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);\n    WiFi.onEvent(onStationModeGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);\n}\n\nvoid WiFiStatus::onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    ESP_LOGI(SVK_TAG, \"WiFi Connected.\");\n\n#ifdef SERIAL_INFO\n    Serial.println(\"WiFi Connected.\");\n#endif\n}\n\nvoid WiFiStatus::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    ESP_LOGI(SVK_TAG, \"WiFi Disconnected. Reason code=%d\", info.wifi_sta_disconnected.reason);\n\n#ifdef SERIAL_INFO\n    Serial.print(\"WiFi Disconnected. Reason code=\");\n    Serial.println(info.wifi_sta_disconnected.reason);\n#endif\n}\n\nvoid WiFiStatus::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info)\n{\n    ESP_LOGI(SVK_TAG, \"WiFi Got IP. localIP=%s, hostName=%s\", WiFi.localIP().toString().c_str(), WiFi.getHostname());\n#ifdef SERIAL_INFO\n    Serial.printf(\"WiFi Got IP. localIP=%s, hostName=%s\\r\\n\", WiFi.localIP().toString().c_str(), WiFi.getHostname());\n#endif\n}\n\nesp_err_t WiFiStatus::wifiStatus(PsychicRequest *request)\n{\n    PsychicJsonResponse response = PsychicJsonResponse(request, false);\n    JsonObject root = response.getRoot();\n    wl_status_t status = WiFi.status();\n    root[\"status\"] = (uint8_t)status;\n    if (status == WL_CONNECTED)\n    {\n        root[\"local_ip\"] = WiFi.localIP().toString();\n        root[\"mac_address\"] = WiFi.macAddress();\n        root[\"rssi\"] = WiFi.RSSI();\n        root[\"ssid\"] = WiFi.SSID();\n        root[\"bssid\"] = WiFi.BSSIDstr();\n        root[\"channel\"] = WiFi.channel();\n        root[\"subnet_mask\"] = WiFi.subnetMask().toString();\n        root[\"gateway_ip\"] = WiFi.gatewayIP().toString();\n        IPAddress dnsIP1 = WiFi.dnsIP(0);\n        IPAddress dnsIP2 = WiFi.dnsIP(1);\n        if (IPUtils::isSet(dnsIP1))\n        {\n            root[\"dns_ip_1\"] = dnsIP1.toString();\n        }\n        if (IPUtils::isSet(dnsIP2))\n        {\n            root[\"dns_ip_2\"] = dnsIP2.toString();\n        }\n    }\n\n    return response.send();\n}\n\nbool WiFiStatus::isConnected()\n{\n    return WiFi.status() == WL_CONNECTED;\n}\n"
  },
  {
    "path": "lib/framework/WiFiStatus.h",
    "content": "#ifndef WiFiStatus_h\n#define WiFiStatus_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <WiFi.h>\n\n#include <ArduinoJson.h>\n#include <PsychicHttp.h>\n#include <IPUtils.h>\n#include <SecurityManager.h>\n\n#define WIFI_STATUS_SERVICE_PATH \"/rest/wifiStatus\"\n\nclass WiFiStatus\n{\npublic:\n    WiFiStatus(PsychicHttpServer *server, SecurityManager *securityManager);\n\n    void begin();\n\n    bool isConnected();\n\nprivate:\n    PsychicHttpServer *_server;\n    SecurityManager *_securityManager;\n\n    // static functions for logging WiFi events to the UART\n    static void onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info);\n    static void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);\n    static void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);\n    esp_err_t wifiStatus(PsychicRequest *request);\n};\n\n#endif // end WiFiStatus_h\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: ESP32 SvelteKit\n\nnav:\n  - Home: index.md\n  - \"Build Tools\":\n      - gettingstarted.md\n      - buildprocess.md\n  - \"Front End\":\n      - sveltekit.md\n      - structure.md\n      - stores.md\n      - components.md\n  - \"Back End\":\n      - statefulservice.md\n      - restfulapi.md\n\nsite_author: elims\nsite_description: >-\n  A simple, secure and extensible framework for IoT projects on ESP32 platforms with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n\n# Repository\nrepo_name: theelims/ESP32-sveltekit\nrepo_url: https://github.com/theelims/ESP32-sveltekit\n\ntheme:\n  name: material\n  logo: media/svelte-logo.png\n  favicon: media/favicon.png\n  icon:\n    repo: fontawesome/brands/github\n  palette:\n    # Palette toggle for light mode\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      toggle:\n        icon: material/weather-night\n        name: Switch to dark mode\n      primary: blue\n      accent: blue\n\n    # Palette toggle for dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      toggle:\n        icon: material/weather-sunny\n        name: Switch to light mode\n      primary: indigo\n      accent: indigo\n\n  features:\n    - navigation.instant\n    - navigation.tracking\n    - navigation.tabs\n    - navigation.tabs.sticky\n    - navigation.sections\n    - navigation.expand\n    - toc.follow\n    - toc.integrate\n    - navigation.top\n    - content.code.copy\n\nmarkdown_extensions:\n  - attr_list\n  - md_in_html\n  - tables\n  - admonition\n  - pymdownx.details\n  - pymdownx.superfences:\n      custom_fences:\n        - name: mermaid\n          class: mermaid\n          format: !!python/name:pymdownx.superfences.fence_code_format\n  - pymdownx.emoji:\n      emoji_index: !!python/name:materialx.emoji.twemoji\n      emoji_generator: !!python/name:materialx.emoji.to_svg\n\nextra:\n  social:\n    - icon: fontawesome/brands/github\n      link: https://github.com/theelims/\n    - icon: fontawesome/brands/discord\n      link: https://discord.gg/MTn9mVUG5n\n  consent:\n    title: Cookie consent\n    description: >-\n      We use cookies to recognize your repeated visits and preferences, as well\n      as to measure the effectiveness of our documentation and whether users\n      find what they're searching for. With your consent, you're helping us to\n      make our documentation better.\n    actions:\n      - accept\n      - reject\n\nplugins:\n  - search:\n      separator: '[\\s\\-,:!=\\[\\]()\"/]+|(?!\\b)(?=[A-Z][a-z])|\\.(?!\\d)|&[lg]t;'\n\ncopyright: |\n  Copyright &copy; 2025 by <a href=\"https://github.com/theelims\"  target=\"_blank\" rel=\"noopener\">elims</a> -\n  <a href=\"#__consent\">Change cookie settings</a>\n"
  },
  {
    "path": "platformio.ini",
    "content": "; PlatformIO Project Configuration File\n;\n;   Build options: build flags, source filter\n;   Upload options: custom upload port, speed and extra flags\n;   Library options: dependencies, extra library storages\n;   Advanced options: extra scripting\n;\n; Please visit documentation for the other options and examples\n; https://docs.platformio.org/page/projectconf.html\n\n[platformio]\ndescription = ESP32 Sveltekit Template\ndata_dir = data\nextra_configs = \n\tfactory_settings.ini\n\tfeatures.ini\ndefault_envs = esp32-s3-devkitc-1\n\n[env]\nframework = arduino\nplatform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.32/platform-espressif32.zip\nbuild_flags = \n\t${factory_settings.build_flags}\n\t${features.build_flags}\n    -D BUILD_TARGET=\\\"$PIOENV\\\"\n    -D APP_NAME=\\\"ESP32-Sveltekit\\\" ; Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename\n    -D APP_VERSION=\\\"0.6.0\\\" ; semver compatible version string\n\n    ; Move all networking stuff to the protocol core 0 and leave business logic on application core 1\n    -D ESP32SVELTEKIT_RUNNING_CORE=0\n\n    ; Uncomment EMBED_WWW to embed the WWW data in the firmware binary\n    -D EMBED_WWW\n\n    ; Uncomment to configure Cross-Origin Resource Sharing\n    ; -D ENABLE_CORS\n    ; -D CORS_ORIGIN=\\\"*\\\"\n\n    ; Uncomment to enable informations from ESP32-Sveltekit in Serial Monitor\n    -D SERIAL_INFO\n\n    ; Uncomment to skip SSL certificate verification when downloading firmware updates\n    -D DOWNLOAD_OTA_SKIP_CERT_VERIFY\n    \n    ; D E B U G   B U I L D F L A G S\n    ; ===============================\n    ; These build flags are only for debugging purposes and should not be used in production\n    -D CONFIG_ARDUHAL_LOG_COLORS\n\n\t; Uncomment to show log messages from the ESP Arduino Core and ESP32-SvelteKit\n\t-D CORE_DEBUG_LEVEL=4\n\n    ; Serve config files from flash and access at /config/filename.json\n    ;-D SERVE_CONFIG_FILES\n\n    ; Uncomment to teleplot all task high watermarks to Serial\n    ; -D TELEPLOT_TASKS\n\n    ; Uncomment to use JSON instead of MessagePack for event messages. Default is MessagePack.\n    ; -D EVENT_USE_JSON=1 \n    \nlib_compat_mode = strict\n\n; Uncomment to include the a Root CA SSL Certificate Bundle for all SSL needs\n; Needs -D FT_DOWNLOAD_FIRMWARE=1 and -D FT_NTP=1\nboard_build.embed_files = src/certs/x509_crt_bundle.bin\n; Source for SSL Cert Store can bei either downloaded from Mozilla with 'mozilla' ('https://curl.se/ca/cacert.pem')\n; or from a curated Adafruit repository with 'adafruit' (https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-filtered.pem)\n; or complied from a 'folder' full of *.pem / *.dem files stored in the ./ssl_certs folder\n;board_ssl_cert_source = mozilla\nboard_ssl_cert_source = adafruit\n\nmonitor_speed = 115200\nmonitor_filters = \n\tesp32_exception_decoder\n    log2file\nboard_build.filesystem = littlefs\nextra_scripts = \n    pre:scripts/build_interface.py\n    pre:scripts/generate_cert_bundle.py\n    scripts/merge_bin.py\n    scripts/rename_fw.py\n    scripts/save_elf.py\nlib_deps = \n\tArduinoJson@>=7.0.0\n    elims/PsychicMqttClient@^0.2.4\n\n[env:esp32-c3-devkitm-1]\nboard = esp32-c3-devkitm-1\nboard_build.mcu = esp32c3\n; Uncomment min_spiffs.csv setting if using EMBED_WWW with ESP32\nboard_build.partitions = min_spiffs.csv\n; Use USB CDC for firmware upload and serial terminal\n; board_upload.before_reset = usb_reset\n; build_flags = \n;     ${env.build_flags}\n;     -DARDUINO_USB_CDC_ON_BOOT=1\n;     -DARDUINO_USB_MODE=1\n\n[env:esp32-s3-devkitc-1]\nboard = esp32-s3-devkitc-1\nboard_build.mcu = esp32s3\nboard_build.partitions = default_8MB.csv\n; Use USB CDC for firmware upload and serial terminal\n; board_upload.before_reset = usb_reset\nbuild_flags = \n   ${env.build_flags}\n   -DARDUINO_USB_CDC_ON_BOOT=1\n   -DARDUINO_USB_MODE=1\n\n[env:esp32dev]\n; Works for nodemcu-32s, devkit-v1 boards and probably others. You can change the pin defines below if needed.\nboard = esp32dev\nboard_build.partitions = min_spiffs.csv\nbuild_flags =\n    ${env.build_flags}\n    -D LED_BUILTIN=2\n    -D KEY_BUILTIN=0\n\n[env:Kincony-B16M]\n; Works for the Kincony B16M smart home controller with Ethernet support\nboard = esp32-s3-devkitc-1\nboard_build.partitions = default_16MB.csv\nboard_upload.before_reset = usb-reset\nbuild_flags = \n    ${env.build_flags}\n    -DARDUINO_USB_CDC_ON_BOOT=1\n    -DARDUINO_USB_MODE=1\n    -DFT_ETHERNET=1\n    -DUSE_TWO_ETH_PORTS=0\n    -DETH_PHY_TYPE=ETH_PHY_W5500\n    -DETH_PHY_ADDR=1\n    -DETH_PHY_CS=41\n    -DETH_PHY_IRQ=2 ; -1 if you won't wire\n    -DETH_PHY_RST=1 ; -1 if you won't wire\n    -DETH_SPI_SCK=42\n    -DETH_SPI_MISO=44\n    -DETH_SPI_MOSI=43\n\n[env:esp32-wt32-eth01]\n; Works for the WT32-ETH01 board with Ethernet support\nboard = wt32-eth01\nboard_build.partitions = min_spiffs.csv\nbuild_flags =\n    ${env.build_flags}\n    ; Not an LED but an unused GPIO pin\n    -DLED_BUILTIN=14\n    -DKEY_BUILTIN=0\n    -DFT_ETHERNET=1\n; Only use the filtered Adafruit SSL cert bundle as the full Mozilla bundle is too large for this board with the whole demo app\nboard_ssl_cert_source = adafruit\n\n\n"
  },
  {
    "path": "scripts/build_interface.py",
    "content": "#   ESP32 SvelteKit --\n#\n#   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n#   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n#   https://github.com/theelims/ESP32-sveltekit\n#\n#   Copyright (C) 2018 - 2023 rjwats\n#   Copyright (C) 2023 - 2024 theelims\n#   Copyright (C) 2023 Maxtrium B.V. [ code available under dual license ]\n#   Copyright (C) 2024 runeharlyk\n#   Copyright (C) 2025 hmbacher\n#\n#   All Rights Reserved. This software may be modified and distributed under\n#   the terms of the LGPL v3 license. See the LICENSE file for details.\n\nfrom pathlib import Path\nfrom shutil import copytree, rmtree, copyfileobj\nfrom os.path import exists, getmtime\nimport os\nimport sys\nimport gzip\nimport mimetypes\nimport glob\nfrom datetime import datetime\n\n# Import shared prebuild utilities\nfrom prebuild_utils import is_build_task\n\n# Check if this script should run\nif not is_build_task(['build', 'upload', 'buildfs', 'erase_upload']):\n    # Skip script execution for all other tasks\n    print(\"Skipping interface build for non-build task.\")\n    sys.exit(0)\n\nImport(\"env\")\n\nproject_dir = env[\"PROJECT_DIR\"]\nbuildFlags = env.ParseFlags(env[\"BUILD_FLAGS\"])\n\ninterface_dir = project_dir + \"/interface\"\noutput_file = project_dir + \"/lib/framework/WWWData.h\"\nsource_www_dir = interface_dir + \"/src\"\nbuild_dir = interface_dir + \"/build\"\nfilesystem_dir = project_dir + \"/data/www\"\n\n\ndef find_latest_timestamp_for_app():\n    return max(\n        (getmtime(f) for f in glob.glob(f\"{source_www_dir}/**/*\", recursive=True))\n    )\n\n\ndef should_regenerate_output_file():\n    if not flag_exists(\"EMBED_WWW\") or not exists(output_file):\n        return True\n    last_source_change = find_latest_timestamp_for_app()\n    last_build = getmtime(output_file)\n\n    print(\n        f\"Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}\"\n    )\n\n    return last_build < last_source_change\n\n\ndef gzip_file(file):\n    with open(file, 'rb') as f_in:\n        with gzip.open(file + '.gz', 'wb') as f_out:\n            copyfileobj(f_in, f_out)\n    os.remove(file)\n\n\ndef flag_exists(flag):\n    for define in buildFlags.get(\"CPPDEFINES\"):\n        if (define == flag or (isinstance(define, list) and define[0] == flag)):\n            return True\n    return False\n\n\ndef get_package_manager():\n    if exists(os.path.join(interface_dir, \"pnpm-lock.yaml\")):\n        return \"pnpm\"\n    if exists(os.path.join(interface_dir, \"yarn.lock\")):\n        return \"yarn\"\n    else:\n        return \"npm\"\n\n\ndef build_webapp():\n    package_manager = get_package_manager()\n    print(f\"Building interface with {package_manager}\")\n    os.chdir(interface_dir)\n    env.Execute(f\"{package_manager} install\")\n    env.Execute(f\"{package_manager} run build\")\n    os.chdir(\"..\")\n\n\ndef embed_webapp():\n    if flag_exists(\"EMBED_WWW\"):\n        print(\"Converting interface to PROGMEM\")\n        build_progmem()\n        return\n    add_app_to_filesystem()\n\n\ndef build_progmem():\n    mimetypes.init()\n    with open(output_file, \"w\") as progmem:\n        progmem.write(\"#include <functional>\\n\")\n        progmem.write(\"#include <Arduino.h>\\n\")\n\n        assetMap = {}\n\n        for idx, path in enumerate(Path(build_dir).rglob(\"*.*\")):\n            asset_path = path.relative_to(build_dir).as_posix()\n            asset_mime = (\n                mimetypes.guess_type(asset_path)[0] or \"application/octet-stream\"\n            )\n            print(f\"Converting {asset_path}\")\n\n            asset_var = f\"ESP_SVELTEKIT_DATA_{idx}\"\n            progmem.write(f\"// {asset_path}\\n\")\n            progmem.write(f\"const uint8_t {asset_var}[] = {{\\n\\t\")\n            file_data = gzip.compress(path.read_bytes())\n\n            for i, byte in enumerate(file_data):\n                if i and not (i % 16):\n                    progmem.write(\"\\n\\t\")\n                progmem.write(f\"0x{byte:02X},\")\n\n            progmem.write(\"\\n};\\n\\n\")\n            assetMap[asset_path] = {\n                \"name\": asset_var,\n                \"mime\": asset_mime,\n                \"size\": len(file_data),\n            }\n\n        progmem.write(\n            \"typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;\\n\\n\"\n        )\n        progmem.write(\"class WWWData {\\n\")\n        progmem.write(\"\\tpublic:\\n\")\n        progmem.write(\n            \"\\t\\tstatic void registerRoutes(RouteRegistrationHandler handler) {\\n\"\n        )\n\n        for asset_path, asset in assetMap.items():\n            progmem.write(\n                f'\\t\\t\\thandler(\"/{asset_path}\", \"{asset[\"mime\"]}\", {asset[\"name\"]}, {asset[\"size\"]});\\n'\n            )\n\n        progmem.write(\"\\t\\t}\\n\")\n        progmem.write(\"};\\n\\n\")\n\n\ndef add_app_to_filesystem():\n    build_path = Path(build_dir)\n    www_path = Path(filesystem_dir)\n    if www_path.exists() and www_path.is_dir():\n        rmtree(www_path)\n    print(\"Copying and compress interface to data directory\")\n    copytree(build_path, www_path)\n    for current_path, _, files in os.walk(www_path):\n        for file in files:\n            gzip_file(os.path.join(current_path, file))\n    if (\"upload\" in BUILD_TARGETS):\n        print(\"Build LittleFS file system image and upload to ESP32\")\n        env.Execute(\"pio run --target uploadfs\")\n\n\nprint(\"running: build_interface.py\")\nif should_regenerate_output_file():\n    build_webapp()\n    embed_webapp()\n"
  },
  {
    "path": "scripts/generate_cert_bundle.py",
    "content": "#!/usr/bin/env python\n#\n# modified ESP32 x509 certificate bundle generation utility to run with platformio\n#\n# Converts PEM and DER certificates to a custom bundle format which stores just the\n# subject name and public key to reduce space\n#\n# The bundle will have the format: number of certificates; crt 1 subject name length; crt 1 public key length;\n# crt 1 subject name; crt 1 public key; crt 2...\n#\n# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD\n# SPDX-License-Identifier: Apache-2.0\n\nfrom __future__ import with_statement\n\nfrom pathlib import Path\nimport os\nimport struct\nimport sys\nimport requests\nfrom io import open\n\n# Import shared prebuild utilities\nfrom prebuild_utils import is_build_task\n\n# Check if this script should run\nif not is_build_task(['build', 'upload', 'buildfs', 'erase_upload']):\n    # Skip script execution for all other tasks\n    print(\"Skipping certificate bundle generation for non-build task.\")\n    sys.exit(0)\n\nImport(\"env\")\n\ntry:\n    from cryptography import x509\n    from cryptography.hazmat.backends import default_backend\n    from cryptography.hazmat.primitives import serialization\nexcept ImportError:\n    env.Execute(\"$PYTHONEXE -m pip install cryptography\")\n\n\nca_bundle_bin_file = 'x509_crt_bundle.bin'\nmozilla_cacert_url = 'https://curl.se/ca/cacert.pem'\nadafruit_filtered_cacert_url = 'https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-filtered.pem'\nadafruit_full_cacert_url = 'https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-full.pem'\ncerts_dir = Path(\"./ssl_certs\")\nbinary_dir = Path(\"./src/certs\")\n\nquiet = False\n\ndef download_cacert_file(source):\n    if source == \"mozilla\":\n        response = requests.get(mozilla_cacert_url)\n    elif source == \"adafruit\":\n        response = requests.get(adafruit_filtered_cacert_url)\n    elif source == \"adafruit-full\":\n        response = requests.get(adafruit_full_cacert_url)\n    else:\n        raise InputError('Invalid certificate source')\n\n    if response.status_code == 200:\n\n        # Ensure the directory exists, create it if necessary\n        os.makedirs(certs_dir, exist_ok=True)\n\n        # Generate the full path to the output file\n        output_file = os.path.join(certs_dir, \"cacert.pem\")\n\n        # Write the certificate bundle to the output file with utf-8 encoding\n        with open(output_file, \"w\", encoding=\"utf-8\") as f:\n            f.write(response.text)\n\n        status('Certificate bundle downloaded to: %s' % output_file)\n    else:\n        status('Failed to fetch the certificate bundle.')\n\ndef status(msg):\n    \"\"\" Print status message to stderr \"\"\"\n    if not quiet:\n        critical(msg)\n\n\ndef critical(msg):\n    \"\"\" Print critical message to stderr \"\"\"\n    sys.stderr.write('SSL Cert Store: ')\n    sys.stderr.write(msg)\n    sys.stderr.write('\\n')\n\n\nclass CertificateBundle:\n    def __init__(self):\n        self.certificates = []\n        self.compressed_crts = []\n\n        if os.path.isfile(ca_bundle_bin_file):\n            os.remove(ca_bundle_bin_file)\n\n    def add_from_path(self, crts_path):\n\n        found = False\n        for file_path in os.listdir(crts_path):\n            found |= self.add_from_file(os.path.join(crts_path, file_path))\n\n        if found is False:\n            raise InputError('No valid x509 certificates found in %s' % crts_path)\n\n    def add_from_file(self, file_path):\n        try:\n            if file_path.endswith('.pem'):\n                status('Parsing certificates from %s' % file_path)\n                with open(file_path, 'r', encoding='utf-8') as f:\n                    crt_str = f.read()\n                    self.add_from_pem(crt_str)\n                    return True\n\n            elif file_path.endswith('.der'):\n                status('Parsing certificates from %s' % file_path)\n                with open(file_path, 'rb') as f:\n                    crt_str = f.read()\n                    self.add_from_der(crt_str)\n                    return True\n\n        except ValueError:\n            critical('Invalid certificate in %s' % file_path)\n            raise InputError('Invalid certificate')\n\n        return False\n\n    def add_from_pem(self, crt_str):\n        \"\"\" A single PEM file may have multiple certificates \"\"\"\n\n        crt = ''\n        count = 0\n        start = False\n\n        for strg in crt_str.splitlines(True):\n            if strg == '-----BEGIN CERTIFICATE-----\\n' and start is False:\n                crt = ''\n                start = True\n            elif strg == '-----END CERTIFICATE-----\\n' and start is True:\n                crt += strg + '\\n'\n                start = False\n                # 🌙 show warning\n                try:\n                    cert = x509.load_pem_x509_certificate(crt.encode(), default_backend())\n                    # Check serial number\n                    if cert.serial_number <= 0:\n                        status(f'Warning: Certificate {cert} has invalid serial number: {cert.serial_number}')\n                    self.certificates.append(cert)\n                    count += 1\n                except Exception as e:\n                    status(f'Failed to load certificate: {e}')\n            if start is True:\n                crt += strg\n\n        if count == 0:\n            raise InputError('No certificate found')\n\n        status('Successfully added %d certificates' % count)\n\n    def add_from_der(self, crt_str):\n        self.certificates.append(x509.load_der_x509_certificate(crt_str, default_backend()))\n        status('Successfully added 1 certificate')\n\n    def create_bundle(self):\n        # Sort certificates in order to do binary search when looking up certificates\n        self.certificates = sorted(self.certificates, key=lambda cert: cert.subject.public_bytes(default_backend()))\n\n        bundle = struct.pack('>H', len(self.certificates))\n\n        for crt in self.certificates:\n            \"\"\" Read the public key as DER format \"\"\"\n            pub_key = crt.public_key()\n            pub_key_der = pub_key.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)\n\n            \"\"\" Read the subject name as DER format \"\"\"\n            sub_name_der = crt.subject.public_bytes(default_backend())\n\n            name_len = len(sub_name_der)\n            key_len = len(pub_key_der)\n            len_data = struct.pack('>HH', name_len, key_len)\n\n            bundle += len_data\n            bundle += sub_name_der\n            bundle += pub_key_der\n\n        return bundle\n\nclass InputError(RuntimeError):\n    def __init__(self, e):\n        super(InputError, self).__init__(e)\n\n\ndef main():\n\n    bundle = CertificateBundle()\n\n    try:\n        cert_source = env.GetProjectOption(\"board_ssl_cert_source\")\n\n        if (cert_source == \"mozilla\" or cert_source == \"adafruit\"):\n            download_cacert_file(cert_source)\n            bundle.add_from_file(os.path.join(certs_dir, \"cacert.pem\"))\n        elif (cert_source == \"folder\"):\n            bundle.add_from_path(certs_dir)\n    except ValueError:\n        critical('Invalid configuration option: use \\'board_ssl_cert_source\\' parameter in platformio.ini' )\n        raise InputError('Invalid certificate')\n\n    status('Successfully added %d certificates in total' % len(bundle.certificates))\n\n    crt_bundle = bundle.create_bundle()\n\n    # Ensure the directory exists, create it if necessary\n    os.makedirs(binary_dir, exist_ok=True)\n\n    output_file = os.path.join(binary_dir, ca_bundle_bin_file)\n\n    with open(output_file, 'wb') as f:\n        f.write(crt_bundle)\n\n    status('Successfully created %s' % output_file)\n\n\ntry:\n    main()\nexcept InputError as e:\n    print(e)\n    sys.exit(2)\n"
  },
  {
    "path": "scripts/merge_bin.py",
    "content": "import os\nimport re\nImport(\"env\")\n\nAPP_BIN = \"$BUILD_DIR/${PROGNAME}.bin\"\nOUTPUT_DIR = \"build{}merged{}\".format(os.path.sep, os.path.sep)\n\nBOARD_CONFIG = env.BoardConfig()\n\ndef readFlag(flag):\n    buildFlags = env.ParseFlags(env[\"BUILD_FLAGS\"])\n    # print(buildFlags.get(\"CPPDEFINES\"))\n    for define in buildFlags.get(\"CPPDEFINES\"):\n        if (define == flag or (isinstance(define, list) and define[0] == flag)):\n            # print(\"Found \"+flag+\" = \"+define[1])\n            # strip quotes (\"\") from define[1]\n            cleanedFlag = re.sub(r'^\"|\"$', '', define[1])\n            return cleanedFlag\n    return None\n\n\ndef merge_bin(source, target, env):\n\n    # check if output directories exist and create if necessary\n    if not os.path.isdir(\"build\"):\n        os.mkdir(\"build\")\n\n    if not os.path.isdir(OUTPUT_DIR):\n        os.mkdir(OUTPUT_DIR)\n\n    MERGED_BIN = \"$PROJECT_DIR{}{}{}_{}_{}_webflash.bin\".format(os.path.sep, OUTPUT_DIR, readFlag(\"APP_NAME\"), env.get('PIOENV'), readFlag(\"APP_VERSION\").replace(\".\", \"-\"))\n\n    # The list contains all extra images (bootloader, partitions, eboot) and\n    # the final application binary\n    flash_images = env.Flatten(env.get(\"FLASH_EXTRA_IMAGES\", [])) + [\"$ESP32_APP_OFFSET\", APP_BIN]\n    flash_size = env.BoardConfig().get(\"upload.flash_size\", \"4MB\")\n    flash_freq = env.BoardConfig().get(\"build.f_flash\", \"40000000L\")\n    flash_freq = str(flash_freq).replace(\"L\", \"\")\n    flash_freq = str(int(int(flash_freq) / 1000000)) + \"m\"\n    flash_mode = env.BoardConfig().get(\"build.flash_mode\", \"dio\")\n    memory_type = env.BoardConfig().get(\"build.arduino.memory_type\", \"qio_qspi\")\n\n    if flash_mode == \"qio\" or flash_mode == \"qout\":\n        flash_mode = \"dio\"\n    if memory_type == \"opi_opi\" or memory_type == \"opi_qspi\":\n        flash_mode = \"dout\"\n\n    # Run esptool to merge images into a single binary\n    env.Execute(\n        \" \".join(\n            [\n                \"$PYTHONEXE\",\n                \"$OBJCOPY\",\n                \"--chip\",\n                BOARD_CONFIG.get(\"build.mcu\", \"esp32\"),\n                \"merge-bin\",\n                \"-o\",\n                MERGED_BIN,\n                \"--flash-mode\",\n                flash_mode,\n                \"--flash-freq\",\n                flash_freq,\n                \"--flash-size\",\n                flash_size    \n            ]\n            + flash_images\n        )\n    )\n\n# Add a post action that runs esptoolpy to merge available flash images\nenv.AddPostAction(APP_BIN , merge_bin)\n"
  },
  {
    "path": "scripts/prebuild_utils.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nShared utilities for PlatformIO pre-build scripts.\n\"\"\"\n\nimport os\nimport sys\n\n\ndef is_build_task(required_tasks):\n    \"\"\"\n    Check if the current PlatformIO task requires pre-build script execution.\n    \n    Args:\n        required_tasks: List of PlatformIO tasks that need the calling script\n    \n    Returns:\n        bool: True if script should run, False if it should be skipped\n    \"\"\"\n    # Get caller's filename for logging\n    try:\n        caller_frame = sys._getframe(1)\n        script_name = os.path.basename(caller_frame.f_code.co_filename)\n    except (AttributeError, ValueError):\n        script_name = os.path.basename(sys.argv[0]) if sys.argv else 'unknown_script'\n    \n    # Check command line arguments for build tasks\n    cmd_args = ' '.join(sys.argv).lower()\n    \n    # Map PlatformIO direct flags to our task names - only skip for truly non-build tasks\n    skip_flags = {\n        '--clean': 'clean',\n        '--erase': 'erase', \n        '--monitor': 'monitor'\n    }\n    \n    # Check for direct PlatformIO flags that should skip pre-build scripts\n    for flag, task_name in skip_flags.items():\n        if flag in cmd_args:\n            if task_name not in required_tasks:\n                print(f\"Skipping {script_name} for {task_name} task\")\n                return False\n    \n    # Check for --target format for non-build targets only\n    skip_targets = ['clean', 'erase', 'monitor', 'metrics-only']\n    has_skip_target = any(\n        f'--target {task}' in cmd_args or f'-t {task}' in cmd_args \n        for task in skip_targets\n        if task not in required_tasks\n    )\n    \n    if has_skip_target:\n        print(f\"Skipping {script_name} for non-build task\")\n        return False\n    \n    # Continue execution for all other tasks (including size, metrics, etc. that may need building)\n    return True"
  },
  {
    "path": "scripts/rename_fw.py",
    "content": "\"\"\" \n    EMS-ESP - https://github.com/emsesp/EMS-ESP\n    Copyright 2020-2023  Paul Derbyshire\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>. \n\n\"\"\"\n\nimport shutil\nimport re\nimport os\nImport(\"env\")\nimport hashlib\n\n\nOUTPUT_DIR = \"build{}release{}\".format(os.path.sep, os.path.sep)\n\ndef readFlag(flag):\n    buildFlags = env.ParseFlags(env[\"BUILD_FLAGS\"])\n    # print(buildFlags.get(\"CPPDEFINES\"))\n    for define in buildFlags.get(\"CPPDEFINES\"):\n        if (define == flag or (isinstance(define, list) and define[0] == flag)):\n            # print(\"Found \"+flag+\" = \"+define[1])\n            # strip quotes (\"\") from define[1]\n            cleanedFlag = re.sub(r'^\"|\"$', '', define[1])\n            return cleanedFlag\n    return None\n\n\ndef bin_copy(source, target, env):\n\n    # get the build info\n    app_version = readFlag(\"APP_VERSION\")\n    app_name = readFlag(\"APP_NAME\")\n    build_target = env.get('PIOENV')\n\n    # print information's\n    print(\"App Version: \" + app_version)\n    print(\"App Name: \" + app_name)\n    print(\"Build Target: \" + build_target)\n\n    # convert . to - so Windows doesn't complain\n    variant = app_name + \"_\" +  build_target + \"_\" + app_version.replace(\".\", \"-\")\n\n    # check if output directories exist and create if necessary\n    if not os.path.isdir(OUTPUT_DIR):\n        os.mkdir(OUTPUT_DIR)\n\n    # create string with location and file names based on variant\n    bin_file = \"{}{}.bin\".format(OUTPUT_DIR, variant)\n    md5_file = \"{}{}.md5\".format(OUTPUT_DIR, variant)\n\n    # check if new target files exist and remove if necessary\n    for f in [bin_file]:\n        if os.path.isfile(f):\n            os.remove(f)\n\n    # check if new target files exist and remove if necessary\n    for f in [md5_file]:\n        if os.path.isfile(f):\n            os.remove(f)\n\n    print(\"Renaming file to \"+bin_file)\n\n    # copy firmware.bin to firmware/<variant>.bin\n    shutil.copy(str(target[0]), bin_file)\n\n    with open(bin_file,\"rb\") as f:\n        result = hashlib.md5(f.read())\n        print(\"Calculating MD5: \"+result.hexdigest())\n        file1 = open(md5_file, 'w')\n        file1.write(result.hexdigest())\n        file1.close()\n\nenv.AddPostAction(\"$BUILD_DIR/${PROGNAME}.bin\", [bin_copy])\nenv.AddPostAction(\"$BUILD_DIR/${PROGNAME}.md5\", [bin_copy])\n"
  },
  {
    "path": "scripts/save_elf.py",
    "content": "import shutil\nimport re\nimport os\nImport(\"env\")\nimport hashlib\n\n\nOUTPUT_DIR = \"build{}elf{}\".format(os.path.sep,os.path.sep)\n\ndef elf_copy(source, target, env):\n    # check if output directories exist and create if necessary\n    if not os.path.isdir(\"build\"):\n        os.mkdir(\"build\")\n\n    if not os.path.isdir(OUTPUT_DIR):\n        os.mkdir(OUTPUT_DIR)\n\n    with open(str(target[0]),\"rb\") as f:\n        result = hashlib.sha256(f.read())\n        \n        # create string with location and file names based on variant\n        elf_file = \"{}{}.elf\".format(OUTPUT_DIR, result.hexdigest())\n\n        # check if new target files exist and remove if necessary\n        for f in [elf_file]:\n            if os.path.isfile(f):\n                os.remove(f)\n\n        print(\"Saving ELF file to \"+elf_file)\n\n        # copy firmware.bin to firmware/<variant>.bin\n        shutil.copy(str(target[0]), elf_file)\n        \nenv.AddPostAction(\"$BUILD_DIR/${PROGNAME}.elf\", [elf_copy])"
  },
  {
    "path": "src/LightMqttSettingsService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <LightMqttSettingsService.h>\n\nLightMqttSettingsService::LightMqttSettingsService(PsychicHttpServer *server,\n                                                   ESP32SvelteKit *sveltekit) : _httpEndpoint(LightMqttSettings::read,\n                                                                                              LightMqttSettings::update,\n                                                                                              this,\n                                                                                              server,\n                                                                                              LIGHT_BROKER_SETTINGS_PATH,\n                                                                                              sveltekit->getSecurityManager(),\n                                                                                              AuthenticationPredicates::IS_AUTHENTICATED),\n                                                                                _fsPersistence(LightMqttSettings::read,\n                                                                                               LightMqttSettings::update,\n                                                                                               this,\n                                                                                               sveltekit->getFS(),\n                                                                                               LIGHT_BROKER_SETTINGS_FILE),\n                                                                                _mqttSettingsService(sveltekit->getMqttSettingsService())\n{\n    // configure settings service update handler to update LED state\n    addUpdateHandler([&](const String &originId)\n                     { onConfigUpdated(); },\n                     false);\n}\n\nvoid LightMqttSettingsService::begin()\n{\n    _httpEndpoint.begin();\n    _fsPersistence.readFromFS();\n}\n\nvoid LightMqttSettingsService::onConfigUpdated()\n{\n    // Notify the MQTT client about the updated configuration\n    _mqttSettingsService->setStatusTopic(_state.stateTopic);\n\n    // Optionally, you can also log or handle the updated configuration here\n    ESP_LOGI(LIGHT_TAG, \"MQTT Configuration updated\");\n}\n"
  },
  {
    "path": "src/LightMqttSettingsService.h",
    "content": "#ifndef LightMqttSettingsService_h\n#define LightMqttSettingsService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <HttpEndpoint.h>\n#include <FSPersistence.h>\n#include <SettingValue.h>\n#include <ESP32SvelteKit.h>\n#include <MqttSettingsService.h>\n\n#define LIGHT_TAG \"💡\"\n\n#define LIGHT_BROKER_SETTINGS_FILE \"/config/brokerSettings.json\"\n#define LIGHT_BROKER_SETTINGS_PATH \"/rest/brokerSettings\"\n\n#ifndef FACTORY_MQTT_STATUS_TOPIC\n#define FACTORY_MQTT_STATUS_TOPIC \"esp32sveltekit/status\"\n#endif // end FACTORY_MQTT_STATUS_TOPIC\n\nclass LightMqttSettings\n{\npublic:\n    String mqttPath;\n    String name;\n    String uniqueId;\n    String stateTopic;\n\n    static void read(LightMqttSettings &settings, JsonObject &root)\n    {\n        root[\"mqtt_path\"] = settings.mqttPath;\n        root[\"name\"] = settings.name;\n        root[\"unique_id\"] = settings.uniqueId;\n        root[\"status_topic\"] = settings.stateTopic;\n    }\n\n    static StateUpdateResult update(JsonObject &root, LightMqttSettings &settings, const String& originID)\n    {\n        settings.mqttPath = root[\"mqtt_path\"] | SettingValue::format(\"homeassistant/light/#{unique_id}\");\n        settings.name = root[\"name\"] | SettingValue::format(\"light-#{unique_id}\");\n        settings.uniqueId = root[\"unique_id\"] | SettingValue::format(\"light-#{unique_id}\");\n        settings.stateTopic = root[\"status_topic\"] | SettingValue::format(FACTORY_MQTT_STATUS_TOPIC);\n        return StateUpdateResult::CHANGED;\n    }\n};\n\nclass LightMqttSettingsService : public StatefulService<LightMqttSettings>\n{\npublic:\n    LightMqttSettingsService(PsychicHttpServer *server, ESP32SvelteKit *sveltekit);\n    void begin();\n    void onConfigUpdated();\n\nprivate:\n    HttpEndpoint<LightMqttSettings> _httpEndpoint;\n    FSPersistence<LightMqttSettings> _fsPersistence;\n    MqttSettingsService *_mqttSettingsService;\n};\n\n#endif // end LightMqttSettingsService_h\n"
  },
  {
    "path": "src/LightStateService.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <LightStateService.h>\n\nLightStateService::LightStateService(PsychicHttpServer *server,\n                                     ESP32SvelteKit *sveltekit,\n                                     LightMqttSettingsService *lightMqttSettingsService) : _httpEndpoint(LightState::read,\n                                                                                                         LightState::update,\n                                                                                                         this,\n                                                                                                         server,\n                                                                                                         LIGHT_SETTINGS_ENDPOINT_PATH,\n                                                                                                         sveltekit->getSecurityManager(),\n                                                                                                         AuthenticationPredicates::IS_AUTHENTICATED),\n                                                                                           _eventEndpoint(LightState::read,\n                                                                                                          LightState::update,\n                                                                                                          this,\n                                                                                                          sveltekit->getSocket(),\n                                                                                                          LIGHT_SETTINGS_EVENT),\n                                                                                           _mqttEndpoint(LightState::homeAssistRead,\n                                                                                                         LightState::homeAssistUpdate,\n                                                                                                         this,\n                                                                                                         sveltekit->getMqttClient()),\n                                                                                           _webSocketServer(LightState::read,\n                                                                                                            LightState::update,\n                                                                                                            this,\n                                                                                                            server,\n                                                                                                            LIGHT_SETTINGS_SOCKET_PATH,\n                                                                                                            sveltekit->getSecurityManager(),\n                                                                                                            AuthenticationPredicates::IS_AUTHENTICATED),\n                                                                                           _mqttClient(sveltekit->getMqttClient()),\n                                                                                           _lightMqttSettingsService(lightMqttSettingsService)\n{\n    // configure led to be output\n    pinMode(LED_BUILTIN, OUTPUT);\n\n    // configure MQTT callback\n    _mqttClient->onConnect(std::bind(&LightStateService::registerConfig, this));\n\n    // configure update handler for when the light settings change\n    _lightMqttSettingsService->addUpdateHandler([&](const String &originId)\n                                                { registerConfig(); },\n                                                false);\n\n    // configure settings service update handler to update LED state\n    addUpdateHandler([&](const String &originId)\n                     { onConfigUpdated(); },\n                     false);\n}\n\nvoid LightStateService::begin()\n{\n    _httpEndpoint.begin();\n    _eventEndpoint.begin();\n    _state.ledOn = DEFAULT_LED_STATE;\n    onConfigUpdated();\n}\n\nvoid LightStateService::onConfigUpdated()\n{\n    digitalWrite(LED_BUILTIN, _state.ledOn ? 1 : 0);\n}\n\nvoid LightStateService::registerConfig()\n{\n    if (!_mqttClient->connected())\n    {\n        return;\n    }\n    String configTopic;\n    String subTopic;\n    String pubTopic;\n\n    JsonDocument doc;\n    _lightMqttSettingsService->read([&](LightMqttSettings &settings)\n                                    {\n    configTopic = settings.mqttPath + \"/config\";\n    subTopic = settings.mqttPath + \"/set\";\n    pubTopic = settings.mqttPath + \"/state\";\n    doc[\"~\"] = settings.mqttPath;\n    doc[\"name\"] = settings.name;\n    doc[\"unique_id\"] = settings.uniqueId; });\n    doc[\"cmd_t\"] = \"~/set\";\n    doc[\"stat_t\"] = \"~/state\";\n    doc[\"schema\"] = \"json\";\n    doc[\"brightness\"] = false;\n\n    String payload;\n    serializeJson(doc, payload);\n    _mqttClient->publish(configTopic.c_str(), 0, false, payload.c_str());\n\n    _mqttEndpoint.configureTopics(pubTopic, subTopic);\n}\n"
  },
  {
    "path": "src/LightStateService.h",
    "content": "#ifndef LightStateService_h\n#define LightStateService_h\n\n/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <LightMqttSettingsService.h>\n\n#include <EventSocket.h>\n#include <HttpEndpoint.h>\n#include <MqttEndpoint.h>\n#include <EventEndpoint.h>\n#include <WebSocketServer.h>\n#include <ESP32SvelteKit.h>\n\n#define DEFAULT_LED_STATE false\n#define OFF_STATE \"OFF\"\n#define ON_STATE \"ON\"\n\n#define LIGHT_SETTINGS_ENDPOINT_PATH \"/rest/lightState\"\n#define LIGHT_SETTINGS_SOCKET_PATH \"/ws/lightState\"\n#define LIGHT_SETTINGS_EVENT \"led\"\n\nclass LightState\n{\npublic:\n    bool ledOn;\n\n    static void read(LightState &settings, JsonObject &root)\n    {\n        root[\"led_on\"] = settings.ledOn;\n    }\n\n    static StateUpdateResult update(JsonObject &root, LightState &lightState, const String& originID)\n    {\n        boolean newState = root[\"led_on\"] | DEFAULT_LED_STATE;\n        if (lightState.ledOn != newState)\n        {\n            lightState.ledOn = newState;\n            return StateUpdateResult::CHANGED;\n        }\n        return StateUpdateResult::UNCHANGED;\n    }\n\n    static void homeAssistRead(LightState &settings, JsonObject &root)\n    {\n        root[\"state\"] = settings.ledOn ? ON_STATE : OFF_STATE;\n    }\n\n    static StateUpdateResult homeAssistUpdate(JsonObject &root, LightState &lightState, const String& originID)\n    {\n        String state = root[\"state\"];\n        // parse new led state\n        boolean newState = false;\n        if (state.equals(ON_STATE))\n        {\n            newState = true;\n        }\n        else if (!state.equals(OFF_STATE))\n        {\n            return StateUpdateResult::ERROR;\n        }\n        // change the new state, if required\n        if (lightState.ledOn != newState)\n        {\n            lightState.ledOn = newState;\n            return StateUpdateResult::CHANGED;\n        }\n        return StateUpdateResult::UNCHANGED;\n    }\n};\n\nclass LightStateService : public StatefulService<LightState>\n{\npublic:\n    LightStateService(PsychicHttpServer *server,\n                      ESP32SvelteKit *sveltekit,\n                      LightMqttSettingsService *lightMqttSettingsService);\n\n    void begin();\n\nprivate:\n    HttpEndpoint<LightState> _httpEndpoint;\n    EventEndpoint<LightState> _eventEndpoint;\n    MqttEndpoint<LightState> _mqttEndpoint;\n    WebSocketServer<LightState> _webSocketServer;\n    PsychicMqttClient *_mqttClient;\n    LightMqttSettingsService *_lightMqttSettingsService;\n\n    void registerConfig();\n    void onConfigUpdated();\n};\n\n#endif\n"
  },
  {
    "path": "src/main.cpp",
    "content": "/**\n *   ESP32 SvelteKit\n *\n *   A simple, secure and extensible framework for IoT projects for ESP32 platforms\n *   with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.\n *   https://github.com/theelims/ESP32-sveltekit\n *\n *   Copyright (C) 2018 - 2023 rjwats\n *   Copyright (C) 2023 - 2025 theelims\n *\n *   All Rights Reserved. This software may be modified and distributed under\n *   the terms of the LGPL v3 license. See the LICENSE file for details.\n **/\n\n#include <ESP32SvelteKit.h>\n#include <LightMqttSettingsService.h>\n#include <LightStateService.h>\n#include <PsychicHttpServer.h>\n\n#define SERIAL_BAUD_RATE 115200\n\nPsychicHttpServer server;\n\nESP32SvelteKit esp32sveltekit(&server, 70);\n\nLightMqttSettingsService lightMqttSettingsService = LightMqttSettingsService(&server,\n                                                                             &esp32sveltekit);\n\nLightStateService lightStateService = LightStateService(&server,\n                                                        &esp32sveltekit,\n                                                        &lightMqttSettingsService);\n\nvoid setup()\n{\n    // start serial and filesystem\n    Serial.begin(SERIAL_BAUD_RATE);\n\n    // start ESP32-SvelteKit\n    esp32sveltekit.begin();\n\n    // load the initial light settings\n    lightStateService.begin();\n    // start the light service\n    lightMqttSettingsService.begin();\n}\n\nvoid loop()\n{\n    // Delete Arduino loop task, as it is not needed in this example\n    vTaskDelete(NULL);\n}\n"
  },
  {
    "path": "ssl_certs/DigiCert_Global_Root_CA.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\nQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\nCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\nnh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\nT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\ngdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\nTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\nDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\nhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\nPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\nYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\nCAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n-----END CERTIFICATE-----\n"
  }
]