commit 3e191a4f606abfb46293a41076882f7dcbc93d40 Author: Jean Jacques Avril Date: Mon Oct 6 18:27:50 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..07d045c --- /dev/null +++ b/README.md @@ -0,0 +1,304 @@ +# RFID-Master-Client + +A comprehensive RFID access control system based on ESP32, providing a web UI for management, OTA updates, and synchronization with an external server. + +## Overview + +The RFID-Master-Client project is an embedded system for access control using RFID cards. It combines hardware control with a modern web interface, enabling management of users, settings, and logs through a user-friendly interface. + +## Features + +- **RFID Access Control**: Management of RFID cards and access logging +- **Web UI**: Modern SvelteKit-based user interface for configuration and monitoring +- **WiFi Management**: Support for Station and Access Point modes +- **OTA Updates**: Over-the-Air firmware updates for easy maintenance +- **Server Synchronization**: Automatic data synchronization with an external server via RPC +- **Hardware Configuration**: Customizable hardware settings (relays, sensors, pins) +- **Logging and Debugging**: Comprehensive logging system with various levels +- **TFT Display**: Local display for status and interaction +- **Protobuf Communication**: Efficient serial communication between components + +## Architecture + +The system consists of several modules: + +### Backend (C++ on ESP32) + +- **Core Modules**: Logger, Settings, Hardware Configuration +- **RFID Module**: Database and processing of RFID cards +- **WebUI Module**: HTTP server for the web interface +- **WiFi Manager**: Network management +- **OTA Manager**: Firmware updates +- **RPC Module**: Communication with external server +- **Device Manager**: Hardware control (relays, LEDs) + +### Frontend (SvelteKit) + +- **Dashboard**: Overview of system status +- **User Management**: RFID cards and permissions +- **Settings**: System and hardware configuration +- **Logs**: Display of system logs + +### Communication + +- **Protobuf**: Binary serial communication between ESP32 and connected sensors +- **HTTP/REST**: Web API for the user interface +- **WebSocket**: Real-time updates in the web UI + +## Prerequisites + +### Hardware + +- ESP32 development board (e.g., AZ-Delivery-DevKit-V4) +- RFID reader (MFRC522 or compatible) +- TFT display (ST7735/ST7789) +- Relay module for access control +- RTC module for timestamps + +### Software + +- PlatformIO (for ESP32 development) +- Node.js 18+ (for web UI) +- protoc (Protocol Buffers Compiler) + +## Installation + +1. **Clone the repository**: + + ```bash + git clone + cd RFID-Master-Client + ``` + +2. **Install PlatformIO dependencies**: + + ```bash + pio pkg install + ``` + +3. **Install web UI dependencies**: + + ```bash + cd ui + npm install + cd .. + ``` + +4. **Generate protobuf files**: + ```bash + cd ui + npm run proto + cd .. + ``` + +## Build and Flash + +### Development Environment + +```bash +pio run -e dev +pio run -e dev -t upload +pio run -e dev -t monitor +``` + +### Production Environment + +```bash +pio run -e prod +pio run -e prod -t upload +``` + +### Build Web UI + +```bash +cd ui +npm run build +npm run copy +cd .. +``` + +## Configuration + +### Hardware Configuration + +Edit `lib/data/hardware_config.hpp` for pin assignments and hardware parameters. + +### System Settings + +Configure via the web UI: + +- WiFi settings +- Server synchronization +- Log level +- Hardware parameters + +### Partitioning + +- `partitions_custom.csv`: For development (more memory for debugging) +- `partitions_prod.csv`: For production (optimized for size) + +## Usage + +1. **Initial Configuration**: + + - Start device in AP mode + - Connect to WiFi "RFID-Master" + - Open web UI at `http://192.168.4.1` + - Configure WiFi and server settings + +2. **Add RFID Cards**: + + - Navigate to "Users" in web UI + - Scan new RFID card or enter manually + - Assign permissions + +3. **Monitoring**: + - Dashboard for system status + - Logs for debugging + - View access logs + +## API Reference + +### REST API Endpoints + +- `GET /api/status`: Retrieve device status +- `GET /api/users`: Retrieve user list +- `POST /api/users`: Add new user +- `GET /api/logs`: Retrieve system logs +- `POST /api/settings`: Update settings + +### Protobuf Messages + +- `device.proto`: Device information and status +- `webui.proto`: Web UI communication +- `control_communication.proto`: Control messages +- `settings.proto`: Configuration settings +- `hardware.proto`: Hardware configuration + +## Development + +### Code Structure + +``` +src/ +├── main.cpp # Main entry point +├── webui/ # Web server and API +├── wifi/ # WiFi management +├── device/ # Device management +lib/ +├── data/ # Data models and configuration +├── hardware/ # Hardware abstractions +├── log/ # Logging system +├── ota/ # OTA updates +├── rpc/ # Server communication +├── tft/ # Display control +ui/ # Web UI (SvelteKit) +proto/ # Protocol Buffer definitions +``` + +### Debugging + +- Use `pio run -e dev -t monitor` for serial logs +- Adjust log level via web UI +- Core dumps are enabled in dev environment + +### Tests + +```bash +# Unit tests (if implemented) +pio test + +# Web UI tests +cd ui +npm run check +``` + +## Deployment + +1. **Build firmware**: + + ```bash + pio run -e prod + ``` + +2. **Build and copy web UI**: + + ```bash + cd ui + npm run build + npm run copy + cd .. + ``` + +3. **Flash**: + + ```bash + pio run -e prod -t upload + ``` + +4. **Flash filesystem** (for web UI): + ```bash + pio run -e prod -t uploadfs + ``` + +## Troubleshooting + +### Common Issues + +- **WiFi connection failed**: Check SSID/password, verify signal strength +- **RFID not recognized**: Check hardware connections, test MFRC522 module +- **Web UI unreachable**: Check firewall settings, verify IP address +- **OTA update failed**: Check network connection, verify firmware size + +### Analyze Logs + +- Set log level to DEBUG for detailed output +- Use serial monitor for real-time logs +- Web UI logs for HTTP requests + +### Hardware Debugging + +- Verify pin assignments in `hardware_config.hpp` +- Ensure proper power supply to modules +- Test I2C/serial connections + +## Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +### Code Standards + +- Use C++17 for ESP32 code +- TypeScript for web UI +- Protobuf for serial communication +- Document new features and API changes + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Support + +For questions or issues: + +- Open an issue in the GitHub repository +- Check logs for error indications +- Ensure all prerequisites are met + +## Changelog + +### [Unreleased] + +- Initial release of the RFID-Master-Client system +- Complete RFID access control +- Web UI with SvelteKit +- OTA update functionality +- Server synchronization + +--- + +For detailed information about specific modules, see the corresponding README files in the subdirectories. diff --git a/data/web/index.html.gz b/data/web/index.html.gz new file mode 100644 index 0000000..64abdf2 Binary files /dev/null and b/data/web/index.html.gz differ diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/data/hardware_config.cpp b/lib/data/hardware_config.cpp new file mode 100644 index 0000000..d33357c --- /dev/null +++ b/lib/data/hardware_config.cpp @@ -0,0 +1,131 @@ +#include "hardware_config.hpp" +#include +#include +#include +#include + +HardwareConfigRepo::HardwareConfigRepo() { + setDefaults(); +} + +HardwareConfigRepo::~HardwareConfigRepo() { + // Cleanup if needed +} + +void HardwareConfigRepo::setChangeCallback(std::function callback) { + changeCallback_ = callback; +} + +void HardwareConfigRepo::setDefaults() { + data_.hold_duration_ms = DEFAULT_HOLD_DURATION_MS; + data_.override = DEFAULT_OVERRIDE; + data_.relay_pin = DEFAULT_RELAY_PIN; + data_.sensor_rx_pin = DEFAULT_SENSOR_RX_PIN; + data_.sensor_tx_pin = DEFAULT_SENSOR_TX_PIN; + + // Initialize LedConfig to defaults (empty) + // Nanopb initializes to zero, so ok +} + +bool HardwareConfigRepo::fileExists() { + return LittleFS.exists(filename_); +} + +bool HardwareConfigRepo::load() { + if (!LittleFS.begin()) { + LOG_ERROR("HardwareConfigRepo: LittleFS mount failed"); + setDefaults(); + return false; + } + + if (!fileExists()) { + // File doesn't exist, use defaults + setDefaults(); + runCallback(); + return true; + } + + File file = LittleFS.open(filename_, "r"); + if (!file) { + LOG_ERROR("Failed to open hardware config file for reading"); + setDefaults(); + return false; + } + + // Read file into buffer + size_t fileSize = file.size(); + uint8_t buffer[fileSize]; + file.read(buffer, fileSize); + file.close(); + + // Decode protobuf + pb_istream_t stream = pb_istream_from_buffer(buffer, fileSize); + bool status = pb_decode(&stream, hardware_HardwareConfig_fields, &data_); + + if (!status) { + LOG_ERROR("Failed to decode hardware config protobuf"); + setDefaults(); + return false; + } + + runCallback(); + + return true; +} + +bool HardwareConfigRepo::save() { + if (!LittleFS.begin()) { + LOG_ERROR("HardwareConfigRepo: LittleFS mount failed"); + return false; + } + + // Encode to buffer first to get size + size_t bufferSize = 0; + pb_get_encoded_size(&bufferSize, hardware_HardwareConfig_fields, &data_); + + uint8_t buffer[bufferSize]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, bufferSize); + + bool status = pb_encode(&stream, hardware_HardwareConfig_fields, &data_); + if (!status) { + LOG_ERROR("Failed to encode hardware config protobuf"); + return false; + } + + // Write to file + File file = LittleFS.open(filename_, "w"); + if (!file) { + LOG_ERROR("Failed to open hardware config file for writing"); + return false; + } + + size_t bytesWritten = file.write(buffer, bufferSize); + file.close(); + + if (bytesWritten != bufferSize) { + LOG_ERROR("Failed to write complete hardware config file"); + return false; + } + + runCallback(); + + return true; +} + +void HardwareConfigRepo::setData(const hardware_HardwareConfig& newData) { + data_ = newData; + save(); // Persist immediately +} + +void HardwareConfigRepo::resetToDefaults() { + setDefaults(); + save(); +} + +void HardwareConfigRepo::runCallback() { + if (changeCallback_) { + changeCallback_(data_); + } +} + +HardwareConfigRepo hardwareConfigRepo; \ No newline at end of file diff --git a/lib/data/hardware_config.hpp b/lib/data/hardware_config.hpp new file mode 100644 index 0000000..de3e5bd --- /dev/null +++ b/lib/data/hardware_config.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +class HardwareConfigRepo { +public: + HardwareConfigRepo(); + ~HardwareConfigRepo(); + + // Set callback that gets called when config changes (load/save) + void setChangeCallback(std::function callback); + + // Load config from LittleFS, use defaults if file doesn't exist + bool load(); + + + // Get reference to config data + hardware_HardwareConfig& getData() { return data_; } + + // Set config data + void setData(const hardware_HardwareConfig& newData); + + // Reset to default values + void resetToDefaults(); + +private: + // Save current config to LittleFS + bool save(); + hardware_HardwareConfig data_; + const char* filename_ = "/hardware.pb"; + std::function changeCallback_ = nullptr; + + // Default values + static constexpr uint32_t DEFAULT_HOLD_DURATION_MS = 5000; + static constexpr bool DEFAULT_OVERRIDE = false; + static constexpr uint32_t DEFAULT_RELAY_PIN = 2; + static constexpr uint32_t DEFAULT_SENSOR_RX_PIN = 16; + static constexpr uint32_t DEFAULT_SENSOR_TX_PIN = 17; + + // Helper methods + void setDefaults(); + bool fileExists(); + void runCallback(); +}; + +extern HardwareConfigRepo hardwareConfigRepo; \ No newline at end of file diff --git a/lib/data/library.json b/lib/data/library.json new file mode 100644 index 0000000..8789ba7 --- /dev/null +++ b/lib/data/library.json @@ -0,0 +1,6 @@ +{ + "name": "hardware", + "dependencies": { + "nanopb": "*" + } +} diff --git a/lib/data/rfid.cpp b/lib/data/rfid.cpp new file mode 100644 index 0000000..674aa78 --- /dev/null +++ b/lib/data/rfid.cpp @@ -0,0 +1,282 @@ +#include "rfid.hpp" +#include +RfidDB rfidDB; + +RfidDB::RfidDB(const String& filename) + : filename_(filename), tmpFilename_(filename + ".tmp"), initialized_(false) {} + +RfidDB::~RfidDB() {} +void printDatabaseContents() { + LOG_DEBUG("--- RFID Database Contents (Stored Values) ---"); + rfidDB.iterate([](uint32_t stored_id) { + // 'stored_id' ist der Wert, wie er in der Datei steht (Byte-geswappt) + Serial.printf("Stored HEX: 0x%08X\n", stored_id); + }); + LOG_DEBUG("-------------------------------------------"); +} + +bool RfidDB::begin() { + if (initialized_) { + return true; + } + if (!LittleFS.begin()) { + LOG_ERROR("RfidDB: LittleFS mount failed"); + return false; + } + + // Ensure the file exists, creating it if necessary. + if (!LittleFS.exists(filename_)) { + File f = LittleFS.open(filename_, "w"); + if (!f) { + return false; // Could not create the file + } + f.close(); + } + initialized_ = true; + printDatabaseContents(); + return true; +} + +uint32_t RfidDB::count() { + if (!initialized_) return 0; + + File f = LittleFS.open(filename_, "r"); + if (!f) return 0; + + uint32_t n = fileEntryCount(f); + f.close(); + return n; +} + +bool RfidDB::contains(uint32_t raw_id) { + if (!initialized_) { + LOG_DEBUG("RfidDB: contains not initialized"); + return false; + } + + File f = LittleFS.open(filename_, "r"); + if (!f) { + LOG_DEBUG("RfidDB: contains failed to open file"); + return false; + } + + uint32_t id = byteSwap(raw_id); + uint32_t idx; + bool found = false; + bool ok = binarySearch(f, idx, id, found); + + f.close(); + LOG_DEBUG("RfidDB: contains id=%08X, ok=%d, found=%d", raw_id, ok, found); + return ok && found; +} + +void RfidDB::iterate(std::function callback) { + if (!initialized_ || !callback) return; + + File f = LittleFS.open(filename_, "r"); + if (!f) return; + + uint32_t n = fileEntryCount(f); + for (uint32_t i = 0; i < n; ++i) { + uint32_t v; + if (readEntryAt(f, i, v)) { + callback(v); + } + } + f.close(); +} + +uint32_t RfidDB::fileEntryCount(File &f) { + return f.size() / ENTRY_SIZE; +} + +// 🚀 More efficient read/write methods +bool RfidDB::readEntryAt(File &f, uint32_t index, uint32_t &out) { + if (!f.seek(index * ENTRY_SIZE, SeekSet)) return false; + return f.read(reinterpret_cast(&out), ENTRY_SIZE) == ENTRY_SIZE; +} + +bool RfidDB::writeEntryAt(File &f, uint32_t index, uint32_t value) { + if (!f.seek(index * ENTRY_SIZE, SeekSet)) return false; + return f.write(reinterpret_cast(&value), ENTRY_SIZE) == ENTRY_SIZE; +} + + +bool RfidDB::binarySearch(File &f, uint32_t &outIndex, uint32_t key, bool &found) { + // Get the number of entries in the file (each entry is 4 bytes) + uint32_t n = fileEntryCount(f); + + // If the file is empty, return with outIndex = 0 and found = false + if (n == 0) { + outIndex = 0; + found = false; + return true; + } + + // Initialize search boundaries for binary search + uint32_t left = 0; + uint32_t right = n - 1; + + // Perform binary search on the sorted database + // Note: The database must contain values in ascending order of their *swapped* (Little-Endian) representation + // to ensure correct search results. The 'key' parameter is the byte-swapped (Little-Endian) value of the + // raw RFID ID (e.g., raw ID 0x635C426D is swapped to 0x6D425C63 for comparison). + while (left <= right) { + // Calculate the middle index + uint32_t mid = left + (right - left) / 2; + uint32_t v; + + // Read the entry at index 'mid' from the file + // The file stores IDs as Big-Endian (e.g., bytes 63 5C 42 6D for original ID 0x635C426D). + // On this Little-Endian platform (e.g., ESP32), reading 4 bytes into 'v' interprets them as + // Little-Endian, so bytes 63 5C 42 6D become v = 0x6D425C63 (swapped). + if (!readEntryAt(f, mid, v)) { + return false; // Failed to read entry + } + + // Compare the read value 'v' (Little-Endian, swapped) with the search key (also Little-Endian, swapped) + if (v == key) { + outIndex = mid; + found = true; + return true; // Found the ID at index 'mid' + } + + // Since the database is sorted by the swapped (Little-Endian) values, + // adjust the search boundaries based on the comparison + if (v < key) { + left = mid + 1; // Search in the right half + } else { + if (mid == 0) break; // Prevent underflow when right = mid - 1 + right = mid - 1; // Search in the left half + } + } + + // If not found, set outIndex to the insertion point where the key would be added + // to maintain the sorted order of swapped (Little-Endian) values + outIndex = left; + found = false; + return true; +} + +uint32_t RfidDB::byteSwap(uint32_t x) const +{ + return ((x & 0xFF000000) >> 24) | + ((x & 0x00FF0000) >> 8) | + ((x & 0x0000FF00) << 8) | + ((x & 0x000000FF) << 24); +} + +bool RfidDB::add(uint32_t raw_id) { + if (!initialized_) return false; + + File src = LittleFS.open(filename_, "r"); + if (!src) return false; + uint32_t id = byteSwap(raw_id); + uint32_t idx; + bool found; + if (!binarySearch(src, idx, id, found)) { + src.close(); + return false; + } + if (found) { + src.close(); + return true; // Already present, we consider this a success + } + + File dst = LittleFS.open(tmpFilename_, "w"); + if (!dst) { + src.close(); + return false; + } + + bool success = true; + uint32_t n = fileEntryCount(src); + uint32_t v; + + // Copy entries before the insertion point + for (uint32_t i = 0; i < idx; ++i) { + if (!readEntryAt(src, i, v) || !writeEntryAt(dst, i, v)) { + success = false; + break; + } + } + // Insert the new entry + if (success && !writeEntryAt(dst, idx, id)) { + success = false; + } + // Copy the remaining entries + if (success) { + for (uint32_t i = idx; i < n; ++i) { + if (!readEntryAt(src, i, v) || !writeEntryAt(dst, i + 1, v)) { + success = false; + break; + } + } + } + + src.close(); + dst.close(); + + if (!success) { + LittleFS.remove(tmpFilename_); + return false; + } + + // Atomic replace + if (!LittleFS.rename(tmpFilename_, filename_)) { + // Fallback in case rename fails + LittleFS.remove(tmpFilename_); + return false; + } + + return true; +} + +bool RfidDB::remove(uint32_t raw_id) { + if (!initialized_) return false; + + File src = LittleFS.open(filename_, "r"); + if (!src) return false; + uint32_t id = byteSwap(raw_id); + uint32_t idx; + bool found; + if (!binarySearch(src, idx, id, found) || !found) { + src.close(); + return false; // Not found, so nothing to remove + } + + File dst = LittleFS.open(tmpFilename_, "w"); + if (!dst) { + src.close(); + return false; + } + + bool success = true; + uint32_t n = fileEntryCount(src); + uint32_t written = 0; + uint32_t v; + + for (uint32_t i = 0; i < n; ++i) { + if (i == idx) continue; // Skip the entry to be deleted + if (!readEntryAt(src, i, v) || !writeEntryAt(dst, written, v)) { + success = false; + break; + } + written++; + } + + src.close(); + dst.close(); + + if (!success) { + LittleFS.remove(tmpFilename_); + return false; + } + + if (!LittleFS.rename(tmpFilename_, filename_)) { + LittleFS.remove(tmpFilename_); + return false; + } + + return true; +} \ No newline at end of file diff --git a/lib/data/rfid.hpp b/lib/data/rfid.hpp new file mode 100644 index 0000000..a8717a6 --- /dev/null +++ b/lib/data/rfid.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include // For std::function + +class RfidDB { +public: + // The default constructor uses "/rfid.db" + RfidDB(const String& filename = "/rfid.db"); + ~RfidDB(); + + /** + * @brief Initializes the filesystem and the database file. + * Must be called once in setup(). + * @return true on success, otherwise false. + */ + bool begin(); + + /** + * @brief Returns the number of stored IDs. + */ + uint32_t count(); + + /** + * @brief Checks if an ID exists in the database. + * @param id The ID to check. + * @return true if the ID exists, otherwise false. + */ + bool contains(uint32_t id); + + /** + * @brief Adds a new ID. The ID is only added if it does not already exist. + * @param id The ID to add. + * @return true if the ID was successfully added, otherwise false. + */ + bool add(uint32_t id); + + /** + * @brief Removes an ID from the database. + * @param id The ID to remove. + * @return true if the ID was found and removed, otherwise false. + */ + bool remove(uint32_t id); + + /** + * @brief Iterates over all stored IDs and calls a callback function for each. + * @param callback The function to be called for each ID (e.g., a lambda: `[](uint32_t id){ Serial.println(id); }`). + */ + void iterate(std::function callback); + +private: + String filename_; + String tmpFilename_; + bool initialized_ = false; + + static constexpr size_t ENTRY_SIZE = sizeof(uint32_t); + + // Helper methods + bool readEntryAt(File &f, uint32_t index, uint32_t &out); + bool writeEntryAt(File &f, uint32_t index, uint32_t value); + bool binarySearch(File &f, uint32_t &outIndex, uint32_t key, bool &found); + uint32_t byteSwap(uint32_t x) const; + uint32_t fileEntryCount(File &f); +}; + +// Global instance, kept as requested +extern RfidDB rfidDB; \ No newline at end of file diff --git a/lib/data/session.cpp b/lib/data/session.cpp new file mode 100644 index 0000000..7dd8adc --- /dev/null +++ b/lib/data/session.cpp @@ -0,0 +1,108 @@ +#include "session.hpp" +#include // Required for PROGMEM + +// Definition of the global instance +SessionManager sessionManager; + +// The character set is now stored in flash memory to save RAM. +const char charset[] PROGMEM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +SessionManager::SessionManager() { + session_mutex_ = xSemaphoreCreateMutex(); + for (size_t i = 0; i < MAX_SESSIONS; ++i) { + sessions_[i].username[0] = '\0'; + } +} + +SessionManager::~SessionManager() { + vSemaphoreDelete(session_mutex_); +} + +const char* SessionManager::createSession(const char* username) { + const char* result_token = nullptr; + // Lock the mutex to ensure exclusive access to the sessions array. + if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) { + for (size_t i = 0; i < MAX_SESSIONS; ++i) { + if (sessions_[i].username[0] == '\0') { + strncpy(sessions_[i].username, username, sizeof(sessions_[i].username) - 1); + sessions_[i].username[sizeof(sessions_[i].username) - 1] = '\0'; + + generateToken(sessions_[i].token, sizeof(sessions_[i].token)); + + sessions_[i].expiry_time = millis() + (15 * 60 * 1000); + + result_token = sessions_[i].token; + break; // Exit loop once a slot is found + } + } + xSemaphoreGive(session_mutex_); // Release the mutex + } + return result_token; +} + +const char* SessionManager::validateSession(const char* token) { + if (token == nullptr) { + return nullptr; + } + + // We must copy the username to a static buffer, because the session array + // could be modified by another task after we release the mutex. + static char valid_username[33]; + valid_username[0] = '\0'; + + if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) { + for (size_t i = 0; i < MAX_SESSIONS; ++i) { + if (sessions_[i].username[0] != '\0' && strcmp(sessions_[i].token, token) == 0) { + if (millis() < sessions_[i].expiry_time) { + strcpy(valid_username, sessions_[i].username); + } else { + sessions_[i].username[0] = '\0'; // Expired, clear it + } + break; + } + } + xSemaphoreGive(session_mutex_); + } + + return (valid_username[0] != '\0') ? valid_username : nullptr; +} + +bool SessionManager::endSession(const char* token) { + if (token == nullptr) return false; + + bool found = false; + if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) { + for (size_t i = 0; i < MAX_SESSIONS; ++i) { + if (sessions_[i].username[0] != '\0' && strcmp(sessions_[i].token, token) == 0) { + sessions_[i].username[0] = '\0'; + found = true; + break; + } + } + xSemaphoreGive(session_mutex_); + } + return found; +} + +void SessionManager::cleanupExpiredSessions() { + unsigned long current_time = millis(); + if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) { + for (size_t i = 0; i < MAX_SESSIONS; ++i) { + if (sessions_[i].username[0] != '\0') { + if (current_time >= sessions_[i].expiry_time) { + sessions_[i].username[0] = '\0'; + } + } + } + xSemaphoreGive(session_mutex_); + } +} + +void SessionManager::generateToken(char* buffer, size_t buffer_size) { + const size_t charset_size = sizeof(charset) - 1; + for (size_t i = 0; i < buffer_size - 1; ++i) { + // Read the character directly from flash memory (PROGMEM). + buffer[i] = pgm_read_byte(&charset[esp_random() % charset_size]); + } + buffer[buffer_size - 1] = '\0'; +} \ No newline at end of file diff --git a/lib/data/session.hpp b/lib/data/session.hpp new file mode 100644 index 0000000..df2365e --- /dev/null +++ b/lib/data/session.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include // Required for mutex + +// Represents an active session. +// An empty username (username[0] == '\0') indicates the slot is free. +struct Session { + char username[33]; + char token[17]; // 16 chars + null terminator + unsigned long expiry_time; // Expiration timestamp from millis() +}; + +class SessionManager { +public: + SessionManager(); + ~SessionManager(); // Destructor to clean up the mutex + + const char* createSession(const char* username); + const char* validateSession(const char* token); + bool endSession(const char* token); + void cleanupExpiredSessions(); + +private: + static constexpr size_t MAX_SESSIONS = 3; + Session sessions_[MAX_SESSIONS]; + SemaphoreHandle_t session_mutex_; // Mutex to protect access to sessions_ + + void generateToken(char* buffer, size_t buffer_size); +}; + +// Central, global instance. +extern SessionManager sessionManager; \ No newline at end of file diff --git a/lib/data/settings.cpp b/lib/data/settings.cpp new file mode 100644 index 0000000..d653cbf --- /dev/null +++ b/lib/data/settings.cpp @@ -0,0 +1,161 @@ +#include "settings.hpp" +#include +#include +#include +#include +Settings::Settings() { + setDefaults(); +} + +Settings::~Settings() { + // Cleanup if needed +} + +void Settings::setDefaults() { + strcpy(data_.sync_server_url, "https://ssvc.ldev/api/"); + strcpy(data_.device_api_key, "c62a6b5c-914e-49d4-9871-1896a627e0df"); + strcpy(data_.sync_server_url, DEFAULT_SYNC_SERVER_URL); + strcpy(data_.device_api_key, DEFAULT_DEVICE_API_KEY); + data_.sync_interval_seconds = DEFAULT_SYNC_INTERVAL; + data_.auto_sync = DEFAULT_AUTO_SYNC; + data_.wifi_mode = DEFAULT_WIFI_MODE; + strcpy(data_.station_ssid, DEFAULT_STATION_SSID); + strcpy(data_.station_password, DEFAULT_STATION_PASSWORD); + strcpy(data_.ap_ssid, DEFAULT_AP_SSID); + strcpy(data_.ap_password, DEFAULT_AP_PASSWORD); + data_.ap_channel = DEFAULT_AP_CHANNEL; + data_.enable_fallback_ap = DEFAULT_ENABLE_FALLBACK_AP; + data_.log_level = DEFAULT_LOG_LEVEL; + data_.updated_at = millis(); // Use current uptime as timestamp + data_.version = DEFAULT_VERSION; +} + +bool Settings::fileExists() { + return LittleFS.exists(filename_); +} + +bool Settings::load() { + if (!fileExists()) { + // File doesn't exist, use defaults + setDefaults(); + }else{ + File file = LittleFS.open(filename_, "r"); + if (!file) { + LOG_ERROR("Failed to open settings file for reading"); + setDefaults(); + return false; + } + + // Read file into buffer + size_t fileSize = file.size(); + uint8_t buffer[fileSize]; + file.read(buffer, fileSize); + file.close(); + + // Decode protobuf + pb_istream_t stream = pb_istream_from_buffer(buffer, fileSize); + bool status = pb_decode(&stream, settings_SettingsData_fields, &data_); + + if (!status) { + LOG_ERROR("Failed to decode settings protobuf"); + setDefaults(); + return false; + } + } + + // callback if set + runCallback(); + + return true; +} + +bool Settings::save() { + // Update timestamp and version + data_.updated_at = millis(); + data_.version++; + + // Encode to buffer first to get size + size_t bufferSize = 0; + pb_get_encoded_size(&bufferSize, settings_SettingsData_fields, &data_); + + uint8_t buffer[bufferSize]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, bufferSize); + + bool status = pb_encode(&stream, settings_SettingsData_fields, &data_); + if (!status) { + LOG_ERROR("Failed to encode settings protobuf"); + return false; + } + + // Write to file + File file = LittleFS.open(filename_, "w"); + if (!file) { + LOG_ERROR("Failed to open settings file for writing"); + return false; + } + + size_t bytesWritten = file.write(buffer, bufferSize); + file.close(); + + if (bytesWritten != bufferSize) { + LOG_ERROR("Failed to write complete settings file"); + return false; + } + + return true; +} + +void Settings::setData(const settings_SettingsData& newData) { + data_ = newData; + data_.updated_at = millis(); + data_.version++; + + // Notify callback if set + runCallback(); +} + +void Settings::resetToDefaults() { + setDefaults(); +} + +struct SettingsCallbackParams { + settings_SettingsData data; + std::function callback; +}; + +static void settingsChangeTask(void* pvParameters) { + SettingsCallbackParams* params = static_cast(pvParameters); + if (params->callback) { + vTaskDelay(100 / portTICK_PERIOD_MS); // Small delay to ensure system stability + LOG_DEBUG("Settings (Task): Executing change callback..."); + params->callback(params->data); + LOG_DEBUG("Settings (Task): Callback finished."); + } + delete params; + vTaskDelete(NULL); +} + + +void Settings::runCallback() +{ + if (changeCallback_) { + SettingsCallbackParams* params = new SettingsCallbackParams(); + params->data = data_; // Create a safe copy for the task + params->callback = changeCallback_; + + xTaskCreate( + settingsChangeTask, + "SettingsChangeTask", + 4096, // Stack size + params, + 1, // Priority + NULL + ); + } +} + +void Settings::setChangeCallback(std::function callback) { + changeCallback_ = callback; +} + +Settings settings; \ No newline at end of file diff --git a/lib/data/settings.hpp b/lib/data/settings.hpp new file mode 100644 index 0000000..2a0bf4d --- /dev/null +++ b/lib/data/settings.hpp @@ -0,0 +1,56 @@ +#pragma once +#include +#include + +class Settings { +public: + Settings(); + ~Settings(); + + // Set callback that gets called when settings change + void setChangeCallback(std::function callback); + + // Load settings from LittleFS, use defaults if file doesn't exist + bool load(); + + // Save current settings to LittleFS + bool save(); + + // Get reference to settings data + settings_SettingsData& getData() { return data_; } + + // Set settings data and update timestamp/version + void setData(const settings_SettingsData& newData); + + // Reset to default values + void resetToDefaults(); + +private: + void runCallback(); + settings_SettingsData data_; + const char* filename_ = "/settings.pb"; + std::function changeCallback_ = nullptr; + SemaphoreHandle_t dataMutex_ = nullptr; + + + // Default values + static constexpr const char* DEFAULT_SYNC_SERVER_URL = "https://sync.example.com/api"; + static constexpr const char* DEFAULT_DEVICE_API_KEY = ""; + static constexpr uint32_t DEFAULT_SYNC_INTERVAL = 1800; + static constexpr bool DEFAULT_AUTO_SYNC = true; + static constexpr settings_WifiMode DEFAULT_WIFI_MODE = settings_WifiMode_WIFI_MODE_AP_STATION; + static constexpr const char* DEFAULT_STATION_SSID = "HOME"; + static constexpr const char* DEFAULT_STATION_PASSWORD = "redacted"; + static constexpr const char* DEFAULT_AP_SSID = "RFID-Master-AP"; + static constexpr const char* DEFAULT_AP_PASSWORD = "rfid12345"; + static constexpr uint32_t DEFAULT_AP_CHANNEL = 6; + static constexpr bool DEFAULT_ENABLE_FALLBACK_AP = true; + static constexpr settings_LogLevel DEFAULT_LOG_LEVEL = settings_LogLevel_LOG_LEVEL_INFO; + static constexpr uint32_t DEFAULT_VERSION = 1; + + // Helper methods + void setDefaults(); + bool fileExists(); +}; + +extern Settings settings; \ No newline at end of file diff --git a/lib/data/users.cpp b/lib/data/users.cpp new file mode 100644 index 0000000..d96b6c4 --- /dev/null +++ b/lib/data/users.cpp @@ -0,0 +1,392 @@ +#include "users.hpp" +#include +#include +#include +#include +#include +#include +#include + +// Definition of the global instance. +Users users; + +// ================================================================= +// == 1. NANOPB STREAM HELPERS for Arduino File Objects +// ================================================================= + +// A state structure to pass the Arduino File object to Nanopb's stream callbacks. +struct FileStreamState { + File* file; +}; + +// Nanopb callback for writing data to a File. +static bool writeFileCallback(pb_ostream_t *stream, const uint8_t *buf, size_t count) { + FileStreamState* state = (FileStreamState*)stream->state; + return state->file->write(buf, count) == count; +} + +// Nanopb callback for reading from a File. +static bool readFileCallback(pb_istream_t *stream, uint8_t *buf, size_t count) { + FileStreamState* state = (FileStreamState*)stream->state; + if (count == 0) return true; + size_t len = state->file->read(buf, count); + if (len < count) { + stream->bytes_left = 0; // Signal End-Of-File + } + return len > 0 || count == 0; +} + +// Helper to create a Nanopb output stream from an Arduino File object. +static pb_ostream_t pb_ostream_from_file(File &file) { + FileStreamState* state = new FileStreamState{&file}; + pb_ostream_t stream = {&writeFileCallback, state, SIZE_MAX, 0}; + return stream; +} + +// Helper to create a Nanopb input stream from an Arduino File object. +static pb_istream_t pb_istream_from_file(File &file) { + FileStreamState* state = new FileStreamState{&file}; + pb_istream_t stream = {&readFileCallback, state, file.size()}; + return stream; +} + +// This structure is used to pass data to the nanopb decode callbacks +// when decoding into a single, stack-allocated UserData object. +struct DecodeTarget { + UserData* user_buffer; +}; + +// Nanopb callback to decode a username string into our temporary buffer. +static bool decodeUsernameCallback(pb_istream_t *stream, const pb_field_t *field, void **arg) { + DecodeTarget* target = (DecodeTarget*)*arg; + + // 1. Store the length of the incoming string BEFORE reading from the stream. + size_t len = stream->bytes_left; + + if (len >= sizeof(target->user_buffer->username)) { + return false; // Input string is too long for our buffer. + } + + // 2. Read 'len' bytes. This will modify stream->bytes_left. + if (!pb_read(stream, (uint8_t*)target->user_buffer->username, len)) { + return false; + } + + // 3. Use the stored length 'len' to place the null terminator correctly. + target->user_buffer->username[len] = '\0'; + + return true; +} + +// Nanopb callback to decode a password hash (bytes) into our temporary buffer. +static bool decodePasswordCallback(pb_istream_t *stream, const pb_field_t *field, void **arg) { + DecodeTarget* target = (DecodeTarget*)*arg; + if (stream->bytes_left != sizeof(target->user_buffer->password_hash)) { + return false; // A SHA-256 hash MUST be 32 bytes. + } + return pb_read(stream, target->user_buffer->password_hash, stream->bytes_left); +} + +// ================================================================= +// == 2. USERS CLASS IMPLEMENTATION +// ================================================================= + +Users::Users() {} +Users::~Users() {} + +void Users::hashPassword(const char* password, uint8_t* hash_out) const { + SHA256 sha256; + sha256.update(password, strlen(password)); + sha256.finalize(hash_out, 32); +} + +// --- PRIVATE STATIC HELPERS (Moved inside the class scope) --- + +// Generic helper function to iterate over all users in the file +// and execute a callback for each one. The callback can stop the iteration by returning false. +bool Users::_iterateUsers(const char* filename, const std::function& user_callback) { + // check if file exists + if(!LittleFS.exists(filename)){ + return true; // No file is not an error, just means no users to iterate. + } + + File file = LittleFS.open(filename, "r"); + if (!file || file.size() == 0) { + return true; // No file is not an error, just means no users to iterate. + } + + pb_istream_t istream = pb_istream_from_file(file); + + UserData user_buffer; // Single user buffer on the stack + DecodeTarget target = { &user_buffer }; + webui_WebUiUser user_msg = webui_WebUiUser_init_zero; + user_msg.password_hash.funcs.decode = &decodePasswordCallback; + user_msg.password_hash.arg = ⌖ + + bool continue_iterating = true; + while (istream.bytes_left > 0) { + memset(&user_buffer, 0, sizeof(user_buffer)); + + if (pb_decode_delimited(&istream, webui_WebUiUser_fields, &user_msg)) { + strcpy(user_buffer.username, user_msg.username); + if (!user_callback(user_buffer)) { + continue_iterating = false; + break; + } + } else { + // Decoding failed, stop iterating. + break; + } + } + + + delete (FileStreamState*)istream.state; + file.close(); + return continue_iterating; +} + +// Generic helper for streaming transformations (delete, update). +// The callback modifies a user buffer and returns true to keep it, false to delete it. +bool Users::_transformUsers(const char* src_filename, const std::function& transform_callback) { + const char* tmp_filename = "/users.pb.tmp"; + File src_file = LittleFS.open(src_filename, "r"); + if (!src_file) return true; + + File dst_file = LittleFS.open(tmp_filename, "w"); + if (!dst_file) { + src_file.close(); + return false; + } + + pb_istream_t istream = pb_istream_from_file(src_file); + pb_ostream_t ostream = pb_ostream_from_file(dst_file); + + bool success = true; + UserData user_buffer; // Our 1-user buffer + + // --- CORRECTION START --- + + // 1. A structure ONLY for decoding (reading) + DecodeTarget decode_target = { &user_buffer }; + webui_WebUiUser decode_msg = webui_WebUiUser_init_zero; + decode_msg.password_hash.funcs.decode = &decodePasswordCallback; + decode_msg.password_hash.arg = &decode_target; + + // 2. A structure ONLY for encoding (writing) + webui_WebUiUser encode_msg = webui_WebUiUser_init_zero; + encode_msg.password_hash.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) { + UserData* u = (UserData*)*arg; + return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, u->password_hash, sizeof(u->password_hash)); + }; + + // 3. Main loop: Decode with decode_msg, transform user_buffer, encode with encode_msg + + while (istream.bytes_left > 0 && pb_decode_delimited(&istream, webui_WebUiUser_fields, &decode_msg)) { + strcpy(user_buffer.username, decode_msg.username); + if (transform_callback(user_buffer)) { + // Keep: Encode the content from the buffer with the encode_msg structure + strcpy(encode_msg.username, user_buffer.username); + encode_msg.password_hash.arg = &user_buffer; + if (!pb_encode_delimited(&ostream, webui_WebUiUser_fields, &encode_msg)) { + success = false; + break; + } + } + } + + // --- Rest of the function remains the same --- + delete (FileStreamState*)istream.state; + delete (FileStreamState*)ostream.state; + src_file.close(); + dst_file.close(); + + if (!success) { + LittleFS.remove(tmp_filename); + return false; + } + + if (LittleFS.exists(src_filename)) { + LittleFS.remove(src_filename); + } + return LittleFS.rename(tmp_filename, src_filename); +} + +// --- PUBLIC READ-ONLY METHODS --- + +size_t Users::getUserCount() { + if(initialized_) { + return userCount_; + } + + size_t count = 0; + _iterateUsers(filename_, [&](UserData& user) { + count++; + return true; // Continue iterating + }); + userCount_ = count; + initialized_ = true; + return count; +} + +bool Users::userExists(const char* username) const { + bool found = false; + _iterateUsers(filename_, [&](UserData& user) { + if (strcmp(user.username, username) == 0) { + found = true; + return false; // Stop iterating + } + return true; // Continue iterating + }); + return found; +} + +bool Users::checkPassword(const char* username, const char* password) const { + bool success = false; + _iterateUsers(filename_, [&](UserData& user) { + if (strcmp(user.username, username) == 0) { + uint8_t provided_hash[32]; + hashPassword(password, provided_hash); + if (memcmp(user.password_hash, provided_hash, 32) == 0) { + success = true; + } + return false; // Stop iterating, user found. + } + return true; // Continue iterating + }); + return success; +} + +bool Users::loadUsernamesCallback(const std::function &callback) const +{ + if (!callback) return false; + return _iterateUsers(filename_, [&](UserData &user) { + auto res = callback(user.username); + if (!res) return false; // Stop iterating + return true; // Continue iterating + }); +} + +bool Users::addUser(const char* username, const char* password) { + // --- Phase 1: Checking preconditions --- + bool exists = userExists(username); + size_t count = getUserCount(); + + if (exists || count >= MAX_USERS) { + if (exists) { + LOG_ERROR("addUser: User already exists. Aborting."); + } else { + LOG_ERROR("addUser: User limit reached. Aborting."); + } + return false; + } + + // --- Phase 2: Preparing the files --- + const char* tmp_filename = "/users.pb.tmp"; + File dst_file = LittleFS.open(tmp_filename, "w"); + if (!dst_file) { + LOG_ERROR("addUser: Failed to open temporary file. Check filesystem space and permissions."); + return false; + } + + File src_file = LittleFS.open(filename_, "r"); + if (src_file && src_file.size() > 0) { + uint8_t buffer[128]; + while (src_file.available()) { + size_t len = src_file.read(buffer, sizeof(buffer)); + dst_file.write(buffer, len); + } + src_file.close(); + } else { + if (src_file) src_file.close(); + } + + // --- Phase 3: Appending the new user --- + pb_ostream_t ostream = pb_ostream_from_file(dst_file); + webui_WebUiUser new_user_msg = webui_WebUiUser_init_zero; + + // Prepare username for the encoder + strcpy(new_user_msg.username, username); + + // Prepare password hash for the encoder + uint8_t password_hash[32]; + hashPassword(password, password_hash); // Important: Hash is created here, not in the lambda + new_user_msg.password_hash.arg = password_hash; + new_user_msg.password_hash.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) { + return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, (uint8_t*)*arg, 32); + }; + + bool status = pb_encode_delimited(&ostream, webui_WebUiUser_fields, &new_user_msg); + + // Important: Always release resources (stream state and file) + delete (FileStreamState*)ostream.state; + dst_file.close(); + + if (!status) { + LOG_ERROR("addUser: Protobuf encoding (pb_encode_delimited) failed!"); + LOG_DEBUG("addUser: Removing failed temporary file: %s\n", tmp_filename); + LittleFS.remove(tmp_filename); + return false; + } + + // --- Phase 4: Atomic replacement of the old file --- + if (LittleFS.exists(filename_)) { + if (!LittleFS.remove(filename_)) { + LOG_ERROR("addUser: Could not remove old file '%s'. Aborting.\n", filename_); + LittleFS.remove(tmp_filename); // Cleanup temp file + return false; + } + } + + bool rename_ok = LittleFS.rename(tmp_filename, filename_); + + if (rename_ok) { + } else { + LOG_ERROR("addUser: Final rename failed! Filesystem might be in an inconsistent state."); + LittleFS.remove(tmp_filename); // Attempt to clean up + } + + if (rename_ok) { + userCount_++; + } + + return rename_ok; +} + +bool Users::deleteUser(const char* username) { + bool user_was_found = false; + + bool success = _transformUsers(filename_, [&](UserData& user) { + if (strcmp(user.username, username) == 0) { + user_was_found = true; + return false; // Delete this user + } + return true; // Keep this user + }); + + bool user_deleted = success && user_was_found; + if (user_deleted) { + userCount_--; + } + + return user_deleted; +} + +bool Users::updatePassword(const char* username, const char* newPassword) { + bool user_was_found = false; + bool success = _transformUsers(filename_, [&](UserData& user) { + if (strcmp(user.username, username) == 0) { + user_was_found = true; + hashPassword(newPassword, user.password_hash); + } + return true; // Always keep the user, just maybe modified. + }); + return success && user_was_found; +} + +void Users::reset() { + if (LittleFS.exists(filename_)) { + LittleFS.remove(filename_); + } + userCount_ = 0; + initialized_ = false; +} \ No newline at end of file diff --git a/lib/data/users.hpp b/lib/data/users.hpp new file mode 100644 index 0000000..8db47e2 --- /dev/null +++ b/lib/data/users.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include // Ensure this is the version with 'bytes password_hash' +#include // Required for std::function + +// This struct definition is correct and necessary. +struct UserData { + char username[33]; // 32 chars + null terminator + uint8_t password_hash[32]; // 32-byte raw SHA-256 hash +}; + +class Users { +public: + Users(); + ~Users(); + + bool addUser(const char* username, const char* password); + bool deleteUser(const char* username); + bool updatePassword(const char* username, const char* newPassword); + bool userExists(const char* username) const; + bool checkPassword(const char* username, const char* password) const; + bool loadUsernamesCallback(const std::function& callback) const; + size_t getUserCount(); + void reset(); + +private: + const char* filename_ = "/users.pb"; + static constexpr size_t MAX_USERS = 5; + mutable size_t userCount_ = 0; + mutable bool initialized_ = false; + + void hashPassword(const char* password, uint8_t* hash_out) const; static bool _iterateUsers(const char* filename, const std::function& user_callback); + static bool _transformUsers(const char* src_filename, const std::function& transform_callback); +}; + + +extern Users users; \ No newline at end of file diff --git a/lib/hardware/hardware_rtc.cpp b/lib/hardware/hardware_rtc.cpp new file mode 100644 index 0000000..8f958be --- /dev/null +++ b/lib/hardware/hardware_rtc.cpp @@ -0,0 +1,168 @@ +#include "hardware_rtc.hpp" +#include + + +HardwareRTC::HardwareRTC(TwoWire& wire): wire(wire), initialized(false) {} + +HardwareRTC::~HardwareRTC() {} + +void HardwareRTC::begin() { + if(wire.getClock() == 0) { + wire.begin(); + wire.setClock(100000); // Set to 100kHz + } + initialized = true; + LOG_DEBUG("RTC initialized"); + setSystemTime(0); +} + +bool HardwareRTC::setTime(time_t unixTimestamp) { + if (!initialized) { + LOG_WARN("RTC not initialized"); + return false; + } + + // Use time_t, which on ESP32 is 64-bit and safe for this conversion + time_t t = unixTimestamp; + struct tm timeStruct; + gmtime_r(&t, &timeStruct); + + // Check if the year is within the DS1307's valid range (2000-2099) + if (timeStruct.tm_year < 100 || timeStruct.tm_year > 199) { + // Year is out of the 2000-2099 range that the DS1307 can store. + LOG_ERROR("RTC setTime: Year %d is out of range (2000-2099)", timeStruct.tm_year + 1900); + return false; + } + + uint8_t buffer[7]; + buffer[0] = decToBcd(timeStruct.tm_sec); + buffer[1] = decToBcd(timeStruct.tm_min); + buffer[2] = decToBcd(timeStruct.tm_hour); + buffer[3] = decToBcd(timeStruct.tm_wday + 1); + buffer[4] = decToBcd(timeStruct.tm_mday); + buffer[5] = decToBcd(timeStruct.tm_mon + 1); + // The DS1307 only stores years 00-99. We handle this by taking the year since 2000. + buffer[6] = decToBcd(timeStruct.tm_year - 100); + + writeRegisters(buffer, 7); + + // set system time to match RTC + setSystemTime(0); + + LOG_DEBUG("RTC setTime: %s", toDateString(unixTimestamp).c_str()); + return true; +} + +time_t HardwareRTC::getTime() { + if (!initialized) return 0; + + uint8_t buffer[7]; + readRegisters(buffer, 7); + + struct tm timeStruct = {0}; // Important: Initialize the struct to zero + timeStruct.tm_sec = bcdToDec(buffer[0] & 0x7F); + timeStruct.tm_min = bcdToDec(buffer[1]); + timeStruct.tm_hour = bcdToDec(buffer[2] & 0x3F); + timeStruct.tm_wday = bcdToDec(buffer[3]) - 1; + timeStruct.tm_mday = bcdToDec(buffer[4]); + timeStruct.tm_mon = bcdToDec(buffer[5]) - 1; + // Assume all 2-digit years from the RTC are in the 21st century (2000-2099) + timeStruct.tm_year = bcdToDec(buffer[6]) + 100; // Years since 1900 + + // mktime converts a local time struct to a time_t. + // Since the RTC stores time without timezone info, we treat it as UTC. + // timegm is the correct function for this, but mktime is often used + // on embedded systems with the timezone set to UTC. + time_t t = mktime(&timeStruct); + + LOG_DEBUG("RTC getTime: %s", toDateString(t).c_str()); + + // Cast the 64-bit time_t to uint64_t for the return type + return static_cast(t); +} + +void HardwareRTC::setSystemTime(int timezoneOffsetHours) { + if (!initialized) { + LOG_WARN("RTC not initialized"); + return; + } + + + uint64_t unixTime = getTime(); + if (unixTime == 0) return; + + unixTime += timezoneOffsetHours * 3600; + + LOG_DEBUG("RTC setSystemTime: %s", toDateString(unixTime).c_str()); + + struct timeval tv; + // The tv_sec field is of type time_t, which is 64-bit on ESP32 + tv.tv_sec = unixTime; + tv.tv_usec = 0; + settimeofday(&tv, NULL); +} + + +bool HardwareRTC::isRunning() { + if (!initialized) return false; + + uint8_t buffer[1]; + readRegisters(buffer, 1); + return !(buffer[0] & 0x80); // CH bit is 0 when running +} + +void HardwareRTC::setTimezone(const char* timezone) { + setenv("TZ", timezone, 1); + tzset(); +} + +void HardwareRTC::update() +{ + static uint32_t nextSync = 0; + time_t now = millis(); + if (now >= nextSync) { + if(!isRunning()) { + LOG_WARN("RTC is not running, skipping update"); + return; + } + setSystemTime(0); + nextSync = now + (15UL * 60UL * 1000UL); // every 15 minutes + LOG_DEBUG("RTC update: System time synchronized with RTC"); + } +} + +String HardwareRTC::toDateString(time_t timestamp) { + struct tm* timeinfo = gmtime(×tamp); + char buffer[40]; + strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeinfo); + return String(buffer); +} + +uint8_t HardwareRTC::bcdToDec(uint8_t bcd) { + return ((bcd >> 4) * 10) + (bcd & 0x0F); +} + +uint8_t HardwareRTC::decToBcd(uint8_t dec) { + return ((dec / 10) << 4) | (dec % 10); +} + +void HardwareRTC::readRegisters(uint8_t* buffer, uint8_t length) { + Wire.beginTransmission(DS1307_ADDRESS); + Wire.write(0x00); // start at register 0 + Wire.endTransmission(); + + Wire.requestFrom(DS1307_ADDRESS, length); + for (uint8_t i = 0; i < length; i++) { + buffer[i] = Wire.read(); + } +} + +void HardwareRTC::writeRegisters(uint8_t* buffer, uint8_t length) { + Wire.beginTransmission(DS1307_ADDRESS); + Wire.write(0x00); // start at register 0 + for (uint8_t i = 0; i < length; i++) { + Wire.write(buffer[i]); + } + Wire.endTransmission(); +} + diff --git a/lib/hardware/hardware_rtc.hpp b/lib/hardware/hardware_rtc.hpp new file mode 100644 index 0000000..b6be1cb --- /dev/null +++ b/lib/hardware/hardware_rtc.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#define DS1307_ADDRESS 0x68 +#define EEPROM_ADDRESS 0x50 + +class HardwareRTC { +public: + HardwareRTC(TwoWire& wire = Wire); + ~HardwareRTC(); + + void begin(); + + // Set RTC time from a 64-bit Unix timestamp + bool setTime(time_t unixTimestamp); + + // Get current time as a 64-bit Unix timestamp + time_t getTime(); + + static time_t getSystemTime() { + struct timeval tv; + if (gettimeofday(&tv, nullptr) != 0) { + LOG_ERROR("Failed to get system time for log entry"); + return 0; // Fallback-Wert + } else { + return tv.tv_sec; // Unix-Timestamp in Sekunden + } + gettimeofday(&tv, NULL); + return tv.tv_sec; + } + // Helper method to convert time_t to date string + static String toDateString(time_t timestamp); + + + void setSystemTime(int timezoneOffsetHours = 0); + bool isRunning(); + void setTimezone(const char* timezone); + void update(); // Placeholder for future use + +private: + bool initialized; + TwoWire& wire; + + uint8_t bcdToDec(uint8_t bcd); + uint8_t decToBcd(uint8_t dec); + + void readRegisters(uint8_t* buffer, uint8_t length); + void writeRegisters(uint8_t* buffer, uint8_t length); +}; \ No newline at end of file diff --git a/lib/hardware/hardware_serial.cpp b/lib/hardware/hardware_serial.cpp new file mode 100644 index 0000000..8b963b4 --- /dev/null +++ b/lib/hardware/hardware_serial.cpp @@ -0,0 +1,222 @@ +#include "hardware_serial.hpp" +#include +#include + +//#define DEBUG_PROTO +#ifdef DEBUG_PROTO +#define LOG(msg) LOG_DEBUG(msg) +#else +#define LOG(msg) +#endif + +ProtoSerial::ProtoSerial() + : serial(nullptr), initialized(false), lastError{0}, callback(nullptr), + currentState(WAITING_FOR_SYNC_START), payloadLength(0), messageAvailable(false), + lastUpdate(0), lastByteTime(0) { + receivedMessage = INCOMING_MESSAGE_INIT; +} + +ProtoSerial::~ProtoSerial() { + // No delete needed; serial is caller-managed +} + +void ProtoSerial::begin(SerialType& serialPort) { + serial = &serialPort; + initialized = true; + currentState = WAITING_FOR_SYNC_START; + payloadLength = 0; + messageAvailable = false; + lastError[0] = '\0'; + lastUpdate = 0; + lastByteTime = 0; + snprintf(lastError, sizeof(lastError), "Initialized ProtoSerial"); + LOG(lastError); +} + +bool ProtoSerial::sendMessage(const OutgoingMessage& message) { + if (!initialized || !serial || !serial->availableForWrite()) { + snprintf(lastError, sizeof(lastError), "Serial not initialized or not ready"); + LOG(lastError); + return false; + } + + uint8_t buffer[OUTGOING_MESSAGE_SIZE]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + + // Encode the message + if (!pb_encode(&stream, OUTGOING_MESSAGE_FIELDS, &message)) { + snprintf(lastError, sizeof(lastError), "Failed to encode message"); + LOG(lastError); + return false; + } + + uint16_t messageLength = stream.bytes_written; + if (messageLength > sizeof(buffer)) { + snprintf(lastError, sizeof(lastError), "Encoded message too large: %u", messageLength); + LOG(lastError); + return false; + } + + // Send framing: sync start, length, payload, CRC, sync end + serial->write(SYNC_START); + serial->write((uint8_t*)&messageLength, sizeof(messageLength)); + serial->write(buffer, messageLength); + uint8_t crc = calculate_crc8(buffer, messageLength); + serial->write(crc); + serial->write(SYNC_END); + serial->flush(); + + #ifdef DEBUG_PROTO + dumpHex(buffer, messageLength, "Sent"); + snprintf(lastError, sizeof(lastError), "Sent message, length: %u, CRC: 0x%02X", messageLength, crc); + LOG(lastError); + #endif + return true; +} + +void ProtoSerial::update() { + if (!initialized || !serial) { + snprintf(lastError, sizeof(lastError), "Serial not initialized"); + LOG(lastError); + return; + } + + // Throttle updates to ~100Hz + unsigned long now = millis(); + if (now - lastUpdate < 10) return; + lastUpdate = now; + + // Timeout if no data received for 1 second + if (currentState != WAITING_FOR_SYNC_START && now - lastByteTime > 1000) { + snprintf(lastError, sizeof(lastError), "Receive timeout"); + LOG(lastError); + currentState = WAITING_FOR_SYNC_START; + payloadLength = 0; + } + + while (serial->available()) { + lastByteTime = now; + switch (currentState) { + case WAITING_FOR_SYNC_START: { + uint8_t byte = serial->read(); + if (byte == SYNC_START) { + currentState = WAITING_FOR_LENGTH; + LOG("Received SYNC_START"); + } + break; + } + case WAITING_FOR_LENGTH: { + if (serial->available() >= sizeof(payloadLength)) { + serial->readBytes((uint8_t*)&payloadLength, sizeof(payloadLength)); + if (payloadLength > sizeof(payloadBuffer) || payloadLength == 0) { + snprintf(lastError, sizeof(lastError), "Invalid payload length: %u", payloadLength); + LOG(lastError); + currentState = WAITING_FOR_SYNC_START; + payloadLength = 0; + } else { + currentState = READING_PAYLOAD; + snprintf(lastError, sizeof(lastError), "Received length: %u", payloadLength); + LOG(lastError); + } + } + break; + } + case READING_PAYLOAD: { + if (serial->available() >= payloadLength) { + serial->readBytes(payloadBuffer, payloadLength); + currentState = READING_CRC; + } + break; + } + case READING_CRC: { + if (serial->available() >= 1) { + uint8_t received_crc = serial->read(); + uint8_t expected_crc = calculate_crc8(payloadBuffer, payloadLength); + if (received_crc != expected_crc) { + snprintf(lastError, sizeof(lastError), "CRC mismatch: received 0x%02X, expected 0x%02X", received_crc, expected_crc); + LOG(lastError); + } else if (serial->available() >= 1 && serial->read() != SYNC_END) { + snprintf(lastError, sizeof(lastError), "Missing SYNC_END"); + LOG(lastError); + } else { + dumpHex(payloadBuffer, payloadLength, "Received"); + processReceivedMessage(payloadBuffer, payloadLength); + snprintf(lastError, sizeof(lastError), "Received valid message, length: %u", payloadLength); + LOG(lastError); + } + currentState = WAITING_FOR_SYNC_START; + payloadLength = 0; + } + break; + } + } + } +} + +bool ProtoSerial::hasMessage() const { + return messageAvailable; +} + +const IncomingMessage& ProtoSerial::getMessage() const { + return receivedMessage; +} + +void ProtoSerial::clearMessage() { + messageAvailable = false; + receivedMessage = INCOMING_MESSAGE_INIT; + snprintf(lastError, sizeof(lastError), "Message cleared"); + LOG(lastError); +} + +void ProtoSerial::setCallback(Callback cb) { + callback = cb; + snprintf(lastError, sizeof(lastError), "Callback set"); + LOG(lastError); +} + +void ProtoSerial::processReceivedMessage(uint8_t* buffer, uint16_t length) { + receivedMessage = INCOMING_MESSAGE_INIT; + pb_istream_t stream = pb_istream_from_buffer(buffer, length); + if (pb_decode(&stream, INCOMING_MESSAGE_FIELDS, &receivedMessage)) { + if (receivedMessage.which_payload == 0) { + snprintf(lastError, sizeof(lastError), "Invalid message: which_payload not set"); + LOG(lastError); + return; + } + messageAvailable = true; + if (callback) { + callback(receivedMessage); + } + snprintf(lastError, sizeof(lastError), "Message decoded successfully"); + LOG(lastError); + } else { + snprintf(lastError, sizeof(lastError), "Failed to decode message"); + LOG(lastError); + } +} + +uint8_t ProtoSerial::calculate_crc8(const uint8_t* data, uint16_t len) { + uint8_t crc = 0x00; + for (uint16_t i = 0; i < len; i++) { + crc ^= data[i]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80) crc = (crc << 1) ^ 0x31; // CRC-8 polynomial + else crc <<= 1; + } + } + return crc; +} + +void ProtoSerial::dumpHex(const uint8_t* data, uint16_t len, const char* label) { +#ifdef DEBUG_PROTO + Serial.print(label); + Serial.print(": "); + for (uint16_t i = 0; i < len; i++) { + if (i % 16 == 0) Serial.println(); + Serial.printf("%02X ", data[i]); + } + Serial.println(); +#endif +} + +ProtoSerial pserial; \ No newline at end of file diff --git a/lib/hardware/hardware_serial.hpp b/lib/hardware/hardware_serial.hpp new file mode 100644 index 0000000..2347eed --- /dev/null +++ b/lib/hardware/hardware_serial.hpp @@ -0,0 +1,171 @@ +#pragma once + +#ifdef ESP32 +#include +typedef HardwareSerial SerialType; +#define SERIAL_TYPE HardwareSerial +#elif defined(ESP8266) +#include +typedef HardwareSerial SerialType; +#define SERIAL_TYPE HardwareSerial +#else +#error "Unsupported platform. Only ESP32 and ESP8266 are supported." +#endif + +#include +#include +#include "hardware.pb.h" + +// Define the role of this device +// Uncomment one of the following defines to set the role +#define HARDWARE_SERIAL_ROLE_CONTROL +// #define HARDWARE_SERIAL_ROLE_SENSOR + +// Based on role, define the message types +#if defined(HARDWARE_SERIAL_ROLE_CONTROL) +typedef hardware_SensorToControlMessage IncomingMessage; +typedef hardware_ControlToSensorMessage OutgoingMessage; +#define INCOMING_MESSAGE_SIZE hardware_SensorToControlMessage_size +#define OUTGOING_MESSAGE_SIZE hardware_ControlToSensorMessage_size +#define INCOMING_MESSAGE_FIELDS hardware_SensorToControlMessage_fields +#define OUTGOING_MESSAGE_FIELDS hardware_ControlToSensorMessage_fields +#define INCOMING_MESSAGE_INIT hardware_SensorToControlMessage_init_zero +#define OUTGOING_MESSAGE_INIT hardware_ControlToSensorMessage_init_zero +#elif defined(HARDWARE_SERIAL_ROLE_SENSOR) +typedef hardware_ControlToSensorMessage IncomingMessage; +typedef hardware_SensorToControlMessage OutgoingMessage; +#define INCOMING_MESSAGE_SIZE hardware_ControlToSensorMessage_size +#define OUTGOING_MESSAGE_SIZE hardware_SensorToControlMessage_size +#define INCOMING_MESSAGE_FIELDS hardware_ControlToSensorMessage_fields +#define OUTGOING_MESSAGE_FIELDS hardware_SensorToControlMessage_fields +#define INCOMING_MESSAGE_INIT hardware_ControlToSensorMessage_init_zero +#define OUTGOING_MESSAGE_INIT hardware_ControlToSensorMessage_init_zero +#else +#error "Must define either HARDWARE_SERIAL_ROLE_CONTROL or HARDWARE_SERIAL_ROLE_SENSOR" +#endif + +/** + * @class ProtoSerial + * @brief Manages serial communication with Protocol Buffers for ESP32/ESP8266. + * Supports length-prefixed messages with CRC-8 checksum and sync bytes. + * Configured for SENSOR or CONTROL role via HARDWARE_SERIAL_ROLE_* macros. + * On ESP8266, uses UART0 (Serial, RX=GPIO3, TX=GPIO1). On ESP32, uses caller-provided UART. + */ +class ProtoSerial { +public: + using Callback = void (*)(const IncomingMessage&); ///< Callback for received messages. + + /** + * @brief Constructor. + */ + ProtoSerial(); + + /** + * @brief Destructor. No-op (serial is managed by caller). + */ + ~ProtoSerial(); + + /** + * @brief Initializes serial communication with a caller-provided serial port. + * @param serialPort Reference to a configured SerialType (e.g., Serial for ESP8266, HardwareSerial for ESP32). + */ + void begin(SerialType& serialPort); + + /** + * @brief Sends a Protocol Buffers message with sync bytes and CRC-8. + * @param message The message to send (OutgoingMessage type). + * @return True if sent successfully, false otherwise. + */ + bool sendMessage(const OutgoingMessage& message); + + /** + * @brief Checks for incoming messages (non-blocking). + */ + void update(); + + /** + * @brief Checks if a message is available. + * @return True if a message is ready to be read. + */ + bool hasMessage() const; + + /** + * @brief Gets the received message. + * @return Reference to the received message (valid only if hasMessage() is true). + */ + const IncomingMessage& getMessage() const; + + /** + * @brief Clears the received message. + */ + void clearMessage(); + + /** + * @brief Sets the callback for received messages. + * @param cb Callback function to handle incoming messages. + */ + void setCallback(Callback cb); + + /** + * @brief Checks if the serial interface is initialized. + * @return True if initialized. + */ + bool isInitialized() const { return initialized; } + + /** + * @brief Gets the last error message (for debugging). + * @return Last error string or empty if none. + */ + const char* getLastError() const { return lastError; } + +private: + SerialType* serial; ///< Serial interface (pointer to caller-provided SerialType). + bool initialized; ///< Tracks initialization state. + char lastError[64]; ///< Last error message (fixed-size to avoid heap issues). + Callback callback; ///< Callback for received messages. + + // Framing constants + static const uint8_t SYNC_START = 0xAA; ///< Start byte for message framing. + static const uint8_t SYNC_END = 0xBB; ///< End byte for message framing. + + // Receive state machine + enum RxState { + WAITING_FOR_SYNC_START, ///< Waiting for start byte (0xAA). + WAITING_FOR_LENGTH, ///< Waiting for 2-byte length. + READING_PAYLOAD, ///< Reading payload bytes. + READING_CRC ///< Reading CRC-8 byte. + }; + RxState currentState; ///< Current state of receive state machine. + uint16_t payloadLength; ///< Length of incoming payload. + uint8_t payloadBuffer[INCOMING_MESSAGE_SIZE + 10]; ///< Buffer for payload (plus overhead). + IncomingMessage receivedMessage; ///< Decoded incoming message. + bool messageAvailable; ///< True if a message is ready. + unsigned long lastUpdate; ///< Timestamp of last update (for throttling). + unsigned long lastByteTime; ///< Timestamp of last byte received (for timeout). + + // Helper methods + /** + * @brief Processes a received payload and decodes it. + * @param buffer Payload buffer. + * @param length Payload length. + */ + void processReceivedMessage(uint8_t* buffer, uint16_t length); + + /** + * @brief Calculates CRC-8 for a buffer. + * @param data Buffer to compute CRC over. + * @param len Length of buffer. + * @return CRC-8 value. + */ + uint8_t calculate_crc8(const uint8_t* data, uint16_t len); + + /** + * @brief Logs a hex dump of a buffer (debug only). + * @param data Buffer to dump. + * @param len Length of buffer. + * @param label Label for the dump. + */ + void dumpHex(const uint8_t* data, uint16_t len, const char* label); +}; + +extern ProtoSerial pserial; \ No newline at end of file diff --git a/lib/log/logger.cpp b/lib/log/logger.cpp new file mode 100644 index 0000000..4ef3fc6 --- /dev/null +++ b/lib/log/logger.cpp @@ -0,0 +1,149 @@ +#include "logger.hpp" +#include +#include +#include // For va_list, vsnprintf +#include + +Logger::Logger() + : logCount(0), logIndex(0), currentLogLevel(LOG_INFO), + logCallback(nullptr), logQueue(nullptr), logTaskHandle(nullptr) {} + +Logger::~Logger() { + // Clean up FreeRTOS objects + if (logTaskHandle) vTaskDelete(logTaskHandle); + if (logQueue) vQueueDelete(logQueue); +} + +void Logger::begin(UBaseType_t taskPriority, uint32_t taskStackSize) { + // Create a queue that can hold up to 10 LogEntry items. + logQueue = xQueueCreate(10, sizeof(LogEntry)); + + if (logQueue == nullptr) { + Serial.println("FATAL: Failed to create logger queue!"); + return; + } + + // Create the background task. It passes 'this' so the static function + // can access the instance's members. + xTaskCreate( + logProcessingTask, + "LoggerTask", + taskStackSize, + this, // Pass the current object instance to the task + taskPriority, + &logTaskHandle + ); + + if (logTaskHandle == nullptr) { + Serial.println("FATAL: Failed to create logger task!"); + } +} + +void Logger::addLog(LogLevel level, const char* format, va_list args) { + if (level < currentLogLevel) { + return; + } + + LogEntry entry; + entry.level = level; + + // Verwende Systemzeit statt millis() + struct timeval tv; + if (gettimeofday(&tv, nullptr) != 0) { + LOG_ERROR("Failed to get system time for log entry"); + entry.timestamp = 0; // Fallback-Wert + } else { + entry.timestamp = tv.tv_sec; // Unix-Timestamp in Sekunden + } + + vsnprintf(entry.message, sizeof(entry.message), format, args); + + // Add to internal circular buffer for history + logs[logIndex] = entry; + logIndex = (logIndex + 1) % MAX_LOGS; + if (logCount < MAX_LOGS) { + logCount++; + } + + if (logQueue != nullptr) { + xQueueSend(logQueue, &entry, pdMS_TO_TICKS(10)); + } +} + +// This is the function that runs in its own task. +void Logger::logProcessingTask(void* instance) { + // The instance pointer is the 'this' we passed in xTaskCreate. + Logger* loggerInstance = static_cast(instance); + LogEntry entryToProcess; + + while (true) { + if (xQueueReceive(loggerInstance->logQueue, &entryToProcess, portMAX_DELAY) == pdPASS) { + if (loggerInstance->logCallback) { + loggerInstance->logCallback(entryToProcess); + } + } + } +} + +void Logger::setLogLevel(LogLevel level) { + currentLogLevel = level; +} + +LogLevel Logger::getLogLevel() const { + return currentLogLevel; +} + +void Logger::setLogCallback(std::function callback) { + logCallback = callback; +} + +// --- NEW: Public methods now handle the variable arguments --- +void Logger::debug(const char* format, ...) { + va_list args; + va_start(args, format); + addLog(LOG_DEBUG, format, args); + va_end(args); +} + +void Logger::info(const char* format, ...) { + va_list args; + va_start(args, format); + addLog(LOG_INFO, format, args); + va_end(args); +} + +void Logger::warn(const char* format, ...) { + va_list args; + va_start(args, format); + addLog(LOG_WARN, format, args); + va_end(args); +} + +void Logger::error(const char* format, ...) { + va_list args; + va_start(args, format); + addLog(LOG_ERROR, format, args); + va_end(args); +} + +const LogEntry* Logger::getLogs(int& count) const { + count = logCount; + return logs; +} + +void Logger::clear() { + logCount = 0; + logIndex = 0; +} + +const char* Logger::levelToString(LogLevel level) const { + switch (level) { + case LOG_DEBUG: return "D"; + case LOG_INFO: return "I"; + case LOG_WARN: return "W"; + case LOG_ERROR: return "E"; + default: return "U"; + } +} + +Logger logger; \ No newline at end of file diff --git a/lib/log/logger.hpp b/lib/log/logger.hpp new file mode 100644 index 0000000..95f4889 --- /dev/null +++ b/lib/log/logger.hpp @@ -0,0 +1,68 @@ +#pragma once +#include +#include +#include // For std::function +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/task.h" + + +enum LogLevel { + LOG_DEBUG, + LOG_INFO, + LOG_WARN, + LOG_ERROR +}; + +struct LogEntry { + LogLevel level; + time_t timestamp; + char message[128]; +}; + +class Logger { +public: + Logger(); + ~Logger(); + + void begin(UBaseType_t taskPriority = 1, uint32_t taskStackSize = 2048); + + void setLogLevel(LogLevel level); + LogLevel getLogLevel() const; + + // Use the more flexible std::function for callbacks + void setLogCallback(std::function callback); + + // --- NEW: Variadic functions for printf-style formatting --- + void debug(const char* format, ...); + void info(const char* format, ...); + void warn(const char* format, ...); + void error(const char* format, ...); + + const LogEntry* getLogs(int& count) const; + void clear(); + const char* levelToString(LogLevel level) const; + +private: + static const int MAX_LOGS = 20; + LogEntry logs[MAX_LOGS]; + int logCount; + int logIndex; + LogLevel currentLogLevel; + + std::function logCallback; + + QueueHandle_t logQueue; + TaskHandle_t logTaskHandle; + static void logProcessingTask(void* instance); + void addLog(LogLevel level, const char* format, va_list args); +}; + +extern Logger logger; + +// --- OPTIONAL: Performance-enhancing macros --- +#define LOG_DEBUG(format, ...) if(logger.getLogLevel() <= LOG_DEBUG) { logger.debug(format, ##__VA_ARGS__); } +#define LOG_INFO(format, ...) if(logger.getLogLevel() <= LOG_INFO) { logger.info(format, ##__VA_ARGS__); } +#define LOG_WARN(format, ...) if(logger.getLogLevel() <= LOG_WARN) { logger.warn(format, ##__VA_ARGS__); } +#define LOG_ERROR(format, ...) if(logger.getLogLevel() <= LOG_ERROR) { logger.error(format, ##__VA_ARGS__); } \ No newline at end of file diff --git a/lib/ota/ota_manager.cpp b/lib/ota/ota_manager.cpp new file mode 100644 index 0000000..6a21fea --- /dev/null +++ b/lib/ota/ota_manager.cpp @@ -0,0 +1,94 @@ +#include "ota_manager.hpp" + +OTAManager::OTAManager() : enabled(false), status("Disabled") {} + +OTAManager::~OTAManager() {} + +void OTAManager::begin() { + ArduinoOTA.setHostname("RFID-Master-Client"); + + ArduinoOTA.onStart([this]() { + this->onStart(); + }); + + ArduinoOTA.onEnd([this]() { + this->onEnd(); + }); + + ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { + this->onProgress(progress, total); + }); + + ArduinoOTA.onError([this](ota_error_t error) { + this->onError(error); + }); + + LOG_INFO("OTA Manager initialized"); +} + +void OTAManager::enable() { + if (!enabled) { + ArduinoOTA.begin(); + enabled = true; + status = "Enabled"; + LOG_INFO("OTA enabled"); + } +} + +void OTAManager::disable() { + if (enabled) { + ArduinoOTA.end(); + enabled = false; + status = "Disabled"; + LOG_INFO("OTA disabled"); + } +} + +bool OTAManager::isEnabled() const { + return enabled; +} + +String OTAManager::getStatus() const { + return status; +} + +void OTAManager::onStart() { + status = "Starting OTA update..."; + LOG_INFO("OTA update started"); +} + +void OTAManager::onEnd() { + status = "OTA update completed"; + LOG_INFO("OTA update completed"); +} + +void OTAManager::onProgress(unsigned int progress, unsigned int total) { + status = "OTA progress: " + String(progress / (total / 100)) + "%"; + LOG_DEBUG("OTA progress: %u/%u", progress, total); +} + +void OTAManager::onError(ota_error_t error) { + String errorMsg; + switch (error) { + case OTA_AUTH_ERROR: + errorMsg = "Auth Failed"; + break; + case OTA_BEGIN_ERROR: + errorMsg = "Begin Failed"; + break; + case OTA_CONNECT_ERROR: + errorMsg = "Connect Failed"; + break; + case OTA_RECEIVE_ERROR: + errorMsg = "Receive Failed"; + break; + case OTA_END_ERROR: + errorMsg = "End Failed"; + break; + default: + errorMsg = "Unknown Error"; + break; + } + status = "OTA Error: " + errorMsg; + LOG_ERROR("OTA error: %s", errorMsg.c_str()); +} \ No newline at end of file diff --git a/lib/ota/ota_manager.hpp b/lib/ota/ota_manager.hpp new file mode 100644 index 0000000..1184d29 --- /dev/null +++ b/lib/ota/ota_manager.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class OTAManager { +public: + OTAManager(); + ~OTAManager(); + + void begin(); + void enable(); + void disable(); + bool isEnabled() const; + String getStatus() const; + +private: + bool enabled; + String status; + + void onStart(); + void onEnd(); + void onProgress(unsigned int progress, unsigned int total); + void onError(ota_error_t error); +}; \ No newline at end of file diff --git a/lib/rpc/library.json b/lib/rpc/library.json new file mode 100644 index 0000000..bd07111 --- /dev/null +++ b/lib/rpc/library.json @@ -0,0 +1,6 @@ +{ + "name": "ota", + "dependencies": { + "hardware": "*" + } +} diff --git a/lib/rpc/rpc.cpp b/lib/rpc/rpc.cpp new file mode 100644 index 0000000..ba5809b --- /dev/null +++ b/lib/rpc/rpc.cpp @@ -0,0 +1,445 @@ +#include "rpc.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LAST_SYNC_TIME_FILE "/last_sync.time" + +// ---- Constructor / Destructor ---- + +RPC::RPC() { + // NOTE: Initialization is done here. Ensure this global object is created + // AFTER core systems like LittleFS are initialized in setup(). + apiKey_[0] = '\0'; + baseUrl_[0] = '\0'; + + if (!accessLogQueue) { + accessLogQueue = xQueueCreate(100, sizeof(RpcAccessLogEntry)); + if (!accessLogQueue) { + LOG_ERROR("RPC: FATAL: Failed to create access log queue!"); + } + } + + syncTriggerSemaphore_ = xSemaphoreCreateBinary(); + if (!syncTriggerSemaphore_) { + LOG_ERROR("RPC: FATAL: Failed to create sync trigger semaphore!"); + } + + // Load the last sync time from persistent storage on startup. + this->lastSyncTime = loadLastSyncTime(); + LOG_INFO("RPC: Instance created. Loaded last sync timestamp: %s", HardwareRTC::toDateString(this->lastSyncTime).c_str()); +} + +RPC::~RPC() { + stopAutoSync(); + if (accessLogQueue) { + vQueueDelete(accessLogQueue); + accessLogQueue = nullptr; + } + if (syncTriggerSemaphore_) { + vSemaphoreDelete(syncTriggerSemaphore_); + syncTriggerSemaphore_ = nullptr; + } +} + +// ---- Public Methods ---- + +bool RPC::sync(bool forceDownload) { + if (isSyncing_) { + logger.warn("RPC: Sync aborted, another sync is already in progress."); + return false; + } + if (baseUrl_[0] == '\0') { + LOG_ERROR("RPC: Sync aborted (Base URL not set). Call startAutoSync first."); + return false; + } + if (WiFi.status() != WL_CONNECTED) { + LOG_ERROR("RPC: Sync aborted (no WiFi connection)."); + return false; + } + + isSyncing_ = true; + LOG_INFO("RPC: Starting synchronization process..."); + + bool pendingChanges = false; + bool syncSuccess = false; + + if (!syncLogsAndCheckForUpdates(&pendingChanges)) { + LOG_ERROR("RPC: Log synchronization failed."); + syncSuccess = false; + } else { + // Log sync was successful, now check if a DB download is needed. + if (pendingChanges || forceDownload) { + const char* dbPath = "/rfid.db"; + LOG_DEBUG("RPC: Pending changes: %d, forceDownload: %d", pendingChanges, forceDownload); + if (!downloadDatabaseToFile(dbPath)) { + LOG_ERROR("RPC: Failed to download database."); + syncSuccess = false; + } else { + syncSuccess = true; + } + } else { + LOG_DEBUG("RPC: No pending changes reported by server, skipping database download."); + syncSuccess = true; + } + } + + isSyncing_ = false; + if(syncSuccess) { + LOG_INFO("RPC: Synchronization completed successfully."); + } else { + LOG_ERROR("RPC: Synchronization failed."); + } + return syncSuccess; +} + +void RPC::addAccessLog(uint32_t rfidId) { + if (!accessLogQueue) { + return; // Queue not created, do nothing. + } + struct timeval tv; + RpcAccessLogEntry entry; + + entry.timestamp = HardwareRTC::getSystemTime(); + entry.rfidId = rfidId; + + if (xQueueSend(accessLogQueue, &entry, 0) != pdPASS) { + LOG_WARN("RPC: Access log queue is full, dropping entry."); + return; + } + + // --- TRIGGER LOGIC --- + // If the queue has reached the threshold and we can sync, trigger it. + if (uxQueueMessagesWaiting(accessLogQueue) >= LOG_SYNC_THRESHOLD && baseUrl_[0] != '\0') { + LOG_INFO("RPC: Queue size: %d. Triggering immediate sync.", uxQueueMessagesWaiting(accessLogQueue)); + xSemaphoreGive(syncTriggerSemaphore_); + } +} + +// ---- Auto-Sync Task Management ---- + +void RPC::startAutoSync(uint32_t intervalMs) { + stopAutoSync(); // Always stop any existing task to ensure a clean restart. + + if (baseUrl_[0] == '\0') { + LOG_ERROR("RPC: Cannot start auto-sync. Base URL is not configured."); + return; + } + + LOG_INFO("RPC: Starting auto-sync"); + syncIntervalMs = intervalMs; + autoSyncRunning = true; + + BaseType_t result = xTaskCreate( + autoSyncTask, "AutoSyncTask", 8192, this, 1, &autoSyncTaskHandle + ); + + if (result != pdPASS) { + LOG_ERROR("RPC: Failed to create auto-sync task."); + autoSyncRunning = false; + autoSyncTaskHandle = nullptr; + } else { + LOG_INFO("RPC: Auto-sync task started successfully."); + } +} + +void RPC::stopAutoSync() { + if (!autoSyncTaskHandle) { + return; + } + + LOG_INFO("RPC: Attempting to stop auto-sync task..."); + autoSyncRunning = false; // Signal the task to exit its loop + + // Wake the task if it's waiting on the semaphore + if (syncTriggerSemaphore_) { + xSemaphoreGive(syncTriggerSemaphore_); + } + + // Wait for the task to terminate itself (indicated by handle becoming nullptr) + uint32_t startTime = millis(); + while (autoSyncTaskHandle != nullptr && millis() - startTime < 1000) { + vTaskDelay(pdMS_TO_TICKS(50)); + } + + // If the task did not stop gracefully, force its deletion + if (autoSyncTaskHandle != nullptr) { + LOG_ERROR("RPC: Task did not stop gracefully. Forcing deletion."); + vTaskDelete(autoSyncTaskHandle); + autoSyncTaskHandle = nullptr; + } else { + LOG_INFO("RPC: Auto-sync task stopped gracefully."); + } +} + +void RPC::setApiKeyAndBaseUrl(const char* apiKey, const char* baseUrl) { + if (apiKey) { + strncpy(apiKey_, apiKey, sizeof(apiKey_) - 1); + apiKey_[sizeof(apiKey_) - 1] = '\0'; // Ensure null-termination + } else { + apiKey_[0] = '\0'; // Set to empty string + } + + if (baseUrl) { + strncpy(baseUrl_, baseUrl, sizeof(baseUrl_) - 1); + baseUrl_[sizeof(baseUrl_) - 1] = '\0'; + } else { + baseUrl_[0] = '\0'; + } +} + +// ---- Private Helper Methods ---- + +HttpResponse RPC::sendRequest(const String& url, const String& method, const uint8_t* payload, size_t payloadLen, const String& contentType) { + HttpResponse response; // Default httpCode is 0 + if (WiFi.status() != WL_CONNECTED) { + response.httpCode = -1; // Custom code for no WiFi + return response; + } + + HTTPClient http; + http.begin(url); + http.setTimeout(10000); + + if (apiKey_[0] != '\0') { + http.addHeader("x-device-key", apiKey_); + } + + if (method.equalsIgnoreCase("POST")) { + http.addHeader("Content-Type", contentType); + response.httpCode = http.POST(const_cast(payload), payloadLen); + } else { + response.httpCode = http.GET(); + } + + if (response.httpCode > 0) { + int len = http.getSize(); + if (len > 0) { + response.payload.resize(len); + WiFiClient* stream = http.getStreamPtr(); + stream->readBytes(response.payload.data(), len); + } + } else { + LOG_ERROR(("RPC: HTTP " + method + " " + url + " failed: " + http.errorToString(response.httpCode)).c_str()); + } + + http.end(); // Encapsulated resource management + return response; +} + + +bool RPC::syncLogsAndCheckForUpdates(bool* pendingChanges) { + // NOTE: This implementation assumes the logs should be sent with the sync request. + // The previous code collected logs but did not send them. This version does. + std::vector logBatch; + RpcAccessLogEntry entry; + + // Drain the queue to send all pending logs. + // To be more memory efficient on larger queues, this could be done in batches. + while (xQueueReceive(accessLogQueue, &entry, 0) == pdPASS) { + logBatch.push_back(entry); + } + + control_communication_SyncRequest request = control_communication_SyncRequest_init_zero; + request.has_lastSync = (lastSyncTime > 0); + request.lastSync = lastSyncTime; + + request.accessLogs.arg = &logBatch; + request.accessLogs.funcs.encode = [](pb_ostream_t* stream, const pb_field_t* field, void* const* arg) -> bool { + auto logs = static_cast*>(*arg); + if (!logs || logs->empty()) { + return true; // No logs to encode, return success + } + for (const auto& log : *logs) { + control_communication_SyncRequest_AccessLogsEntry entry = { log.timestamp, log.rfidId }; + if (!pb_encode_tag_for_field(stream, field)) { + return false; + } + if (!pb_encode_submessage(stream, control_communication_SyncRequest_AccessLogsEntry_fields, &entry)) { + return false; + } + } + return true; + }; + + uint8_t requestBuffer[512]; + pb_ostream_t ostream = pb_ostream_from_buffer(requestBuffer, sizeof(requestBuffer)); + if (!pb_encode(&ostream, control_communication_SyncRequest_fields, &request)) { + LOG_ERROR("RPC: Failed to encode sync request."); + return false; + } + + + String url = String(baseUrl_) + "device-communication/sync-binary"; + HttpResponse httpResponse = sendRequest(url, "POST", requestBuffer, ostream.bytes_written, "application/octet-stream"); + + if (httpResponse.httpCode != HTTP_CODE_OK) { + LOG_ERROR("RPC: Sync logs request failed with HTTP code %d", httpResponse.httpCode) + return false; + } + + // A successful connection was made. + lastSyncConnection = millis(); + + control_communication_SyncResponse response = control_communication_SyncResponse_init_zero; + pb_istream_t istream = pb_istream_from_buffer(httpResponse.payload.data(), httpResponse.payload.size()); + + if (!pb_decode(&istream, control_communication_SyncResponse_fields, &response)) { + LOG_ERROR("RPC: Failed to decode sync response."); + return false; + } + + // As per your design, only update lastSyncTime if the server reports pending changes. + if (response.pendingChanges) { + this->lastSyncTime = response.currentTime; + saveLastSyncTime(this->lastSyncTime); + } + + // Pass pendingChanges flag back to the caller. + if (pendingChanges) { + *pendingChanges = response.pendingChanges; + } + + if(newTimeCallback_){ + newTimeCallback_(response.currentTime); + } + return true; +} + +bool RPC::downloadDatabaseToFile(const char* filePath) { + String url = String(baseUrl_) + "device-communication/db"; + + if (WiFi.status() != WL_CONNECTED) { + return false; + } + + HTTPClient http; + http.begin(url); + http.setTimeout(10000); + if (apiKey_[0] != '\0') { + http.addHeader("x-device-key", apiKey_); + } + + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + LOG_ERROR(("RPC: DB download failed with HTTP code " + String(httpCode)).c_str()); + http.end(); + return false; + } + + lastSyncConnection = millis(); + + int len = http.getSize(); + if (len <= 0) { // Removed the multiple of 4 check, as it might be too restrictive. + LOG_ERROR(("RPC: Invalid database size: " + String(len) + " bytes.").c_str()); + http.end(); + return false; + } + + LOG_INFO(("RPC: Downloading database (" + String(len) + " bytes) to file '" + String(filePath) + "'...").c_str()); + + File dbFile = LittleFS.open(filePath, "w"); + if (!dbFile) { + LOG_ERROR(("RPC: Failed to open " + String(filePath) + " for writing.").c_str()); + http.end(); + return false; + } + + // Write the file content by streaming it from the WiFi client. + WiFiClient* stream = http.getStreamPtr(); + const size_t bufferSize = 1024; + uint8_t buffer[bufferSize]; + size_t written = 0; + + // Read from the stream in chunks until all bytes are received + while (http.connected() && (written < (size_t)len)) { + // Get available data size + size_t available = stream->available(); + if (available) { + // Read up to bufferSize bytes + int bytesRead = stream->read(buffer, std::min(bufferSize, available)); + // Write the chunk to the file + dbFile.write(buffer, bytesRead); + written += bytesRead; + } + } + + dbFile.close(); + http.end(); + + if (written != (size_t)len) { + LOG_ERROR("RPC: File download incomplete. Wrote %d of %d bytes.", (unsigned long)written, len); + LittleFS.remove(filePath); // Delete partial file + return false; + } + + LOG_INFO("RPC: Database downloaded successfully."); + return true; +} + +// ---- Persistence Methods ---- + +void RPC::saveLastSyncTime(time_t timestamp) { + File file = LittleFS.open(LAST_SYNC_TIME_FILE, "w"); + if (!file) { + LOG_ERROR("RPC: Failed to open last_sync.time for writing."); + return; + } + file.write(reinterpret_cast(×tamp), sizeof(timestamp)); + file.close(); +} + +time_t RPC::loadLastSyncTime() { + if (!LittleFS.exists(LAST_SYNC_TIME_FILE)) { + return 0; + } + File file = LittleFS.open(LAST_SYNC_TIME_FILE, "r"); + if (!file || file.size() != sizeof(time_t)) { + LOG_ERROR("RPC: Corrupt or unreadable timestamp file. Resetting to 0."); + if(file) file.close(); + LittleFS.remove(LAST_SYNC_TIME_FILE); + return 0; + } + + time_t timestamp = 0; + file.read(reinterpret_cast(×tamp), sizeof(timestamp)); + file.close(); + return timestamp; +} + +// ---- FreeRTOS Task ---- + +void RPC::autoSyncTask(void* pvParameters) { + RPC* rpc = static_cast(pvParameters); + + while (rpc->autoSyncRunning) { + // Wait for either the timer to expire OR the trigger semaphore to be given. + if (xSemaphoreTake(rpc->syncTriggerSemaphore_, pdMS_TO_TICKS(rpc->syncIntervalMs)) == pdTRUE) { + LOG_INFO("RPC: Sync triggered by log queue."); + } else { + LOG_INFO("RPC: Sync triggered by timer."); + } + + if (!rpc->autoSyncRunning) { + break; // Exit immediately if stop was called while waiting + } + + rpc->sync(); // Call sync without forcing a download + } + + // Safer handshake to signal graceful shutdown + rpc->autoSyncTaskHandle = nullptr; + + vTaskDelete(NULL); // Task deletes itself +} + +RPC rpc; \ No newline at end of file diff --git a/lib/rpc/rpc.hpp b/lib/rpc/rpc.hpp new file mode 100644 index 0000000..c25bbac --- /dev/null +++ b/lib/rpc/rpc.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include +#include +#include // Required for semaphores +#include + + +/** + * @struct RpcAccessLogEntry + * @brief Holds a single access log record to be queued for synchronization. + */ +struct RpcAccessLogEntry { + time_t timestamp; + uint32_t rfidId; +}; + +/** + * @struct HttpResponse + * @brief Encapsulates the result of an HTTP request. + */ +struct HttpResponse { + int httpCode = 0; + std::vector payload; +}; + +/** + * @class RPC + * @brief Manages remote procedure calls (RPC) for device synchronization. + * * This class handles queuing access logs, synchronizing them with a server, + * and downloading updated data. It uses a background FreeRTOS task for + * automatic, periodic synchronization. + */ +class RPC { +public: + RPC(); + ~RPC(); + + /** + * @brief Performs a full, one-time synchronization cycle. + * @param forceDownload If true, the database will be downloaded even if the + * server reports no pending changes. + * @return true on success, false on failure. + */ + bool sync(bool forceDownload = false); + + /** + * @brief Adds an access log entry to the queue for the next sync cycle. + * If the queue reaches a threshold, a sync may be triggered. + * @param rfidId The RFID ID that was accessed. + */ + void addAccessLog(uint32_t rfidId); + + /** + * @brief Starts or restarts the automatic background synchronization task. + * @param intervalMs The sync interval in milliseconds. + */ + void startAutoSync(uint32_t intervalMs); + + /** + * @brief Stops the automatic synchronization task gracefully. + */ + void stopAutoSync(); + + /** + * @brief Sets the API key and base URL for server communication. + */ + void setApiKeyAndBaseUrl(const char* apiKey, const char* baseUrl); + + /** + * @brief Gets the timestamp (from millis()) of the last successful communication. + * @return The last connection timestamp, or 0 if never connected. + */ + time_t getLastConnectTime() const { return lastSyncConnection; } + + /** + * @brief Checks if a sync operation is currently in progress. + * @return true if syncing, false otherwise. + */ + bool isSyncing() const { return isSyncing_; } + + /** + * @brief Sets a callback function to get the current time in seconds since epoch. + * This is used to set the lastSync time during sync operations. + * @param callback A function that returns the current time as time_t. + */ + void setNewTimeCallback(std::function callback) { + newTimeCallback_ = callback; + } + +private: + // --- Member Variables --- + + // Configuration + char apiKey_[65]; // 64 chars + 1 null-terminator + char baseUrl_[257]; // 256 chars + 1 null-terminator + + // State + time_t lastSyncTime = 0; + time_t lastSyncConnection = 0; + bool isSyncing_ = false; + + // FreeRTOS components + QueueHandle_t accessLogQueue = nullptr; + TaskHandle_t autoSyncTaskHandle = nullptr; + SemaphoreHandle_t syncTriggerSemaphore_ = nullptr; + uint32_t syncIntervalMs = 300000; // Default: 5 minutes + bool autoSyncRunning = false; + + // Constants + static constexpr uint8_t LOG_SYNC_THRESHOLD = 20; + + // --- Private Helper Methods --- + + /** + * @brief Sends an HTTP request and returns the full response. + * This method is self-contained and manages the HTTPClient lifecycle. + */ + HttpResponse sendRequest(const String& url, const String& method = "GET", + const uint8_t* payload = nullptr, size_t payloadLen = 0, + const String& contentType = ""); + + /** + * @brief Streams an HTTP GET response directly to a file on LittleFS. + * This is memory-efficient for large files like a database. + */ + bool downloadDatabaseToFile(const char* filePath); + + /** + * @brief Sends queued logs to the server and checks if updates are pending. + * This is the first step in the sync process. + */ + bool syncLogsAndCheckForUpdates(bool* pendingChanges); + + /** + * @brief Saves the last successful database download timestamp to flash. + */ + void saveLastSyncTime(time_t timestamp); + + /** + * @brief Loads the last successful database download timestamp from flash. + */ + time_t loadLastSyncTime(); + + /** + * @brief The static function that runs as the FreeRTOS background task. + */ + static void autoSyncTask(void* pvParameters); + + std::function newTimeCallback_; +}; + +extern RPC rpc; \ No newline at end of file diff --git a/lib/tft/tft.cpp b/lib/tft/tft.cpp new file mode 100644 index 0000000..94117e3 --- /dev/null +++ b/lib/tft/tft.cpp @@ -0,0 +1,3 @@ +#include "tft.hpp" + +TFT tft; \ No newline at end of file diff --git a/lib/tft/tft.hpp b/lib/tft/tft.hpp new file mode 100644 index 0000000..f4d5a7a --- /dev/null +++ b/lib/tft/tft.hpp @@ -0,0 +1,48 @@ +#include +#include // Wichtig, um von der Print-Klasse erben zu können + +#define TFT_CS 5 +#define TFT_RST 4 +#define TFT_DC 19 +#define TFT_MOSI 23 +#define TFT_SCLK 18 + +class TFT : public Print { +public: + void setup() { + // Initialize the TFT display + // Setup SPI and display parameters + tft.initR(INITR_144GREENTAB); // Initialize a ST7735S chip, green tab + tft.fillScreen(ST77XX_BLACK); + tft.setRotation(1); // Landscape orientation + tft.setTextColor(ST77XX_WHITE); + tft.setTextSize(1); + //tft.setScrollDefinition(0, tft.height(), 0); + tft.setCursor(0, 0); + } + + + virtual size_t write(uint8_t c) override { + return tft.write(c); + } + + Adafruit_ST7735& getGfx() { + return tft; + } + + void checkCursor() { + if (tft.getCursorY() >= tft.height()) { + tft.setCursor(0, 0); + tft.fillScreen(ST77XX_BLACK); + } + } + + void resetCursor() { + tft.fillScreen(ST77XX_BLACK); + tft.setCursor(0, 0); + } +private: + Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST); +}; + +extern TFT tft; // Deine globale Instanz \ No newline at end of file diff --git a/partitions_custom.csv b/partitions_custom.csv new file mode 100644 index 0000000..9e7bafe --- /dev/null +++ b/partitions_custom.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +ota_0, app, ota_0, 0x10000, 0x160000, +ota_1, app, ota_1, 0x170000, 0x160000, +spiffs, data, spiffs, 0x2D0000, 0x120000, +coredump, data, coredump, 0x3F0000, 0x10000, \ No newline at end of file diff --git a/partitions_custom_ota.csv b/partitions_custom_ota.csv new file mode 100644 index 0000000..b83ac14 --- /dev/null +++ b/partitions_custom_ota.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +ota_0, app, ota_0, 0x10000, 0x270000, +spiffs, data, spiffs, 0x280000, 0x170000, +coredump, data, coredump,0x3F0000, 0x10000, diff --git a/partitions_prod.csv b/partitions_prod.csv new file mode 100644 index 0000000..9e956fa --- /dev/null +++ b/partitions_prod.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +ota_0, app, ota_0, 0x10000, 0x160000, +ota_1, app, ota_1, 0x170000, 0x160000, +spiffs, data, spiffs, 0x2D0000, 0x130000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..0555768 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,94 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = dev + +[env] +platform = espressif32 +board = az-delivery-devkit-v4 +framework = arduino +board_build.tasks_stack_size = 8192 +lib_deps = + adafruit/Adafruit ST7735 and ST7789 Library@^1.11.0 + https://github.com/avrilogic/nanopb.git#fix/esp32-pio-include-path + ESP32Async/AsyncTCP@^3.4.8 + ESP32Async/ESPAsyncWebServer@^3.8.1 + rweather/Crypto@^0.4.0 + adafruit/Adafruit NeoPixel@^1.15.1 + ArduinoOTA +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +board_build.filesystem = littlefs +#lib_extra_dirs = .pio/libdeps/dev/Nanopb +build_unflags = -std=gnu++11 -fno-rtti +build_flags = + -std=gnu++17 + -ffunction-sections + -fdata-sections + -Wl,--gc-sections + -D CONFIG_BT_ENABLED=n + -D CONFIG_BLUEDROID_ENABLED=n +custom_nanopb_protos = + + + + + + + + + + + custom_nanopb_options = + --error-on-unmatched + +[env:dev] +build_type = debug +board_build.partitions = partitions_custom.csv +build_flags = + ${env.build_flags} + -Os + -D DEBUG_MODE + -D CORE_DEBUG_LEVEL=4 + -D LOG_LOCAL_LEVEL=4 + -fstack-protector-strong + + -D CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH=y + -D CONFIG_ESP32_COREDUMP_DATA_FORMAT_ELF=y + -D CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y + + -D CONFIG_ESP_TASK_WDT=y + -D CONFIG_ESP_TASK_WDT_TIMEOUT_S=15 + -D CONFIG_ESP_TASK_WDT_PANIC=y + + +[env:prod] +build_type = release +board_build.partitions = partitions_prod.csv +build_flags = + ${env.build_flags} + + ; --- CORE OPTIMIZATIONS --- + -Os ; <-- 1. Optimize for SIZE, not speed + -fno-exceptions ; <-- 2. Disable C++ exceptions + + ; --- DISABLE BLUETOOTH --- + -D CONFIG_BT_ENABLED=n ; <-- 2. Disable Bluetooth stack + -D CONFIG_BLUEDROID_ENABLED=n ; <-- 2. Disable classic Bluetooth + + ; --- STRIP DEBUG SYMBOLS (already correct) --- + -D NDEBUG + -D CORE_DEBUG_LEVEL=0 + -D LOG_LOCAL_LEVEL=0 + + ; --- Security vs. Size Trade-off (optional) --- + -fstack-protector ; Keep this for security. Removing it saves a tiny amount of space. + + ; --- OTHER FLAGS (already correct) --- + -D CONFIG_ESP32_ENABLE_COREDUMP=n + -D CONFIG_ESP_TASK_WDT=y + -D CONFIG_ESP_TASK_WDT_TIMEOUT_S=60 + -D CONFIG_ESP_TASK_WDT_PANIC=n \ No newline at end of file diff --git a/proto/control_communication.options b/proto/control_communication.options new file mode 100644 index 0000000..9daa492 --- /dev/null +++ b/proto/control_communication.options @@ -0,0 +1,9 @@ +# Nanopb options for control_communication.proto +# Adjust these limits as needed for memory/performance. + +# RfidId +control_communication.RfidId.value max_length:8 + +# DeviceCommunicationSyncRequest +# Limit number of log entries and their key/value sizes. +control_communication.SyncRequest.logs max_count:50 \ No newline at end of file diff --git a/proto/control_communication.proto b/proto/control_communication.proto new file mode 100644 index 0000000..7bd4c0c --- /dev/null +++ b/proto/control_communication.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; +package control_communication; + +message RfidId { + uint32 value = 1; +} + +message SyncResponse { + int64 currentTime = 1; + bool pendingChanges = 2; +} + +message SyncRequest { + optional int64 lastSync = 1; + map accessLogs = 2; +} \ No newline at end of file diff --git a/proto/device.options b/proto/device.options new file mode 100644 index 0000000..4e293e8 --- /dev/null +++ b/proto/device.options @@ -0,0 +1,15 @@ +# Nanopb options for device.proto + +# DeviceInfo +device.DeviceInfo.device_id max_length:18 +device.DeviceInfo.firmware_version max_length:8 +device.DeviceInfo.hardware_version max_length:8 +device.DeviceInfo.sta_ip max_length:16 +device.DeviceInfo.sta_gateway max_length:16 +device.DeviceInfo.ap_ip max_length:16 + +# DeviceLogEntry +device.DeviceLogEntry.message max_length:128 + +# DeviceStatus +device.DeviceStatus.logs max_length:20 \ No newline at end of file diff --git a/proto/device.proto b/proto/device.proto new file mode 100644 index 0000000..e0eb209 --- /dev/null +++ b/proto/device.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +package device; + +// WiFi connection state enumeration +enum WifiConnectionState { + WIFI_DISCONNECTED = 0; + WIFI_CONNECTED = 1; + WIFI_CONNECTING = 2; + WIFI_FAILED = 3; +} + +// Device information +message DeviceInfo { + string device_id = 1; + string firmware_version = 2; + string hardware_version = 3; + uint32 uptime_seconds = 4; + // STA mode info + WifiConnectionState sta_connection_state = 5; + string sta_ip = 6; + string sta_gateway = 7; + int32 sta_signal_strength = 8; // Signal strength in dBm + // AP mode info + WifiConnectionState ap_connection_state = 9; + string ap_ip = 10; + uint32 ap_client_count = 11; +} + +enum DeviceLogLevel { + LOG_LEVEL_DEBUG = 0; + LOG_LEVEL_INFO = 1; + LOG_LEVEL_WARN = 2; + LOG_LEVEL_ERROR = 3; +} + +message DeviceLogEntry { + int64 timestamp = 1; // Unix timestamp in milliseconds + DeviceLogLevel level = 2; + string message = 3; +} + +message DeviceStatus { + DeviceInfo info = 1; + repeated DeviceLogEntry logs = 2; +} \ No newline at end of file diff --git a/proto/hardware.options b/proto/hardware.options new file mode 100644 index 0000000..ce35ac4 --- /dev/null +++ b/proto/hardware.options @@ -0,0 +1,12 @@ +# Nanopb options for hardware.proto + +# LED configuration +hardware.FadeParams.colors max_count:5 + +hardware.SensorOTAEnable.ssid max_length:32 +hardware.SensorOTAEnable.password max_length:64 +hardware.SensorOTAEnable.static_ip max_length:16 +hardware.SensorOTAEnable.netmask max_length:16 +hardware.SensorOTAEnable.gateway max_length:16 +hardware.SensorOTAEnableResponse.ip_address max_length:16 +hardware.SensorOTAEnableResponse.error_message max_length:64 \ No newline at end of file diff --git a/proto/hardware.proto b/proto/hardware.proto new file mode 100644 index 0000000..7c9854a --- /dev/null +++ b/proto/hardware.proto @@ -0,0 +1,104 @@ +syntax = "proto3"; + +package hardware; + +// LED animation types +enum LedAnimation { + LED_ANIMATION_STATIC = 0; + LED_ANIMATION_PULSE = 1; + LED_ANIMATION_FADE = 2; + LED_ANIMATION_FLICKER = 3; +} + +// Hardware configuration +message HardwareConfig { + uint32 hold_duration_ms = 1; + bool override = 2; + uint32 relay_pin = 3; + uint32 sensor_rx_pin = 4; + uint32 sensor_tx_pin = 5; + LedConfig on_open_led = 6; + LedConfig default_led = 7; + LedConfig on_invalid_led = 8; + bool enable_serial_sensor = 9; + // repeated char sensor_api_key = 10; + // bool enable_ws_sensor = 11; + LedConfig on_override_open_led = 12; + LedConfig on_interception_led = 13; +} + +// LED configuration +message LedConfig { + // General properties that apply to all animations + uint32 brightness = 1; // 0-255 + uint32 duration_ms = 2; // 0 for indefinite + + oneof animation_params { + StaticParams static_params = 3; + PulseParams pulse_params = 4; + FadeParams fade_params = 5; + FlickerParams flicker_params = 6; + } +} + +// Define the specific parameters for each animation type +message StaticParams { + uint32 color = 1; +} +message PulseParams { + uint32 color = 1; + uint32 speed_ms = 2; +} +message FadeParams { + repeated uint32 colors = 1; // Fade between these colors + uint32 speed_ms = 2; +} +message FlickerParams { + uint32 color = 1; + uint32 intensity = 2; // e.g., 0-100 +} + +message RfidReading { + uint32 card_id = 1; +} + +message SensorToControlMessage { + uint32 sensor_id = 1; + oneof payload { + RfidReading rfid_reading = 2; + SensorOTAEnableResponse ota_response = 3; + // Add other sensor message types as needed + } +} + +message SensorOTAEnable { + string ssid = 1; + string password = 2; + uint32 timeout_seconds = 3; + bool as_station_mode = 4; + bool use_static_ip = 5; + string static_ip = 6; + string netmask = 7; + string gateway = 8; +} + +message SensorRestart { +} + + +message SensorOTAEnableResponse { + bool success = 1; + string ip_address = 2; + string error_message = 3; +} + + +message ControlToSensorMessage { + uint32 control_id = 1; + oneof payload { + LedConfig led_config = 2; + SensorOTAEnable ota_enable = 3; + SensorRestart restart = 4; + // Add other control message types as needed + } +} \ No newline at end of file diff --git a/proto/settings.options b/proto/settings.options new file mode 100644 index 0000000..070df47 --- /dev/null +++ b/proto/settings.options @@ -0,0 +1,12 @@ +# Nanopb options for settings.proto + +# SettingsData +settings.SettingsData.sync_server_url max_length:256 +settings.SettingsData.device_api_key max_length:64 +settings.SettingsData.station_ssid max_length:32 +settings.SettingsData.station_password max_length:64 +settings.SettingsData.ap_ssid max_length:32 +settings.SettingsData.ap_password max_length:64 + +# UpdateSettingsResponse +settings.UpdateSettingsResponse.error max_length:96 \ No newline at end of file diff --git a/proto/settings.proto b/proto/settings.proto new file mode 100644 index 0000000..564c602 --- /dev/null +++ b/proto/settings.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package settings; + +// WiFi mode enumeration +enum WifiMode { + WIFI_MODE_UNSPECIFIED = 0; + WIFI_MODE_STATION = 1; + WIFI_MODE_AP = 2; + WIFI_MODE_AP_STATION = 3; +} + +// Log level enumeration +enum LogLevel { + LOG_LEVEL_DEBUG = 0; + LOG_LEVEL_INFO = 1; + LOG_LEVEL_WARN = 2; + LOG_LEVEL_ERROR = 3; +} + +// Settings data structure +message SettingsData { + string sync_server_url = 1; + string device_api_key = 2; + uint32 sync_interval_seconds = 3; + bool auto_sync = 4; + WifiMode wifi_mode = 5; + string station_ssid = 6; + string station_password = 7; + string ap_ssid = 8; + string ap_password = 9; + uint32 ap_channel = 10; + bool enable_fallback_ap = 11; + int64 updated_at = 12; // Unix timestamp in milliseconds + uint32 version = 13; // Settings version for change tracking + LogLevel log_level = 14; +} + +// Request to get current settings +message GetSettingsRequest { +} + +// Response with current settings +message GetSettingsResponse { + SettingsData settings = 1; +} + +// Request to update settings +message UpdateSettingsRequest { + SettingsData settings = 1; +} + +// Response to settings update +message UpdateSettingsResponse { + bool success = 1; + optional string error = 2; +} \ No newline at end of file diff --git a/proto/webui.options b/proto/webui.options new file mode 100644 index 0000000..859026d --- /dev/null +++ b/proto/webui.options @@ -0,0 +1,36 @@ +# Nanopb options for webui.proto + +# Use callback-based strings for dynamic memory allocation +# No max_length specified to enable callbacks +webui.WebUiLoginRequest.username max_length:32 +webui.WebUiLoginRequest.password max_length:64 + +# WebUiLoginResponse +webui.WebUiLoginResponse.token max_length:128 +webui.WebUiLoginResponse.error max_length:96 + +# WebUiUser +webui.WebUiUser.username max_length:32 +webui.WebUiUser.password_hash max_length:32 + +# GetUsersResponse +webui.GetUsersResponse.usernames max_length:32 + +# AddUserRequest +webui.AddUserRequest.username max_length:32 +webui.AddUserRequest.password max_length:64 + +# DeleteUserRequest +webui.DeleteUserRequest.username max_length:32 + +# UpdateUserPasswordRequest +webui.UpdateUserPasswordRequest.username max_length:32 +webui.UpdateUserPasswordRequest.new_password max_length:64 + +# Response errors +webui.AddUserResponse.error max_length:96 +webui.DeleteUserResponse.error max_length:96 +webui.UpdateUserPasswordResponse.error max_length:96 + +# WebUiAuthCheckResponse +webui.WebUiAuthCheckResponse.username max_length:32 \ No newline at end of file diff --git a/proto/webui.proto b/proto/webui.proto new file mode 100644 index 0000000..e7ec45b --- /dev/null +++ b/proto/webui.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +package webui; + +// Login request from Web UI +message WebUiLoginRequest { + string username = 1; // 1-32 chars + string password = 2; // 1-64 chars (plain or hashed) +} + +// Login response +message WebUiLoginResponse { + bool success = 1; + optional string token = 2; // session/JWT when success + optional string error = 3; // error message when failed +} + +// Logout response +message WebUiLogoutResponse { + bool success = 1; + string error = 2; +} + +// User data structure +message WebUiUser { + string username = 1; // 1-32 chars + bytes password_hash = 2; // 32 bytes (SHA-256) +} + +// User list (max 5 users) +message WebUiUserList { + repeated WebUiUser users = 1; +} + +// Request to get users +message GetUsersRequest { +} + +// Response with users +message GetUsersResponse { + repeated string usernames = 1; // List of usernames +} + +// Request to add user +message AddUserRequest { + string username = 1; + string password = 2; +} + +// Response to add user +message AddUserResponse { + bool success = 1; + optional string error = 2; // e.g., "table full", "user exists" +} + +// Request to delete user +message DeleteUserRequest { + string username = 1; +} + +// Response to delete user +message DeleteUserResponse { + bool success = 1; + optional string error = 2; // e.g., "user not found" +} + +// Request to update user password +message UpdateUserPasswordRequest { + string username = 1; + string new_password = 2; +} + +// Response to update user password +message UpdateUserPasswordResponse { + bool success = 1; + optional string error = 2; // e.g., "user not found" +} + +// Auth check response +message WebUiAuthCheckResponse { + bool authenticated = 1; + optional string username = 2; +} \ No newline at end of file diff --git a/src/device/device_manager.cpp b/src/device/device_manager.cpp new file mode 100644 index 0000000..bec916d --- /dev/null +++ b/src/device/device_manager.cpp @@ -0,0 +1,255 @@ +#include "device_manager.hpp" +#include "rfid.hpp" // For rfidDB +#include +#include + +DeviceManager::DeviceManager() { + // Constructor +} + +DeviceManager::~DeviceManager() { + if (taskHandle != nullptr) { + vTaskDelete(taskHandle); + taskHandle = nullptr; + } + if (mutex != nullptr) { + vSemaphoreDelete(mutex); + mutex = nullptr; + } +} + +void DeviceManager::begin() { + if (taskHandle == nullptr) { + scans = 0; // Reset scan count on start + // Initialize relay pin + setRelayPin(relayPin); + // Create mutex + mutex = xSemaphoreCreateMutex(); + // Create the FreeRTOS task + xTaskCreate( + taskFunction, // Task function + "DeviceManagerTask", // Name + 4096, // Stack size + this, // Parameter + 1, // Priority + &taskHandle // Task handle + ); + } +} + +void DeviceManager::end() { + if (taskHandle != nullptr) { + vTaskDelete(taskHandle); + taskHandle = nullptr; + } + // Reset state + xSemaphoreTake(mutex, portMAX_DELAY); + isOpen = false; + isOverridden = false; + openTillTime = 0; + xSemaphoreGive(mutex); +} + +void DeviceManager::setOpenDuration(uint32_t ms) { + openDurationMs = ms; +} + +void DeviceManager::processRfid(uint32_t cardId) { + if(isOverridden) { + LOG_DEBUG("DeviceManager: RFID card scanned while overridden, ignoring: %08X", cardId); + return; // Ignore scans while overridden + } + scans++; // Increment scan count + if (rfidDB.contains(cardId)) { + LOG_DEBUG("DeviceManager: Valid RFID card scanned: %08X", cardId); + // Valid card + xSemaphoreTake(mutex, portMAX_DELAY); + if (!isOverridden) { + isOpen = true; + openTillTime = millis() + openDurationMs; + if (onDoorOpenCb) { + onDoorOpenCb(); + } + } + setLEDState(LEDState::ON_OPEN); + xSemaphoreGive(mutex); + // If overridden, do nothing or perhaps log + } else { + LOG_DEBUG("DeviceManager: Invalid RFID card scanned: %08X", cardId); + setLEDState(LEDState::ON_INVALID); + // Invalid card + if (onInvalidCardCb) { + onInvalidCardCb(); + } + } +} + +void DeviceManager::overrideOpen(bool open) { + LOG_DEBUG("DeviceManager::overrideOpen called with open=%d, current isOverridden=%d, isOpen=%d", open, isOverridden, isOpen); + xSemaphoreTake(mutex, portMAX_DELAY); + if (open) { + if (!isOverridden) { + LOG_DEBUG("DeviceManager: Enabling override mode"); + setLEDState(LEDState::ON_OVERRIDE_OPEN); + openTillTime = 0; // Override keeps it open indefinitely + isOverridden = true; + isOpen = true; + if (onDoorOpenCb) { + onDoorOpenCb(); + } + } else { + LOG_DEBUG("DeviceManager: Override already enabled"); + } + } else { + if (isOverridden) { + LOG_DEBUG("DeviceManager: Disabling override mode"); + setLEDState(LEDState::ON_DEFAULT); + isOpen = false; + isOverridden = false; + if (onDoorCloseCb) { + onDoorCloseCb(); + } + } else { + LOG_DEBUG("DeviceManager: Override already disabled"); + } + } + LOG_DEBUG("DeviceManager::overrideOpen finished, isOverridden=%d, isOpen=%d", isOverridden, isOpen); + xSemaphoreGive(mutex); +} + +DeviceManager::State DeviceManager::getState() const { + xSemaphoreTake(mutex, portMAX_DELAY); + State state = {isOpen, isOverridden}; + xSemaphoreGive(mutex); + return state; +} + +uint32_t DeviceManager::getScanCount() const { + return scans; +} + +void DeviceManager::onDoorOpen(std::function callback) { + onDoorOpenCb = callback; +} + +void DeviceManager::onDoorClose(std::function callback) { + onDoorCloseCb = callback; +} + +void DeviceManager::onInvalidCard(std::function callback) { + onInvalidCardCb = callback; +} + +void DeviceManager::onLEDStateChange(std::function callback) +{ + onLEDStateChangeCb = callback; +} + +void DeviceManager::setRelayPin(int pin) { + if(pin == relayPin && relayInitialized) { + return; // No change + } + relayPin = pin; + pinMode(relayPin, OUTPUT); + digitalWrite(relayPin, isOpen ? HIGH : LOW); + relayInitialized = true; +} + +void DeviceManager::setRelay(bool state) { + if (relayInitialized) { + digitalWrite(relayPin, state ? HIGH : LOW); + } +} + +void DeviceManager::setLEDState(LEDState state) { + if(ledState == state) { + return; // No change + } + resetToDefaultLEDStateTimer = 0; + + ledState = state; + ledStateChanged = true; +} + +void DeviceManager::applyLEDState() { + bool apply =false; + unsigned long now = millis(); + if(ledStateChanged) { + apply = true; + ledStateChanged = false; + }else if(resetToDefaultLEDStateTimer > 0 && now >= resetToDefaultLEDStateTimer) { + apply = true; + resetToDefaultLEDStateTimer = 0; + ledState = LEDState::ON_DEFAULT; + } + if (apply && onLEDStateChangeCb) { + hardware_HardwareConfig currentConfig = hardwareConfigRepo.getData(); + hardware_ControlToSensorMessage message = hardware_ControlToSensorMessage_init_zero; + message.which_payload = hardware_ControlToSensorMessage_led_config_tag; + switch (ledState) + { + case LEDState::ON_DEFAULT: + // Set to default LED config + message.payload.led_config = currentConfig.default_led; + break; + case LEDState::ON_OPEN: + // Set to open LED config + resetToDefaultLEDStateTimer = millis() + openDurationMs; + message.payload.led_config = currentConfig.on_open_led; + break; + case LEDState::ON_OVERRIDE_OPEN: + // Set to override open LED config + message.payload.led_config = currentConfig.on_override_open_led; + break; + case LEDState::ON_INVALID: + // Set to invalid LED config + resetToDefaultLEDStateTimer = millis() + 2000; // 3 seconds + message.payload.led_config = currentConfig.on_invalid_led; + break; + case LEDState::ON_INTERCEPT: + // Set to intercept LED config + message.payload.led_config = currentConfig.on_interception_led; + break; + default: + break; + } + onLEDStateChangeCb(message); + } +} + + +void DeviceManager::taskFunction(void* param) { + DeviceManager* self = static_cast(param); + self->runTask(); +} + +void DeviceManager::runTask() { + while (true) { + xSemaphoreTake(mutex, portMAX_DELAY); + unsigned long now = millis(); + // Check if it's time to close the door + if (!isOverridden && openTillTime > 0 && now >= openTillTime) { + LOG_DEBUG("DeviceManager: Closing door due to timeout, openTillTime=%lu, now=%lu", openTillTime, now); + isOpen = false; + openTillTime = 0; + if (onDoorCloseCb) { + onDoorCloseCb(); + } + } + + // Control relay based on door state + if (isOpen != previousIsOpen) { + LOG_DEBUG("DeviceManager: Setting relay to %d (isOpen=%d, isOverridden=%d)", isOpen, isOpen, isOverridden); + setRelay(isOpen); + previousIsOpen = isOpen; + } + + // Apply LED state changes if needed + applyLEDState(); + + xSemaphoreGive(mutex); + + // Delay for 100ms + vTaskDelay(pdMS_TO_TICKS(100)); + } +} \ No newline at end of file diff --git a/src/device/device_manager.hpp b/src/device/device_manager.hpp new file mode 100644 index 0000000..3ecef16 --- /dev/null +++ b/src/device/device_manager.hpp @@ -0,0 +1,166 @@ +#pragma once + +#define RELAY_PIN 2 + +#include +#include +#include "rfid.hpp" +#include "hardware.pb.h" + +/** + * @enum DoorState + * @brief Internal states for the door locking mechanism. + */ +enum class DoorState { + CLOSED, + OPENING, + OPEN, + CLOSING, + OVERRIDDEN_OPEN +}; + +/** + * @enum LEDState + * @brief States for LED indication. + */ +enum class LEDState { + ON_DEFAULT, + ON_OPEN, + ON_INVALID, + ON_OVERRIDE_OPEN, + ON_INTERCEPT +}; + +/** + * @class DeviceManager + * @brief Manages the door locking mechanism with RFID processing, overrides, and timing. + * + * This class handles the state of a door lock, processes RFID cards, manages open durations, + * and provides callbacks for various events. It runs its own FreeRTOS task for timing. + */ +class DeviceManager { +public: + /** + * @struct State + * @brief Represents the current state of the device. + */ + struct State { + bool currentlyOpen; + bool currentlyOverridden; + }; + + DeviceManager(); + ~DeviceManager(); + + /** + * @brief Initializes the device manager and starts the background task. + */ + void begin(); + + /** + * @brief Stops the device manager and its background task gracefully. + */ + void end(); + + /** + * @brief Sets the duration the door stays open after a successful RFID card. + * @param ms Duration in milliseconds. + */ + void setOpenDuration(uint32_t ms); + + /** + * @brief Processes an RFID card read event. + * @param cardId The RFID card ID read. + */ + void processRfid(uint32_t cardId); + + /** + * @brief Overrides the door state. + * @param open True to override open, false to override close. + */ + void overrideOpen(bool open); + + /** + * @brief Gets the current state of the device. + * @return State struct with currentlyOpen and currentlyOverridden. + */ + State getState() const; + + /** + * @brief Gets the number of RFID scans since the device started. + * @return The scan count. + */ + uint32_t getScanCount() const; + + /** + * @brief Sets the callback for door open event. + */ + void onDoorOpen(std::function callback); + + /** + * @brief Sets the callback for door close event. + */ + void onDoorClose(std::function callback); + + /** + * @brief Sets the callback for invalid card event. + */ + void onInvalidCard(std::function callback); + + /** + * @brief Sets the callback for LED state change event. + * @param callback Function to call with the new LED state message. + */ + void onLEDStateChange(std::function callback); + + /** + * @brief Sets the relay pin number. + * @param pin The GPIO pin number for the relay. + */ + void setRelayPin(int pin); + + /** + * @brief Controls the relay state. + * @param state True to activate relay (HIGH), false to deactivate (LOW). + */ + void setRelay(bool state); + + /** + * @brief Sets the LED state and triggers the LED state change callback. + * @param state The new LEDState to set. + */ + void setLEDState(LEDState state); + +private: + // Internal state + volatile bool isOpen = false; + volatile bool isOverridden = false; + uint32_t openDurationMs = 5000; // Default 5 seconds + uint32_t openTillTime = 0; + uint32_t scans = 0; + + // Relay control + volatile int relayPin = RELAY_PIN; + volatile bool relayInitialized = false; + volatile bool previousIsOpen = false; + + // Mutex for thread safety + SemaphoreHandle_t mutex; + + // Callbacks + std::function onDoorOpenCb = nullptr; + std::function onDoorCloseCb = nullptr; + std::function onInvalidCardCb = nullptr; + std::function onLEDStateChangeCb = nullptr; + + // Task + TaskHandle_t taskHandle = nullptr; + static void taskFunction(void* param); + void runTask(); + + // LED State + LEDState ledState = LEDState::ON_DEFAULT; + uint32_t resetToDefaultLEDStateTimer = 0; + void applyLEDState(); + volatile bool ledStateChanged = false; +}; \ No newline at end of file diff --git a/src/device/device_status.cpp b/src/device/device_status.cpp new file mode 100644 index 0000000..1cfbeb4 --- /dev/null +++ b/src/device/device_status.cpp @@ -0,0 +1,86 @@ +#include "device_status.hpp" +#include +#include +#include + +extern WiFiManager wifiManager; + +// Version constants +const char* FIRMWARE_VERSION = "1.0.0"; +const char* HARDWARE_VERSION = "1.0"; + +DeviceStatus::DeviceStatus() { + status = device_DeviceStatus_init_zero; +} + +DeviceStatus::~DeviceStatus() { + // Clean up if needed +} + +void DeviceStatus::setInfo(const device_DeviceInfo& info) { + status.has_info = true; + status.info = info; +} + +bool DeviceStatus::writeStreamCallback(pb_ostream_t *stream, const uint8_t *buf, size_t count) { + StreamState* state = (StreamState*)stream->state; + return state->stream->write(buf, count) == count; +} + +const device_DeviceStatus& DeviceStatus::getStatus() { + populate(); + return status; +} + +void DeviceStatus::populate() { + // Populate device info from wifi manager + status.has_info = true; + wifiManager.getState(status.info); + + // Set device ID to MAC address + String mac = WiFi.macAddress(); + strncpy(status.info.device_id, mac.c_str(), sizeof(status.info.device_id) - 1); + status.info.device_id[sizeof(status.info.device_id) - 1] = '\0'; + + // Set firmware version + strncpy(status.info.firmware_version, FIRMWARE_VERSION, sizeof(status.info.firmware_version) - 1); + status.info.firmware_version[sizeof(status.info.firmware_version) - 1] = '\0'; + + // Set hardware version + strncpy(status.info.hardware_version, HARDWARE_VERSION, sizeof(status.info.hardware_version) - 1); + status.info.hardware_version[sizeof(status.info.hardware_version) - 1] = '\0'; + + // Set uptime in seconds + status.info.uptime_seconds = millis() / 1000; + + // Set up the logs callback to use the logger's logs + status.logs.funcs.encode = [](pb_ostream_t *pb_stream, const pb_field_t *field, void * const *arg) -> bool { + int count; + const LogEntry* log_entries = logger.getLogs(count); + for (int i = 0; i < count; ++i) { + device_DeviceLogEntry log; + log.timestamp = (int64_t)log_entries[i].timestamp * 1000; // Convert to milliseconds + log.level = (device_DeviceLogLevel)log_entries[i].level; + strncpy(log.message, log_entries[i].message, sizeof(log.message) - 1); + log.message[sizeof(log.message) - 1] = '\0'; + + if (!pb_encode_tag_for_field(pb_stream, field) || + !pb_encode_submessage(pb_stream, device_DeviceLogEntry_fields, &log)) { + return false; + } + } + return true; + }; + status.logs.arg = nullptr; // Not used +} + +bool DeviceStatus::encode(Stream& stream) { + populate(); + + // Create pb_ostream_t from Stream + StreamState state = {&stream}; + pb_ostream_t pb_stream = {&writeStreamCallback, &state, SIZE_MAX, 0}; + + // Encode the message + return pb_encode(&pb_stream, device_DeviceStatus_fields, &status); +} \ No newline at end of file diff --git a/src/device/device_status.hpp b/src/device/device_status.hpp new file mode 100644 index 0000000..bc41bff --- /dev/null +++ b/src/device/device_status.hpp @@ -0,0 +1,31 @@ +#ifndef DEVICE_STATUS_HPP +#define DEVICE_STATUS_HPP + +#include +#include +#include +#include "wifi/wifi_manager.hpp" + +class DeviceStatus { +private: + device_DeviceStatus status; + + // Stream callback + struct StreamState { + Stream* stream; + }; + + static bool writeStreamCallback(pb_ostream_t *stream, const uint8_t *buf, size_t count); + +public: + DeviceStatus(); + ~DeviceStatus(); + + void setInfo(const device_DeviceInfo& info); + + void populate(); + const device_DeviceStatus& getStatus(); + bool encode(Stream& stream); +}; + +#endif // DEVICE_STATUS_HPP \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bbaaa10 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "webui/webui.hpp" +#include +#include +#include +#include "wifi/wifi_manager.hpp" +#include +#include +#include +#include "device/device_manager.hpp" +#include "hardware.pb.h" +#include +#include "ota_manager.hpp" + + +#define RXD2 16 // GPIO16 (RX2) +#define TXD2 17 // GPIO17 (TX2) + +WebUI webui; +WiFiManager wifiManager; +HardwareRTC rtc(Wire); +DeviceManager deviceManager; +OTAManager otaManager; +void setup() { + // **Hardware Initialization** + // Init I2C + Wire.begin(22, 21); // SDA=21, SCL=22 for ESP32 + Wire.setClock(100000); + Serial.begin(115200); + + // **System Initialization** + // Init logger + logger.setLogCallback([](const LogEntry& entry) { + Serial.print("["); + Serial.print(logger.levelToString(entry.level)); + Serial.print("] "); + Serial.println(entry.message); + }); + logger.begin(); + rtc.begin(); + otaManager.begin(); + logger.setLogLevel(LOG_DEBUG); + + tft.setup(); + delay(100); + rpc.setNewTimeCallback([](time_t timestamp) { + rtc.setTime(timestamp); + }); + + tft.println("Starting..."); + // Init filesystem + if (!LittleFS.begin(true)) { + LOG_ERROR("Failed to mount LittleFS"); + return; + } + rfidDB.begin(); + deviceManager.begin(); + + wifiManager.onStaConnected([](IPAddress ip) { + webui.begin(); + }); + wifiManager.onApStarted([](IPAddress ip) { + webui.begin(); + }); + wifiManager.onPreShutdown([]() { + webui.end(); + }); + + webui.setOTAManager(&otaManager); + + settings.setChangeCallback([](const settings_SettingsData& newData) { + wifiManager.begin(newData); + rpc.setApiKeyAndBaseUrl(newData.device_api_key, newData.sync_server_url); + // Set log level + logger.setLogLevel(static_cast(newData.log_level)); + // React to specific changes if needed + if (newData.auto_sync) { + rpc.startAutoSync(newData.sync_interval_seconds * 1000); + } else { + rpc.stopAutoSync(); + } + }); + + // Load settings initially + if (!settings.load()) { + LOG_ERROR("Failed to load settings"); + } else { + LOG_INFO("Settings loaded"); + } + + pserial.setCallback([](const IncomingMessage& msg) { + LOG_DEBUG("Received message via pserial"); + if (msg.which_payload == hardware_SensorToControlMessage_rfid_reading_tag) { + uint32_t cardId = msg.payload.rfid_reading.card_id; + deviceManager.processRfid(cardId); + rpc.addAccessLog(cardId); + } else if (msg.which_payload == hardware_SensorToControlMessage_ota_response_tag) { + if (msg.payload.ota_response.success) { + LOG_INFO("Sensor OTA update enabled successfully"); + LOG_INFO("Sensor IP: %s", msg.payload.ota_response.ip_address); + } else { + LOG_WARN("Sensor OTA update enable failed"); + } + } else { + LOG_WARN("Received unknown message type via serial"); + } + }); + + hardwareConfigRepo.setChangeCallback([](const hardware_HardwareConfig& newData) { + // Apply hardware config changes if needed + if (newData.enable_serial_sensor) { + Serial2.begin(9600, SERIAL_8N1, newData.sensor_rx_pin, newData.sensor_tx_pin); + vTaskDelay(80 / portTICK_PERIOD_MS); // Give some time for Serial2 to initialize + pserial.begin(Serial2); + } else { + Serial2.end(); + } + + deviceManager.setRelayPin(newData.relay_pin); + deviceManager.setOpenDuration(newData.hold_duration_ms); + deviceManager.overrideOpen(newData.override); + }); + hardwareConfigRepo.load(); + + deviceManager.onLEDStateChange([](hardware_ControlToSensorMessage message) { + pserial.sendMessage(message); + }); + + webui.onSendToSensor([](hardware_ControlToSensorMessage message) { + pserial.sendMessage(message); + }); +} + +void loop() { + rtc.update(); + pserial.update(); + wifiManager.update(); + ArduinoOTA.handle(); +} + diff --git a/src/webui/auth.cpp b/src/webui/auth.cpp new file mode 100644 index 0000000..db9198f --- /dev/null +++ b/src/webui/auth.cpp @@ -0,0 +1,49 @@ +#include "auth.hpp" +#include + +Auth::Auth(Users& users, SessionManager& sessionManager) + : users_(users), sessionManager_(sessionManager) {} + +const char* Auth::login(const char* username, const char* password) { + if (users_.checkPassword(username, password)) { + return sessionManager_.createSession(username); + } + return nullptr; +} + +bool Auth::logout(const char* token) { + return sessionManager_.endSession(token); +} + +const char* Auth::checkAuth(const char* token) { + // check if users exist + if (!isAuthRequired()) { + return "no-auth"; + } + return sessionManager_.validateSession(token); +} + +ArMiddlewareCallback Auth::middleware() { + return [this](AsyncWebServerRequest* request, ArMiddlewareNext next) { + if (!isAuthRequired()) { + // No users exist, authentication not required + next(); + return; + } + // Check Authorization header for Bearer token + if (request->hasHeader("Authorization")) { + String authHeader = request->getHeader("Authorization")->value(); + if (authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); // Remove "Bearer " + const char* username = checkAuth(token.c_str()); + if (username) { + request->setAttribute("username", username); + next(); + return; + } + } + } + // If no valid auth, respond with 401 Unauthorized + request->send(401, CONTENT_TYPE_TEXT, CONTENT_TYPE_TEXT); + }; +} diff --git a/src/webui/auth.hpp b/src/webui/auth.hpp new file mode 100644 index 0000000..e5c1aed --- /dev/null +++ b/src/webui/auth.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include "webui_strings.hpp" + +class Auth { +public: + Auth(Users& users, SessionManager& sessionManager); + + // Login: returns token on success, nullptr on failure + const char* login(const char* username, const char* password); + + // Logout: ends session for token + bool logout(const char* token); + + // Check auth: returns username if token valid, nullptr otherwise + const char* checkAuth(const char* token); + + bool isAuthRequired() const { + return users_.getUserCount() > 0; + } + + // Middleware function + ArMiddlewareCallback middleware(); + +private: + Users& users_; + SessionManager& sessionManager_; +}; \ No newline at end of file diff --git a/src/webui/webui.cpp b/src/webui/webui.cpp new file mode 100644 index 0000000..545e23c --- /dev/null +++ b/src/webui/webui.cpp @@ -0,0 +1,183 @@ +#include "webui.hpp" +#include +#include +#include + +WebUI::WebUI(uint16_t port) : server(port), auth_(users, sessionManager) {} + +WebUI::~WebUI() { + end(); +} + +void WebUI::begin(bool restartIfRunning) { + + if (isRunning_ && !restartIfRunning) { + LOG_INFO(WEBUI_SERVER_ALREADY_RUNNING); + return; + } + if (!LittleFS.begin(true)) { + LOG_ERROR(LITTLEFS_MOUNT_FAILED); + return; + } + end(); + setupRoutes(); + server.begin(); + isRunning_ = true; + LOG_INFO(WEBUI_SERVER_STARTED); +} + +void WebUI::end() { + if (!isRunning_) return; + server.end(); + isRunning_ = false; + vTaskDelay(pdMS_TO_TICKS(50)); +} + +void WebUI::setupRoutes() { + // API endpoint for login + server.on("/api/login", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleLogin(request, data, len, index, total); + }); + + // API endpoint for logout + server.on("/api/logout", HTTP_POST, [this](AsyncWebServerRequest *request) { + handleLogout(request); + }); + + // API endpoint for auth check + server.on("/api/authchk", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleAuthChk(request); + }); + + // API endpoints for settings + server.on("/api/settings", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleGetSettings(request); + }).addMiddleware(auth_.middleware()); + + server.on("/api/settings", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleUpdateSettings(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + + // API endpoint for status + server.on("/api/status", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleGetStatus(request); + }).addMiddleware(auth_.middleware()); + + // API endpoints for hardware + server.on("/api/hardware", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleGetHardware(request); + }).addMiddleware(auth_.middleware()); + + server.on("/api/hardware", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleUpdateHardware(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + + // API endpoints for users + + server.on("/api/users/password", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleUpdateUserPassword(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + + server.on("/api/users", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleGetUsers(request); + }).addMiddleware(auth_.middleware()); + server.on("/api/users", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleAddUser(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + server.on("/api/users", HTTP_DELETE, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleDeleteUser(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + + // API endpoints for RFID database + server.on("/api/db", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleGetRfidDatabase(request); + }).addMiddleware(auth_.middleware()); + + server.on("/api/db", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleUpdateRfidDatabase(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + + // API endpoints for RPC sync + server.on("/api/sync/full", HTTP_POST, [this](AsyncWebServerRequest *request) { + handleFullSync(request); + }).addMiddleware(auth_.middleware()); + server.on("/api/sync", HTTP_POST, [this](AsyncWebServerRequest *request) { + handleSync(request); + }).addMiddleware(auth_.middleware()); + + // API endpoint for OTA enable + server.on("/api/ota", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + handleEnableOTA(request, data, len, index, total); + }).addMiddleware(auth_.middleware()); + + // API endpoint for sensor restart + server.on("/api/restart-sensor", HTTP_POST, [this](AsyncWebServerRequest *request) { + handleRestartSensor(request); + }).addMiddleware(auth_.middleware()); + + // API endpoints for ESP OTA + server.on("/api/esp-ota-enable", HTTP_POST, [this](AsyncWebServerRequest *request) { + handleEnableESPOTA(request); + }).addMiddleware(auth_.middleware()); + + server.on("/api/esp-ota-disable", HTTP_POST, [this](AsyncWebServerRequest *request) { + handleDisableESPOTA(request); + }).addMiddleware(auth_.middleware()); + + server.on("/api/esp-ota-status", HTTP_GET, [this](AsyncWebServerRequest *request) { + handleGetESPOTAStatus(request); + }).addMiddleware(auth_.middleware()); + + server.on("/api/esp-ota-upload", HTTP_POST, [](AsyncWebServerRequest *request) { + }, [this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + handleESPOTAUpload(request, filename, index, data, len, final); + }).addMiddleware(auth_.middleware()); + + server.onNotFound([this](AsyncWebServerRequest *request) { + handleRoot(request); + }); +} + +void WebUI::sendProtobufResponse(AsyncWebServerRequest *request, void *proto_msg, const pb_msgdesc_t *fields) { + size_t resp_size = 0; + if (!pb_get_encoded_size(&resp_size, fields, proto_msg)) { + LOG_ERROR("PROTO-API: failed to get encoded size"); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + uint8_t *resp_buffer = new uint8_t[resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, resp_size); + if (!pb_encode(&resp_stream, fields, proto_msg)) { + LOG_ERROR("PROTO-API: response encode failed"); + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + size_t actual_size = resp_stream.bytes_written; + LOG_DEBUG("PROTO-API: encoded size=%d", actual_size); + + auto send_lambda = [resp_buffer, actual_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t copy_len = std::min(maxLen, actual_size - index); + std::memcpy(buf, resp_buffer + index, copy_len); + if (index + copy_len >= actual_size) { + delete[] resp_buffer; + } + return copy_len; + }; + + AsyncWebServerResponse *response = request->beginResponse((__FlashStringHelper*)CONTENT_TYPE_OCTET, actual_size, send_lambda); + request->send(response); +} + +void WebUI::handleRoot(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse(LittleFS, "/web/index.html.gz", "text/html", false); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); +} diff --git a/src/webui/webui.hpp b/src/webui/webui.hpp new file mode 100644 index 0000000..6c63d62 --- /dev/null +++ b/src/webui/webui.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "webui_strings.hpp" +#include "auth.hpp" +#include "ota_manager.hpp" + +class WebUI { +public: + WebUI(uint16_t port = 80); + ~WebUI(); + + void begin(bool restartIfRunning=false); + void end(); + + void onSendToSensor(std::function callback); + void setOTAManager(OTAManager* otaManager); + +private: + AsyncWebServer server; + Auth auth_; + bool isRunning_ = false; + OTAManager* otaManager_ = nullptr; + void setupRoutes(); + void handleLogin(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleLogout(AsyncWebServerRequest *request); + void handleAuthChk(AsyncWebServerRequest *request); + void handleGetSettings(AsyncWebServerRequest *request); + void handleUpdateSettings(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleGetStatus(AsyncWebServerRequest *request); + void handleGetHardware(AsyncWebServerRequest *request); + void handleUpdateHardware(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleGetUsers(AsyncWebServerRequest *request); + void handleAddUser(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleDeleteUser(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleUpdateUserPassword(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleGetRfidDatabase(AsyncWebServerRequest *request); + void handleUpdateRfidDatabase(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleSync(AsyncWebServerRequest *request); + void handleFullSync(AsyncWebServerRequest *request); + void handleEnableOTA(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + void handleRestartSensor(AsyncWebServerRequest *request); + void handleEnableESPOTA(AsyncWebServerRequest *request); + void handleDisableESPOTA(AsyncWebServerRequest *request); + void handleGetESPOTAStatus(AsyncWebServerRequest *request); + void handleESPOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + void handleRoot(AsyncWebServerRequest *request); + // Protobuf callback functions + static bool decode_string(pb_istream_t *stream, const pb_field_t *field, void **arg); + static bool encode_string(pb_ostream_t *stream, const pb_field_t *field, void * const *arg); + static bool encode_string_array(pb_ostream_t *stream, const pb_field_t *field, void * const *arg); + void sendProtobufResponse(AsyncWebServerRequest *request, void *proto_msg, const pb_msgdesc_t *fields); + + // Callback for sending messages to sensor + std::function sendToSensorCallback = nullptr; +}; \ No newline at end of file diff --git a/src/webui/webui_auth.cpp b/src/webui/webui_auth.cpp new file mode 100644 index 0000000..45ab79c --- /dev/null +++ b/src/webui/webui_auth.cpp @@ -0,0 +1,127 @@ +#include "webui.hpp" +#include +#include +#include +#include // Für sichere Buffer-Handling (optional, fallback zu new) + + + +void WebUI::handleLogin(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static std::vector buffer; // Non-static, aber static für chunked callback (per-request, assume no overlap) + // Alternative ohne STL: static uint8_t* buffer = nullptr; static size_t buffer_size = 0; (wie original, aber mit Warnung) + + if (index == 0) { + if (strcmp_P(request->contentType().c_str(), CONTENT_TYPE_OCTET) != 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + if (total == 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer.resize(total); // Oder: buffer = new uint8_t[total]; buffer_size = total; + } + + std::memcpy(buffer.data() + index, data, len); // Oder buffer + index + + if (index + len >= total) { + pb_istream_t stream = pb_istream_from_buffer(buffer.data(), buffer.size()); + webui_WebUiLoginRequest login_req = webui_WebUiLoginRequest_init_zero; + bool decode_ok = pb_decode(&stream, webui_WebUiLoginRequest_fields, &login_req); + + buffer.clear(); // Oder delete[] buffer; buffer = nullptr; + + if (!decode_ok || stream.bytes_left != 0) { + LOG_ERROR("/api/login: protobuf decode failed"); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + LOG_DEBUG("/api/login: decoded username='%s' (password length=%d)", login_req.username, strlen(login_req.password)); // Password maskiert + + const char* token = auth_.login(login_req.username, login_req.password); + bool success = (token != nullptr); + LOG_DEBUG("/api/login: auth result success=%d", success); + + webui_WebUiLoginResponse login_resp = webui_WebUiLoginResponse_init_zero; + login_resp.success = success; + if (success) { + login_resp.has_token = true; + strcpy(login_resp.token, token); + } else { + char error_buf[30]; + strcpy_P(error_buf, INVALID_CREDENTIALS); + login_resp.has_error = true; + strcpy(login_resp.error, error_buf); + } + + sendProtobufResponse(request, &login_resp, webui_WebUiLoginResponse_fields); + } +} + +void WebUI::handleLogout(AsyncWebServerRequest *request) { + const char* auth_value = request->getHeader("Authorization")->value().c_str(); + if (!auth_value || strncmp(auth_value, "Bearer ", 7) != 0) { + request->send(401, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)UNAUTHORIZED); + return; + } + + const char* token = auth_value + 7; // Skip "Bearer " + bool success = auth_.logout(token); + + webui_WebUiLogoutResponse logout_resp = webui_WebUiLogoutResponse_init_zero; // Angenommen, proto definiert + logout_resp.success = success; + + LOG_DEBUG("/api/logout: success=%d", success); + sendProtobufResponse(request, &logout_resp, webui_WebUiLogoutResponse_fields); +} + +void WebUI::handleAuthChk(AsyncWebServerRequest *request) { + // Manuell formatierte Client-IP für Logging + char ip_buf[16]; + snprintf(ip_buf, sizeof(ip_buf), "%d.%d.%d.%d", + request->client()->remoteIP()[0], + request->client()->remoteIP()[1], + request->client()->remoteIP()[2], + request->client()->remoteIP()[3]); + + LOG_DEBUG("/api/authchk: processing request, client=%s", ip_buf); + + // Prepare response + webui_WebUiAuthCheckResponse auth_resp = webui_WebUiAuthCheckResponse_init_zero; + auth_resp.authenticated = !auth_.isAuthRequired(); + + if (auth_.isAuthRequired()) { + // Prüfe Authorization-Header + if (!request->hasHeader("Authorization")) { + LOG_DEBUG("/api/authchk: no Authorization header, client=%s", ip_buf); + } else { + const AsyncWebHeader *auth_header = request->getHeader("Authorization"); + String auth_value = auth_header->value(); + if (!auth_value.startsWith("Bearer ")) { + LOG_DEBUG("/api/authchk: invalid Authorization header, client=%s", ip_buf); + } else { + String token = auth_value.substring(7); // Entferne "Bearer " + LOG_DEBUG("/api/authchk: checking token (masked), client=%s", ip_buf); + const char* username = auth_.checkAuth(token.c_str()); + if (username) { + auth_resp.authenticated = true; + auth_resp.has_username = true; + // Verwende strncpy für Sicherheit (angenommen, username hat max_size in .proto) + strncpy(auth_resp.username, username, sizeof(auth_resp.username) - 1); + auth_resp.username[sizeof(auth_resp.username) - 1] = '\0'; // Null-Terminierung + LOG_DEBUG("/api/authchk: authenticated as '%s', client=%s", auth_resp.username, ip_buf); + } else { + LOG_DEBUG("/api/authchk: invalid token, client=%s", ip_buf); + } + } + } + } else { + LOG_DEBUG("/api/authchk: no auth required, client=%s", ip_buf); + } + + // Entferne & vor _fields (Makro ist bereits Pointer) + sendProtobufResponse(request, &auth_resp, webui_WebUiAuthCheckResponse_fields); +} \ No newline at end of file diff --git a/src/webui/webui_hardware.cpp b/src/webui/webui_hardware.cpp new file mode 100644 index 0000000..31feee9 --- /dev/null +++ b/src/webui/webui_hardware.cpp @@ -0,0 +1,98 @@ +#include "webui.hpp" +#include +#include +#include +#include + +void WebUI::handleGetHardware(AsyncWebServerRequest *request) { + // Get hardware config + hardware_HardwareConfig& config = hardwareConfigRepo.getData(); + + // Encode response + size_t resp_size = 0; + pb_get_encoded_size(&resp_size, hardware_HardwareConfig_fields, &config); + + if (resp_size == 0) { + LOG_ERROR("WebUI: /api/hardware GET: Response size is 0 - protobuf encoding failed!"); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"Response size is 0"); + return; + } + + uint8_t *resp_buffer = new uint8_t[resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, resp_size); + bool encode_ok = pb_encode(&resp_stream, hardware_HardwareConfig_fields, &config); + + if (!encode_ok) { + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + // Send response + AsyncWebServerResponse *response = request->beginResponse(CONTENT_TYPE_OCTET, resp_size, [resp_buffer, resp_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t len = min(maxLen, resp_size - index); + memcpy(buf, resp_buffer + index, len); + if (index + len >= resp_size) { + delete[] resp_buffer; + } + return len; + }); + request->send(response); +} + +void WebUI::handleUpdateHardware(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static uint8_t *buffer = nullptr; + static size_t buffer_size = 0; + + if (index == 0) { + // First chunk + if (strcmp_P(request->contentType().c_str(), CONTENT_TYPE_OCTET) != 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + buffer_size = total; + if (buffer_size == 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer = new uint8_t[buffer_size]; + if (!buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + LOG_INFO("/api/hardware POST: receiving payload total=%d", buffer_size); + } + + memcpy(buffer + index, data, len); + + if (index + len >= total) { + // Last chunk, process + hardware_HardwareConfig update_config = hardware_HardwareConfig_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer, buffer_size); + + bool decode_ok = pb_decode(&stream, hardware_HardwareConfig_fields, &update_config); + + delete[] buffer; + buffer = nullptr; + + if (!decode_ok) { + LOG_ERROR("/api/hardware POST: protobuf decode failed"); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + // Update hardware config + hardwareConfigRepo.setData(update_config); + + // Prepare response (simple success) + AsyncWebServerResponse *response = request->beginResponse(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OK"); + request->send(response); + } +} \ No newline at end of file diff --git a/src/webui/webui_ota.cpp b/src/webui/webui_ota.cpp new file mode 100644 index 0000000..8161603 --- /dev/null +++ b/src/webui/webui_ota.cpp @@ -0,0 +1,169 @@ +#include "webui.hpp" +#include +#include +#include +#include + +void WebUI::handleEnableOTA(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static uint8_t *buffer = nullptr; + static size_t buffer_size = 0; + + if (index == 0) { + // First chunk + if (strcmp_P(request->contentType().c_str(), CONTENT_TYPE_OCTET) != 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + buffer_size = total; + if (buffer_size == 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer = new uint8_t[buffer_size]; + if (!buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + LOG_INFO("/api/ota POST: receiving payload total=%d", buffer_size); + } + + memcpy(buffer + index, data, len); + + if (index + len >= total) { + // Last chunk, process + hardware_SensorOTAEnable ota_enable = hardware_SensorOTAEnable_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer, buffer_size); + + bool decode_ok = pb_decode(&stream, hardware_SensorOTAEnable_fields, &ota_enable); + + delete[] buffer; + buffer = nullptr; + + if (!decode_ok) { + LOG_ERROR("/api/ota POST: protobuf decode failed"); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + // Create ControlToSensorMessage with OTA enable + hardware_ControlToSensorMessage msg = hardware_ControlToSensorMessage_init_zero; + msg.control_id = 0; // Assuming control_id is 0 for now + msg.which_payload = hardware_ControlToSensorMessage_ota_enable_tag; + msg.payload.ota_enable = ota_enable; + + // Send via callback + if (sendToSensorCallback) { + sendToSensorCallback(msg); + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA enable sent"); + } else { + LOG_ERROR("No sendToSensorCallback set"); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"Internal error"); + } + } +} + +void WebUI::handleRestartSensor(AsyncWebServerRequest *request) { + // Create ControlToSensorMessage with restart payload + hardware_ControlToSensorMessage msg = hardware_ControlToSensorMessage_init_zero; + msg.control_id = 0; // Assuming control_id is 0 for now + msg.which_payload = hardware_ControlToSensorMessage_restart_tag; + msg.payload.restart = hardware_SensorRestart_init_zero; + + // Send via callback + if (sendToSensorCallback) { + sendToSensorCallback(msg); + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"Restart command sent"); + } else { + LOG_ERROR("No sendToSensorCallback set"); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"Internal error"); + } +} + +void WebUI::onSendToSensor(std::function callback) { + sendToSensorCallback = callback; +} + +void WebUI::setOTAManager(OTAManager* otaManager) { + otaManager_ = otaManager; +} + +void WebUI::handleEnableESPOTA(AsyncWebServerRequest *request) { + if (!otaManager_) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA manager not initialized"); + return; + } + otaManager_->enable(); + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"ESP OTA enabled"); +} + +void WebUI::handleDisableESPOTA(AsyncWebServerRequest *request) { + if (!otaManager_) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA manager not initialized"); + return; + } + otaManager_->disable(); + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"ESP OTA disabled"); +} + +void WebUI::handleGetESPOTAStatus(AsyncWebServerRequest *request) { + if (!otaManager_) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA manager not initialized"); + return; + } + String status = "{ \"enabled\": " + String(otaManager_->isEnabled() ? "true" : "false") + ", \"status\": \"" + otaManager_->getStatus() + "\" }"; + request->send(200, "application/json", status); +} + +void WebUI::handleESPOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + if (!index) { + LOG_INFO("OTA Update Start: %s (total expected: %u bytes)", filename.c_str(), request->contentLength()); + + // Prüfe freien Speicher (Debug-Info) + Serial.printf("Free sketch space: %u bytes\n", ESP.getFreeSketchSpace()); + + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { // Explizit U_FLASH für Klarheit + Update.printError(Serial); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA update failed to begin"); + return; + } + } + + // Debug: Fortschritt loggen (alle 100 KB oder so, um nicht zu spammen) + static size_t lastLogged = 0; + size_t currentPos = index + len; + if (currentPos - lastLogged > 100000) { // Alle 100 KB + LOG_INFO("Free Heap: %u bytes", ESP.getFreeHeap()); + LOG_INFO("OTA Progress: %u / %u bytes (%d%%)", currentPos, request->contentLength(), + (request->contentLength() > 0 ? (currentPos * 100 / request->contentLength()) : 0)); + lastLogged = currentPos; + } + + // Write firmware + size_t written = Update.write(data, len); + if (written != len) { + Update.printError(Serial); + LOG_ERROR("Write failed: wrote %u of %u bytes", written, len); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA update failed during write"); + return; + } + + if (final) { + if (Update.end(true)) { + LOG_INFO("OTA Update Success: %u bytes total", index + len); + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA update successful. Rebooting..."); + + // Start restart task with delay + xTaskCreate([](void* pvParameters) { + vTaskDelay(2000 / portTICK_PERIOD_MS); // 2 second delay + ESP.restart(); + vTaskDelete(NULL); + }, "RestartTask", 2048, NULL, 1, NULL); + } else { + Update.printError(Serial); + LOG_ERROR("End failed: error %u", Update.getError()); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"OTA update failed to end"); + } + } +} \ No newline at end of file diff --git a/src/webui/webui_protobuf.cpp b/src/webui/webui_protobuf.cpp new file mode 100644 index 0000000..fd97346 --- /dev/null +++ b/src/webui/webui_protobuf.cpp @@ -0,0 +1,34 @@ +#include "webui.hpp" +#include +#include +#include + +// Protobuf callback functions +bool WebUI::decode_string(pb_istream_t *stream, const pb_field_t *field, void **arg) { + size_t len = stream->bytes_left; + char *str = (char*)malloc(len + 1); + if (!str) return false; + if (!pb_read(stream, (uint8_t*)str, len)) { + free(str); + return false; + } + str[len] = '\0'; + *arg = str; + return true; +} + +bool WebUI::encode_string(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) { + char *str = (char*)*arg; + return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, (uint8_t*)str, strlen(str)); +} + +bool WebUI::encode_string_array(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) { + std::vector *usernames = (std::vector*)*arg; + for (const auto& username : *usernames) { + if (!pb_encode_tag_for_field(stream, field) || + !pb_encode_string(stream, (uint8_t*)username.c_str(), username.length())) { + return false; + } + } + return true; +} \ No newline at end of file diff --git a/src/webui/webui_rfid.cpp b/src/webui/webui_rfid.cpp new file mode 100644 index 0000000..946d7f2 --- /dev/null +++ b/src/webui/webui_rfid.cpp @@ -0,0 +1,81 @@ +#include "webui.hpp" +#include +#include + +void WebUI::handleGetRfidDatabase(AsyncWebServerRequest *request) { + // Open the RFID database file directly from LittleFS + File file = LittleFS.open("/rfid.db", "r"); + + if (!file) { + // File doesn't exist or can't be opened + request->send(404, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"RFID database file not found"); + return; + } + + size_t fileSize = file.size(); + + if (fileSize == 0) { + // Empty file + file.close(); + AsyncWebServerResponse *response = request->beginResponse(CONTENT_TYPE_OCTET, 0, [](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + return 0; + }); + request->send(response); + return; + } + + // Send file content directly without modification + AsyncWebServerResponse *response = request->beginResponse(CONTENT_TYPE_OCTET, fileSize, [file](uint8_t *buf, size_t maxLen, size_t reqIndex) mutable -> size_t { + if (reqIndex >= file.size()) { + file.close(); + return 0; + } + + size_t bytesToRead = min(maxLen, (size_t)(file.size() - reqIndex)); + size_t bytesRead = file.read(buf, bytesToRead); + + // Close file when done + if (reqIndex + bytesRead >= file.size()) { + file.close(); + } + + return bytesRead; + }); + + request->send(response); +} + +void WebUI::handleUpdateRfidDatabase(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static File uploadFile; + + if (index == 0) { + // First chunk - open file for writing + uploadFile = LittleFS.open("/rfid.db", "w"); + if (!uploadFile) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_FAILED); + return; + } + LOG_INFO("WebUI: Starting RFID database upload, total size: %d bytes\n", total); + } + + // Write data chunk to file + if (uploadFile) { + size_t bytesWritten = uploadFile.write(data, len); + if (bytesWritten != len) { + uploadFile.close(); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_FAILED); + return; + } + } + + if (index + len >= total) { + // Last chunk - close file and send response + if (uploadFile) { + uploadFile.close(); + LOG_INFO("WebUI: RFID database upload completed, %d bytes written\n", total); + } + + // Send success response + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_SUCCESS); + } +} \ No newline at end of file diff --git a/src/webui/webui_rpc.cpp b/src/webui/webui_rpc.cpp new file mode 100644 index 0000000..35c2951 --- /dev/null +++ b/src/webui/webui_rpc.cpp @@ -0,0 +1,22 @@ +#include "webui.hpp" +#include + +void WebUI::handleSync(AsyncWebServerRequest *request) { + if (rpc.sync(false)) { + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_SUCCESS); + LOG_INFO("WebUI: Normal sync completed successfully"); + } else { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_FAILED); + LOG_ERROR("WebUI: Normal sync failed"); + } +} + +void WebUI::handleFullSync(AsyncWebServerRequest *request) { + if (rpc.sync(true)) { + request->send(200, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_SUCCESS); + LOG_INFO("WebUI: Full sync completed successfully"); + } else { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ACTION_FAILED); + LOG_ERROR("WebUI: Full sync failed"); + } +} \ No newline at end of file diff --git a/src/webui/webui_settings.cpp b/src/webui/webui_settings.cpp new file mode 100644 index 0000000..635efd0 --- /dev/null +++ b/src/webui/webui_settings.cpp @@ -0,0 +1,77 @@ +#include "webui.hpp" +#include +#include +#include +#include +#include +#include // Für std::min + +void WebUI::handleGetSettings(AsyncWebServerRequest *request) { + // Prepare response + settings_GetSettingsResponse resp = settings_GetSettingsResponse_init_zero; + resp.settings = settings.getData(); + resp.has_settings = true; + + String client_ip = request->client()->remoteIP().toString(); + LOG_DEBUG("/api/settings GET: sending settings, client=%s", client_ip.c_str()); + + sendProtobufResponse(request, &resp, settings_GetSettingsResponse_fields); +} + +void WebUI::handleUpdateSettings(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static std::vector buffer; // Sicherer Buffer pro Request + + String client_ip = request->client()->remoteIP().toString(); + + if (index == 0) { + // First chunk + if (strcmp_P(request->contentType().c_str(), CONTENT_TYPE_OCTET) != 0) { + LOG_ERROR("/api/settings POST: invalid content type, client=%s", client_ip.c_str()); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + if (total == 0) { + LOG_ERROR("/api/settings POST: empty request, client=%s", client_ip.c_str()); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer.resize(total); + LOG_INFO("/api/settings POST: receiving payload total=%d, client=%s", total, client_ip.c_str()); + } + + std::memcpy(buffer.data() + index, data, len); + + if (index + len >= total) { + // Last chunk, process + settings_UpdateSettingsRequest update_req = settings_UpdateSettingsRequest_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer.data(), buffer.size()); + + bool decode_ok = pb_decode(&stream, settings_UpdateSettingsRequest_fields, &update_req); + buffer.clear(); + + if (!decode_ok || stream.bytes_left != 0) { + LOG_ERROR("/api/settings POST: protobuf decode failed, client=%s", client_ip.c_str()); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + // Update settings + settings.setData(update_req.settings); + + // Save settings + if (!settings.save()) { + LOG_ERROR("/api/settings POST: failed to save settings, client=%s", client_ip.c_str()); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)FAILED_TO_SAVE_SETTINGS); + return; + } + + // Prepare response + settings_UpdateSettingsResponse update_resp = settings_UpdateSettingsResponse_init_zero; + update_resp.success = true; + + LOG_DEBUG("/api/settings POST: settings updated successfully, client=%s", client_ip.c_str()); + sendProtobufResponse(request, &update_resp, settings_UpdateSettingsResponse_fields); + } +} \ No newline at end of file diff --git a/src/webui/webui_status.cpp b/src/webui/webui_status.cpp new file mode 100644 index 0000000..86162e8 --- /dev/null +++ b/src/webui/webui_status.cpp @@ -0,0 +1,49 @@ +#include "webui.hpp" +#include +#include +#include +#include "device/device_status.hpp" + +void WebUI::handleGetStatus(AsyncWebServerRequest *request) { + // Create DeviceStatus instance + DeviceStatus deviceStatus; + + // Get the populated status + const device_DeviceStatus& status = deviceStatus.getStatus(); + + // Get encoded size + size_t resp_size = 0; + pb_get_encoded_size(&resp_size, device_DeviceStatus_fields, &status); + + if (resp_size == 0) { + LOG_ERROR("WebUI: /api/status GET: Response size is 0 - protobuf encoding failed!"); + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)"Response size is 0"); + return; + } + + uint8_t *resp_buffer = new uint8_t[resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, resp_size); + bool encode_ok = pb_encode(&resp_stream, device_DeviceStatus_fields, &status); + + if (!encode_ok) { + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + // Send response + AsyncWebServerResponse *response = request->beginResponse(CONTENT_TYPE_OCTET, resp_size, [resp_buffer, resp_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t len = min(maxLen, resp_size - index); + memcpy(buf, resp_buffer + index, len); + if (index + len >= resp_size) { + delete[] resp_buffer; + } + return len; + }); + request->send(response); +} \ No newline at end of file diff --git a/src/webui/webui_strings.hpp b/src/webui/webui_strings.hpp new file mode 100644 index 0000000..c0a939f --- /dev/null +++ b/src/webui/webui_strings.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +// Frequently used strings stored in PROGMEM +const char CONTENT_TYPE_OCTET[] PROGMEM = "application/octet-stream"; +const char CONTENT_TYPE_TEXT[] PROGMEM = "text/plain"; +const char INVALID_CONTENT_TYPE[] PROGMEM = "Invalid content type"; +const char EMPTY_REQUEST[] PROGMEM = "Empty request"; +const char MEMORY_ALLOCATION_FAILED[] PROGMEM = "Memory allocation failed"; +const char INVALID_PROTOBUF[] PROGMEM = "Invalid protobuf"; +const char ENCODING_FAILED[] PROGMEM = "Encoding failed"; +const char FAILED_TO_LOAD_SETTINGS[] PROGMEM = "Failed to load settings"; +const char FAILED_TO_SAVE_SETTINGS[] PROGMEM = "Failed to save settings"; +const char LITTLEFS_MOUNT_FAILED[] PROGMEM = "LittleFS Mount Failed"; +const char WEBUI_SERVER_STARTED[] PROGMEM = "WebUI server started"; +const char WEBUI_SERVER_ALREADY_RUNNING[] PROGMEM = "WebUI server already running"; +const char DUMMY_TOKEN[] PROGMEM = "dummy_token"; +const char INVALID_CREDENTIALS[] PROGMEM = "Invalid credentials"; +const char UNAUTHORIZED[] PROGMEM = "Unauthorized"; + +// Generic action response messages +const char ACTION_SUCCESS[] PROGMEM = "Action completed successfully"; +const char ACTION_FAILED[] PROGMEM = "Action failed"; \ No newline at end of file diff --git a/src/webui/webui_users.cpp b/src/webui/webui_users.cpp new file mode 100644 index 0000000..78e706a --- /dev/null +++ b/src/webui/webui_users.cpp @@ -0,0 +1,329 @@ +#include "webui.hpp" +#include +#include +#include +#include + +void WebUI::handleGetUsers(AsyncWebServerRequest *request) { + // CHANGE: Use a temporary in-memory list to collect usernames. + // We use std::vector because it manages memory for us + // and ensures the strings are valid throughout the function's scope. + std::vector usernames; + + // Populate the list using the new callback method from the Users class. + // The lambda function captures our 'usernames' vector by reference ([&]) + // and adds each username it receives to the list. + users.loadUsernamesCallback([&](const char* username) { + usernames.push_back(username); + return true; // Return true to continue iterating. + }); + + + // Prepare the Protobuf response structure. + webui_GetUsersResponse resp = webui_GetUsersResponse_init_zero; + + // Wire up our new encoder callback and pass it a pointer to our temporary list. + resp.usernames.funcs.encode = &WebUI::encode_string_array; + resp.usernames.arg = &usernames; // Pass the address of our vector. + + // --- The rest of the logic remains mostly the same --- + + // Encode response to a buffer. + size_t resp_size = 0; + // Note: pb_get_encoded_size will fail here because the callback needs to run. + // We must encode directly and handle a potentially oversized buffer. + // A safe upper limit: (32 chars username + 2 bytes overhead) * 5 users + overhead + const size_t max_resp_size = (32 + 2) * 5 + 4; + uint8_t *resp_buffer = new uint8_t[max_resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, max_resp_size); + bool encode_ok = pb_encode(&resp_stream, webui_GetUsersResponse_fields, &resp); + + if (!encode_ok) { + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + // Use the actual encoded size from the stream. + size_t final_resp_size = resp_stream.bytes_written; + + // Send response. + AsyncWebServerResponse *response = request->beginResponse(CONTENT_TYPE_OCTET, final_resp_size, [resp_buffer, final_resp_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t len = std::min(maxLen, final_resp_size - index); + if (len > 0) { + memcpy(buf, resp_buffer + index, len); + } + // Delete the buffer only when the transfer is complete. + if (index + len >= final_resp_size) { + delete[] resp_buffer; + } + return len; + }); + request->send(response); +} + +void WebUI::handleAddUser(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static uint8_t *buffer = nullptr; + static size_t buffer_size = 0; + + if (index == 0) { + // First chunk + if (strcmp_P(request->contentType().c_str(), CONTENT_TYPE_OCTET) != 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + buffer_size = total; + if (buffer_size == 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer = new uint8_t[buffer_size]; + if (!buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + } + + memcpy(buffer + index, data, len); + + if (index + len >= total) { + // Last chunk, process + webui_AddUserRequest add_req = webui_AddUserRequest_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer, buffer_size); + + bool decode_ok = pb_decode(&stream, webui_AddUserRequest_fields, &add_req); + + delete[] buffer; + buffer = nullptr; + + if (!decode_ok) { + LOG_ERROR("/api/users POST: protobuf decode failed"); + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + bool success = users.addUser(add_req.username, add_req.password); + + // Prepare response + webui_AddUserResponse add_resp = webui_AddUserResponse_init_zero; + add_resp.success = success; + + if (!success) { + add_resp.has_error = true; + strcpy(add_resp.error, "table full or user exists"); + }else{ + add_resp.has_error = false; + } + + // Encode response + size_t resp_size = 0; + pb_get_encoded_size(&resp_size, webui_AddUserResponse_fields, &add_resp); + + uint8_t *resp_buffer = new uint8_t[resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, resp_size); + bool encode_ok = pb_encode(&resp_stream, webui_AddUserResponse_fields, &add_resp); + + + if (!encode_ok) { + LOG_ERROR("/api/users POST: protobuf encode failed"); + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + // Send response + AsyncWebServerResponse *response = request->beginResponse(CONTENT_TYPE_OCTET, resp_size, [resp_buffer, resp_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t len = min(maxLen, resp_size - index); + memcpy(buf, resp_buffer + index, len); + if (index + len >= resp_size) { + delete[] resp_buffer; + } + return len; + }); + request->send(response); + } +} + +void WebUI::handleDeleteUser(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static uint8_t *buffer = nullptr; + static size_t buffer_size = 0; + + if (index == 0) { + // First chunk + if (request->contentType() != "application/octet-stream") { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + buffer_size = total; + if (buffer_size == 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer = new uint8_t[buffer_size]; + if (!buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + } + + memcpy(buffer + index, data, len); + + if (index + len >= total) { + // Last chunk, process + webui_DeleteUserRequest delete_req = webui_DeleteUserRequest_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer, buffer_size); + + bool decode_ok = pb_decode(&stream, webui_DeleteUserRequest_fields, &delete_req); + + delete[] buffer; + buffer = nullptr; + + if (!decode_ok) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + // Delete user + bool success = users.deleteUser(delete_req.username); + + // Prepare response + webui_DeleteUserResponse delete_resp = webui_DeleteUserResponse_init_zero; + delete_resp.success = success; + + if (!success) { + delete_resp.has_error = true; + strcpy(delete_resp.error, "User not found"); + } + + // Encode response + size_t resp_size = 0; + pb_get_encoded_size(&resp_size, webui_DeleteUserResponse_fields, &delete_resp); + + uint8_t *resp_buffer = new uint8_t[resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, resp_size); + bool encode_ok = pb_encode(&resp_stream, webui_DeleteUserResponse_fields, &delete_resp); + + + if (!encode_ok) { + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + // Send response + AsyncWebServerResponse *response = request->beginResponse("application/octet-stream", resp_size, [resp_buffer, resp_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t len = min(maxLen, resp_size - index); + memcpy(buf, resp_buffer + index, len); + if (index + len >= resp_size) { + delete[] resp_buffer; + } + return len; + }); + request->send(response); + } +} + +void WebUI::handleUpdateUserPassword(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + static uint8_t *buffer = nullptr; + static size_t buffer_size = 0; + + if (index == 0) { + // First chunk + if (request->contentType() != "application/octet-stream") { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_CONTENT_TYPE); + return; + } + + buffer_size = total; + if (buffer_size == 0) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)EMPTY_REQUEST); + return; + } + + buffer = new uint8_t[buffer_size]; + if (!buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + } + + memcpy(buffer + index, data, len); + + if (index + len >= total) { + // Last chunk, process + webui_UpdateUserPasswordRequest update_req = webui_UpdateUserPasswordRequest_init_zero; + pb_istream_t stream = pb_istream_from_buffer(buffer, buffer_size); + + bool decode_ok = pb_decode(&stream, webui_UpdateUserPasswordRequest_fields, &update_req); + + delete[] buffer; + buffer = nullptr; + + if (!decode_ok) { + request->send(400, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)INVALID_PROTOBUF); + return; + } + + // Update password + bool success = users.updatePassword(update_req.username, update_req.new_password); + + // Prepare response + webui_UpdateUserPasswordResponse update_resp = webui_UpdateUserPasswordResponse_init_zero; + update_resp.success = success; + + if (!success) { + update_resp.has_error = true; + strcpy(update_resp.error, "User not found"); + } + + // Encode response + size_t resp_size = 0; + pb_get_encoded_size(&resp_size, webui_UpdateUserPasswordResponse_fields, &update_resp); + + uint8_t *resp_buffer = new uint8_t[resp_size]; + if (!resp_buffer) { + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)MEMORY_ALLOCATION_FAILED); + return; + } + + pb_ostream_t resp_stream = pb_ostream_from_buffer(resp_buffer, resp_size); + bool encode_ok = pb_encode(&resp_stream, webui_UpdateUserPasswordResponse_fields, &update_resp); + + + if (!encode_ok) { + delete[] resp_buffer; + request->send(500, (__FlashStringHelper*)CONTENT_TYPE_TEXT, (__FlashStringHelper*)ENCODING_FAILED); + return; + } + + // Send response + AsyncWebServerResponse *response = request->beginResponse("application/octet-stream", resp_size, [resp_buffer, resp_size](uint8_t *buf, size_t maxLen, size_t index) -> size_t { + size_t len = min(maxLen, resp_size - index); + memcpy(buf, resp_buffer + index, len); + if (index + len >= resp_size) { + delete[] resp_buffer; + } + return len; + }); + request->send(response); + } +} \ No newline at end of file diff --git a/src/wifi/wifi_manager.cpp b/src/wifi/wifi_manager.cpp new file mode 100644 index 0000000..b757277 --- /dev/null +++ b/src/wifi/wifi_manager.cpp @@ -0,0 +1,277 @@ +#include "wifi_manager.hpp" +#include +#include + +WiFiManager::WiFiManager() { + registerEvents(); +} + +WiFiManager::~WiFiManager() { + stop(); + unregisterEvents(); +} + +void WiFiManager::registerEvents() { + WiFi.onEvent(std::bind(&WiFiManager::onWiFiStaGotIP, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP); + WiFi.onEvent(std::bind(&WiFiManager::onWiFiStaDisconnected, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); +} + +void WiFiManager::unregisterEvents() { + WiFi.removeEvent(ARDUINO_EVENT_WIFI_STA_GOT_IP); + WiFi.removeEvent(ARDUINO_EVENT_WIFI_STA_DISCONNECTED); +} + +void WiFiManager::begin(const settings_SettingsData& settings) { + // Safely copy the new configuration + wifi_mode_ = settings.wifi_mode; + strncpy(station_ssid_, settings.station_ssid, sizeof(station_ssid_) - 1); + station_ssid_[sizeof(station_ssid_) - 1] = '\0'; + strncpy(station_password_, settings.station_password, sizeof(station_password_) - 1); + station_password_[sizeof(station_password_) - 1] = '\0'; + strncpy(ap_ssid_, settings.ap_ssid, sizeof(ap_ssid_) - 1); + ap_ssid_[sizeof(ap_ssid_) - 1] = '\0'; + strncpy(ap_password_, settings.ap_password, sizeof(ap_password_) - 1); + ap_password_[sizeof(ap_password_) - 1] = '\0'; + ap_channel_ = settings.ap_channel; + + stop(); // Start with a clean slate + + bool success = false; + switch (wifi_mode_) { + case settings_WifiMode_WIFI_MODE_STATION: + success = applyStationMode(); + break; + case settings_WifiMode_WIFI_MODE_AP: + success = applyAccessPointMode(); + break; + case settings_WifiMode_WIFI_MODE_AP_STATION: + success = applyStationAndAPMode(); + break; + default: + LOG_ERROR("WiFi: Unknown or unspecified WiFi mode. Stopping WiFi."); + state_ = WiFiState::STOPPED; + break; + } + + if (!success) { + state_ = WiFiState::CONNECTION_FAILED; + } +} + + +void WiFiManager::update() { + static uint32_t last_check_time = 0; + uint32_t now = millis(); + if (now - last_check_time < 1000) { + return; // Check every second + }else last_check_time = now; + switch (state_) { + case WiFiState::CONNECTING_STATION: + if (millis() - connection_start_time_ > CONNECTION_TIMEOUT_MS) { + LOG_WARN("WiFi: Station connection timed out."); + WiFi.disconnect(); + state_ = WiFiState::CONNECTION_FAILED; + last_reconnect_attempt_ = millis(); + } + break; + + case WiFiState::CONNECTION_FAILED: + if (millis() - last_reconnect_attempt_ > reconnect_delay_ms_) { + LOG_INFO("WiFi: Attempting to reconnect..."); + applyStationMode(); + } + break; + + default: + break; + } +} + +void WiFiManager::stop() { + if (state_ == WiFiState::STOPPED) { + return; // Already stopped + } + LOG_INFO("WiFi: Stopping all WiFi activity..."); + if (onPreShutdownCallback_) { + onPreShutdownCallback_(); + } + // Give the network stack a moment to release the web server's port. + // 100ms is usually more than enough. + vTaskDelay(pdMS_TO_TICKS(100)); + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + state_ = WiFiState::STOPPED; +} + +bool WiFiManager::isConnected() const { + return (state_ == WiFiState::CONNECTED_STATION || state_ == WiFiState::RUNNING_AP_STA) && (WiFi.status() == WL_CONNECTED); +} + +void WiFiManager::getState(device_DeviceInfo& deviceInfo) const { + // Set STA connection state + switch (state_) { + case WiFiState::CONNECTED_STATION: + case WiFiState::RUNNING_AP_STA: + deviceInfo.sta_connection_state = device_WifiConnectionState_WIFI_CONNECTED; + break; + case WiFiState::CONNECTING_STATION: + deviceInfo.sta_connection_state = device_WifiConnectionState_WIFI_CONNECTING; + break; + case WiFiState::CONNECTION_FAILED: + deviceInfo.sta_connection_state = device_WifiConnectionState_WIFI_FAILED; + break; + default: + deviceInfo.sta_connection_state = device_WifiConnectionState_WIFI_DISCONNECTED; + break; + } + + // Set AP connection state + if (state_ == WiFiState::RUNNING_AP || state_ == WiFiState::RUNNING_AP_STA) { + deviceInfo.ap_connection_state = device_WifiConnectionState_WIFI_CONNECTED; + } else { + deviceInfo.ap_connection_state = device_WifiConnectionState_WIFI_DISCONNECTED; + } + + // Populate STA details if connected + if (deviceInfo.sta_connection_state == device_WifiConnectionState_WIFI_CONNECTED) { + strncpy(deviceInfo.sta_ip, WiFi.localIP().toString().c_str(), sizeof(deviceInfo.sta_ip) - 1); + deviceInfo.sta_ip[sizeof(deviceInfo.sta_ip) - 1] = '\0'; + strncpy(deviceInfo.sta_gateway, WiFi.gatewayIP().toString().c_str(), sizeof(deviceInfo.sta_gateway) - 1); + deviceInfo.sta_gateway[sizeof(deviceInfo.sta_gateway) - 1] = '\0'; + deviceInfo.sta_signal_strength = WiFi.RSSI(); + } else { + memset(deviceInfo.sta_ip, 0, sizeof(deviceInfo.sta_ip)); + memset(deviceInfo.sta_gateway, 0, sizeof(deviceInfo.sta_gateway)); + deviceInfo.sta_signal_strength = 0; + } + + // Populate AP details if running + if (deviceInfo.ap_connection_state == device_WifiConnectionState_WIFI_CONNECTED) { + strncpy(deviceInfo.ap_ip, WiFi.softAPIP().toString().c_str(), sizeof(deviceInfo.ap_ip) - 1); + deviceInfo.ap_ip[sizeof(deviceInfo.ap_ip) - 1] = '\0'; + deviceInfo.ap_client_count = WiFi.softAPgetStationNum(); + } else { + memset(deviceInfo.ap_ip, 0, sizeof(deviceInfo.ap_ip)); + deviceInfo.ap_client_count = 0; + } +} + +// --- Event Handlers --- + +void WiFiManager::onWiFiStaGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { + IPAddress ip = WiFi.localIP(); + LOG_INFO("WiFi: Station connected! IP: %s", ip.toString().c_str()); + + if (wifi_mode_ == settings_WifiMode_WIFI_MODE_AP_STATION) { + state_ = WiFiState::RUNNING_AP_STA; + } else { + state_ = WiFiState::CONNECTED_STATION; + } + + if (onStaConnectedCallback_) { + onStaConnectedCallback_(ip); + } +} + +void WiFiManager::onWiFiStaDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { + LOG_WARN("WiFi: Station disconnected. Reason: %d", info.wifi_sta_disconnected.reason); + state_ = WiFiState::CONNECTION_FAILED; + last_reconnect_attempt_ = millis(); // Set timestamp to start reconnect delay + + if (onDisconnectedCallback_) { + onDisconnectedCallback_(); + } +} + + + +// --- Private Helper Methods --- + +bool WiFiManager::applyStationMode() { + if (!validateStationConfig()) { + LOG_ERROR("WiFi: Invalid station configuration."); + return false; + } + LOG_INFO("WiFi: Connecting to station '%s'...", station_ssid_); + WiFi.mode(WIFI_STA); + WiFi.begin(station_ssid_, station_password_); + state_ = WiFiState::CONNECTING_STATION; + connection_start_time_ = millis(); + return true; +} + +bool WiFiManager::applyAccessPointMode() { + if (!validateAPConfig()) { + LOG_ERROR("WiFi: Invalid AP configuration."); + return false; + } + LOG_INFO("WiFi: Starting AP '%s'...", ap_ssid_); + WiFi.mode(WIFI_AP); + if (WiFi.softAP(ap_ssid_, ap_password_, ap_channel_)) { + IPAddress ip = WiFi.softAPIP(); + LOG_INFO("WiFi: AP started, IP: %s", ip.toString().c_str()); + state_ = WiFiState::RUNNING_AP; + + if (onApStartedCallback_) { + onApStartedCallback_(ip); + } + return true; + + } else { + LOG_ERROR("WiFi: Failed to start AP."); + return false; + } +} + +bool WiFiManager::applyStationAndAPMode() { + if (!validateStationConfig() || !validateAPConfig()) { + LOG_ERROR("WiFi: Invalid station or AP configuration."); + return false; + } + LOG_INFO("WiFi: Starting AP + Station mode..."); + WiFi.mode(WIFI_AP_STA); + + // Start AP + if (!WiFi.softAP(ap_ssid_, ap_password_, ap_channel_)) { + LOG_ERROR("WiFi: Failed to start AP in AP+STA mode."); + return false; + } + + IPAddress ip = WiFi.softAPIP(); + LOG_INFO("WiFi: AP started, IP: %s", ip.toString().c_str()); + + if (onApStartedCallback_) { + onApStartedCallback_(ip); + } + + // Begin connecting to station + WiFi.begin(station_ssid_, station_password_); + state_ = WiFiState::CONNECTING_STATION; + connection_start_time_ = millis(); + return true; +} + + +bool WiFiManager::validateStationConfig() const { + return strlen(station_ssid_) > 0; +} + +bool WiFiManager::validateAPConfig() const { + return strlen(ap_ssid_) > 0 && strlen(ap_password_) >= 8 && ap_channel_ >= 1 && ap_channel_ <= 13; +} + +void WiFiManager::onStaConnected(std::function callback) { + onStaConnectedCallback_ = callback; +} + +void WiFiManager::onApStarted(std::function callback) { + onApStartedCallback_ = callback; +} + +void WiFiManager::onDisconnected(std::function callback) { + onDisconnectedCallback_ = callback; +} + +void WiFiManager::onPreShutdown(std::function callback) { + onPreShutdownCallback_ = callback; +} \ No newline at end of file diff --git a/src/wifi/wifi_manager.hpp b/src/wifi/wifi_manager.hpp new file mode 100644 index 0000000..242b27b --- /dev/null +++ b/src/wifi/wifi_manager.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include "settings.pb.h" // Your nanopb settings header +#include "device.pb.h" // Your nanopb device header + +/** + * @enum WiFiState + * @brief Defines the possible operational states of the WiFiManager. + */ +enum class WiFiState { + STOPPED, + CONNECTING_STATION, + CONNECTED_STATION, + CONNECTION_FAILED, + RUNNING_AP, + RUNNING_AP_STA +}; + +/** + * @class WiFiManager + * @brief A non-blocking, event-driven manager for ESP32 WiFi connectivity. + * + * This class handles WiFi configuration and state management asynchronously, + * using the ESP32's event system instead of blocking delay loops. + */ +class WiFiManager { +public: + WiFiManager(); + ~WiFiManager(); + + /** + * @brief Applies a new WiFi configuration and initiates the connection process. + * @param settings The settings_SettingsData struct containing the new configuration. + */ + void begin(const settings_SettingsData& settings); + + /** + * @brief The main update loop for the manager. Must be called repeatedly from the main loop(). + * This method handles state transitions like connection timeouts and reconnect attempts. + */ + void update(); + + /** + * @brief Stops all WiFi activity and disconnects. + */ + void stop(); + + /** + * @brief Checks if the device is currently connected as a WiFi station. + * @return true if connected to an access point, false otherwise. + */ + bool isConnected() const; + + /** + * @brief Gets the current operational state of the WiFiManager. + * @return The current WiFiState. + */ + WiFiState getState() const { return state_; } + + /** + * @brief Populates the provided DeviceInfo with current WiFi status information. + * @param deviceInfo Reference to the DeviceInfo message to populate. + */ + void getState(device_DeviceInfo& deviceInfo) const; + + /** + * @brief Registers a callback for when the Station interface connects and gets an IP. + */ + void onStaConnected(std::function callback); + + /** + * @brief Registers a callback for when the Access Point interface has started. + */ + void onApStarted(std::function callback); + + /** + * @brief Registers a callback function to be called upon station disconnection. + */ + void onDisconnected(std::function callback); + + /** + * @brief Registers a callback to be called just before WiFi is disconnected or stopped. + * This allows other modules to shut down gracefully. + */ + void onPreShutdown(std::function callback); +private: + // --- Configuration --- + settings_WifiMode wifi_mode_; + char station_ssid_[33]; + char station_password_[65]; + char ap_ssid_[33]; + char ap_password_[65]; + uint8_t ap_channel_; + + // --- State Machine --- + WiFiState state_ = WiFiState::STOPPED; + uint32_t connection_start_time_ = 0; + uint32_t reconnect_delay_ms_ = 10000; // Wait 10s before reconnecting + uint32_t last_reconnect_attempt_ = 0; + static constexpr uint32_t CONNECTION_TIMEOUT_MS = 30000; // 30-second timeout + + // --- WiFi Event Handlers --- + void registerEvents(); + void unregisterEvents(); + void onWiFiStaGotIP(WiFiEvent_t event, WiFiEventInfo_t info); + void onWiFiStaDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); + + // --- Private Helper Methods --- + bool applyStationMode(); + bool applyAccessPointMode(); + bool applyStationAndAPMode(); + bool validateStationConfig() const; + bool validateAPConfig() const; + + + std::function onStaConnectedCallback_ = nullptr; + std::function onApStartedCallback_ = nullptr; + std::function onDisconnectedCallback_ = nullptr; + std::function onPreShutdownCallback_ = nullptr; + +}; \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/ui/.npmrc b/ui/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/ui/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/ui/.prettierignore b/ui/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/ui/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 0000000..8103a0b --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/app.css" +} diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/ui/components.json b/ui/components.json new file mode 100644 index 0000000..65fd565 --- /dev/null +++ b/ui/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src\\app.css", + "baseColor": "zinc" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/ui/eslint.config.js b/ui/eslint.config.js new file mode 100644 index 0000000..2c49fa6 --- /dev/null +++ b/ui/eslint.config.js @@ -0,0 +1,41 @@ +import prettier from 'eslint-config-prettier'; +import { fileURLToPath } from 'node:url'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig } from 'eslint/config'; +import globals from 'globals'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + 'no-undef': 'off' + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..dadf139 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,5398 @@ +{ + "name": "ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.0.1", + "dependencies": { + "svelte-i18n": "^4.0.1", + "ts-proto": "^2.7.7" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.22.0", + "@internationalized/date": "^3.9.0", + "@lucide/svelte": "^0.544.0", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^22", + "bits-ui": "^2.11.4", + "clsx": "^2.1.1", + "eslint": "^9.22.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "mode-watcher": "^1.1.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "singlefile": "^0.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "svelte-sonner": "^1.0.5", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.1.1", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.4.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^7.0.4", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz", + "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.0.tgz", + "integrity": "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@internationalized/date": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.9.0.tgz", + "integrity": "sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lucide/svelte": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.544.0.tgz", + "integrity": "sha512-9f9O6uxng2pLB01sxNySHduJN3HTl5p0HDu4H26VR51vhZfiMzyOMe9Mhof3XAk4l813eTtl+/DYRvGyoRR+yw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", + "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.9.tgz", + "integrity": "sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.43.5", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.43.5.tgz", + "integrity": "sha512-44Mm5csR4mesKx2Eyhtk8UVrLJ4c04BT2wMTfYGKJMOkUqpHP5KLL2DPV0hXUA4t4+T3ZYe0aBygd42lVYv2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.3.2", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.17", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz", + "integrity": "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/type-utils": "8.44.1", + "@typescript-eslint/utils": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.44.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.1.tgz", + "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.1.tgz", + "integrity": "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.44.1", + "@typescript-eslint/types": "^8.44.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz", + "integrity": "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz", + "integrity": "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz", + "integrity": "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1", + "@typescript-eslint/utils": "8.44.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.1.tgz", + "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz", + "integrity": "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.44.1", + "@typescript-eslint/tsconfig-utils": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.1.tgz", + "integrity": "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz", + "integrity": "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bits-ui": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.11.4.tgz", + "integrity": "sha512-OlVBJhNUMDHbIAf8oDAyPchIrU8b1S5NAMm6enMZSKx5HKcf/QPI485/BL1r4EPlv4O3m45e59hBRCETtYFdxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.31.1", + "svelte-toolbelt": "^0.10.4", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/case-anything": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", + "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", + "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dprint-node": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz", + "integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==", + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + } + }, + "node_modules/dprint-node/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.12.4.tgz", + "integrity": "sha512-hD7wPe+vrPgx3U2X2b/wyTMtWobm660PygMGKrWWYTc9lvtY8DpNFDaU2CJQn1szLjGbn/aJ3g8WiXuKakrEkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "license": "MIT" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/intl-messageformat": { + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mode-watcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", + "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "runed": "^0.25.0", + "svelte-toolbelt": "^0.7.1" + }, + "peerDependencies": { + "svelte": "^5.27.0" + } + }, + "node_modules/mode-watcher/node_modules/runed": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", + "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/runed": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.31.1.tgz", + "integrity": "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/singlefile": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/singlefile/-/singlefile-0.0.0.tgz", + "integrity": "sha512-OBf0xxNWd07ZDM0dcpK1jq4AvLmS1dUWDSgUomMBLPr6qU/aut4rCTDZVEi2itqHWfAYQepzkxRI5DUyWPAnkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.39.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.6.tgz", + "integrity": "sha512-bOJXmuwLNaoqPCTWO8mPu/fwxI5peGE5Efe7oo6Cakpz/G60vsnVF6mxbGODaxMUFUKEnjm6XOwHEqOht6cbvw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^2.1.0", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.2.tgz", + "integrity": "sha512-71udP5w2kaSTcX8iV0hn3o2FWlabQHhJTJLIQrCqMsrcOeDUO2VhCQKKCA8AMVHSPwdxLEWkUWh9OKxns5PD9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.3.tgz", + "integrity": "sha512-oTrDR8Z7Wnguut7QH3YKh7JR19xv1seB/bz4dxU5J/86eJtZOU4eh0/jZq4dy6tAlz/KROxnkRQspv5ZEt7t+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svelte-i18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz", + "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", + "license": "MIT", + "dependencies": { + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^10.5.3", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-i18n": "dist/cli.js" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/svelte-sonner": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz", + "integrity": "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "runed": "^0.28.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-sonner/node_modules/runed": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz", + "integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.5.tgz", + "integrity": "sha512-8e+eWTgxw1aiLxhDE8Rb1X6AoLitqpJz+WhAul2W7W58C8KoLoJQf1TgQdFPBiCPJ0Jg5y0Zi1uyua9em4VS0w==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.29.0", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.29.2.tgz", + "integrity": "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.1.1.tgz", + "integrity": "sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "license": "MIT", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-poet": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.12.0.tgz", + "integrity": "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==", + "license": "Apache-2.0", + "dependencies": { + "dprint-node": "^1.0.8" + } + }, + "node_modules/ts-proto": { + "version": "2.7.7", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.7.tgz", + "integrity": "sha512-/OfN9/Yriji2bbpOysZ/Jzc96isOKz+eBTJEcKaIZ0PR6x1TNgVm4Lz0zfbo+J0jwFO7fJjJyssefBPQ0o1V9A==", + "license": "ISC", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "case-anything": "^2.1.13", + "ts-poet": "^6.12.0", + "ts-proto-descriptors": "2.0.0" + }, + "bin": { + "protoc-gen-ts_proto": "protoc-gen-ts_proto" + } + }, + "node_modules/ts-proto-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.0.0.tgz", + "integrity": "sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==", + "license": "ISC", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.1.tgz", + "integrity": "sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1", + "@typescript-eslint/utils": "8.44.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-compression": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz", + "integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "debug": "^4.3.3", + "fs-extra": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, + "node_modules/vite-plugin-singlefile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz", + "integrity": "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.44.1", + "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..d4be74c --- /dev/null +++ b/ui/package.json @@ -0,0 +1,58 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "copy": "node scripts/copy-web-files.js", + "proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto.cmd --ts_proto_out=./src/lib/gen --ts_proto_opt=useOptionals=messages,outputEncodeMethods=true,outputJsonMethods=false,outputClientImpl=true,addGrpcMetadata=false --proto_path=../proto ../proto/*.proto" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.22.0", + "@internationalized/date": "^3.9.0", + "@lucide/svelte": "^0.544.0", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^22", + "bits-ui": "^2.11.4", + "clsx": "^2.1.1", + "eslint": "^9.22.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "mode-watcher": "^1.1.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "singlefile": "^0.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "svelte-sonner": "^1.0.5", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.1.1", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.4.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^7.0.4", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-singlefile": "^2.3.0" + }, + "dependencies": { + "svelte-i18n": "^4.0.1", + "ts-proto": "^2.7.7" + } +} diff --git a/ui/scripts/copy-web-files.js b/ui/scripts/copy-web-files.js new file mode 100644 index 0000000..ba37937 --- /dev/null +++ b/ui/scripts/copy-web-files.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import path from 'path'; +import zlib from 'zlib'; +import { fileURLToPath } from 'url'; +import { pipeline } from 'stream/promises'; + +// --- Konfiguration --- +const GZIP_OPTIONS = { level: zlib.constants.Z_BEST_COMPRESSION }; +// -------------------- + +/** + * Kopiert und komprimiert nur die index.html Datei von sourceDir nach destDir. + * Die Datei wird nur als .gz gespeichert. + * + * @param {string} sourceDir - Quellordner + * @param {string} destDir - Zielordner + */ +async function processIndexHtml(sourceDir, destDir) { + console.log(`Processing index.html from '${sourceDir}' to '${destDir}'`); + + // Erstelle Zielverzeichnis, wenn es nicht existiert + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + const sourcePath = path.join(sourceDir, 'index.html'); + const destPathGz = path.join(destDir, 'index.html.gz'); + + // Prüfe ob index.html existiert + if (!fs.existsSync(sourcePath)) { + throw new Error(`index.html not found in '${sourceDir}'`); + } + + // Komprimiere index.html + console.log(`Compressing ${sourcePath} -> ${destPathGz}`); + try { + const readStream = fs.createReadStream(sourcePath); + const gzipStream = zlib.createGzip(GZIP_OPTIONS); + const writeStream = fs.createWriteStream(destPathGz); + await pipeline(readStream, gzipStream, writeStream); + } catch (err) { + console.error(`Error compressing ${sourcePath}:`, err); + throw err; + } +} + +/** + * Lösche den Ordner und seinen gesamten Inhalt + * + * @param {string} dir - Der zu löschende Ordner + */ +function removeDir(dir) { + if (fs.existsSync(dir)) { + console.log(`Removing existing directory '${dir}'`); + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +// Hauptfunktion +async function main() { + // Projekt-Root-Verzeichnis ermitteln + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const scriptDir = __dirname; + const projectRoot = path.resolve(scriptDir, '..'); + const rootDir = path.resolve(projectRoot, '..'); + + // Quell- und Zielverzeichnisse + const sourceDir = path.join(projectRoot, 'build'); + const destDir = path.join(rootDir, 'data', 'web'); + + if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) { + console.error(`Error: Source directory '${sourceDir}' doesn't exist or is not a directory!`); + console.error("Make sure to build the Svelte app first with 'npm run build'"); + process.exit(1); + } + + removeDir(destDir); + + try { + await processIndexHtml(sourceDir, destDir); + console.log("index.html processed successfully!"); + } catch(err) { + console.error("Error during file processing:", err); + process.exit(1); + } +} + +// Ausführen des Skripts +main(); \ No newline at end of file diff --git a/ui/src/app.css b/ui/src/app.css new file mode 100644 index 0000000..ee2d8d5 --- /dev/null +++ b/ui/src/app.css @@ -0,0 +1,121 @@ +@import "tailwindcss"; + +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.015 286.067); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.21 0.006 285.885); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.015 286.067); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/ui/src/app.d.ts b/ui/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/ui/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/ui/src/app.html b/ui/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/ui/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/ui/src/hooks.server.ts b/ui/src/hooks.server.ts new file mode 100644 index 0000000..8ce0a5d --- /dev/null +++ b/ui/src/hooks.server.ts @@ -0,0 +1,278 @@ +import { + GetSettingsResponse, + SettingsData, + UpdateSettingsRequest, + UpdateSettingsResponse, + WifiMode +} from '$lib/gen/settings'; +import { DeviceInfo } from '$lib/gen/device'; +import { + AddUserRequest, + AddUserResponse, + DeleteUserRequest, + DeleteUserResponse, + GetUsersResponse, + UpdateUserPasswordRequest, + UpdateUserPasswordResponse, + WebUiLoginRequest, + WebUiLoginResponse +} from '$lib/gen/webui'; +import type { Handle } from '@sveltejs/kit'; + +// ESP32 proxy URL - comment out this line to use mock API instead +const ESP32_URL = 'http://192.168.20.38'; + +// Mock settings storage (in-memory for development) +let mockSettings = SettingsData.create({ + syncServerUrl: 'http://mock-server.com', + syncIntervalSeconds: 1800, // 30 minutes in seconds + autoSync: true, + wifiMode: WifiMode.WIFI_MODE_STATION, + stationSsid: 'MockWiFi', + stationPassword: 'password123', + apSsid: 'ESP32-AP', + apPassword: 'ap123456', + apChannel: 6, + enableFallbackAp: true, + updatedAt: Date.now(), + version: 1 +}); + +// Mock users storage (in-memory for development, max 5 users) +let mockUsers: WebUiLoginRequest[] = [ + WebUiLoginRequest.create({ username: 'admin', password: 'password' }), + WebUiLoginRequest.create({ username: 'user1', password: 'pass123' }) +]; + +export const handle: Handle = async ({ event, resolve }) => { + // If ESP32_URL is defined, proxy to ESP32, otherwise use mock + if (ESP32_URL && event.url.pathname.startsWith('/api/')) { + const esp32Url = ESP32_URL + event.url.pathname; + + try { + let body: ArrayBuffer | undefined; + if (event.request.method !== 'GET') { + body = await event.request.arrayBuffer(); + } + + const headers: Record = { + 'Content-Type': 'application/octet-stream' + }; + + // Forward Authorization header if present + const authHeader = event.request.headers.get('Authorization'); + if (authHeader) { + headers['Authorization'] = authHeader; + console.log('Authorization:', authHeader); + } + + const response = await fetch(esp32Url, { + method: event.request.method, + headers, + body: body + }); + + if (!response.ok) { + return new Response(response.statusText, { status: response.status }); + } + + const responseData = await response.arrayBuffer(); + + return new Response(responseData, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); + } catch (error) { + console.error('ESP32 proxy error:', error); + return new Response('ESP32 connection failed', { status: 502 }); + } + } + + // Mock the ESP32 API endpoints (when ESP32_URL is not set) + if (event.url.pathname === '/api/login' && event.request.method === 'POST') { + // Get the binary data + const buffer = await event.request.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + + // Decode the protobuf request + const loginReq = WebUiLoginRequest.decode(uint8Array); + + // Simple authentication (same as ESP32) + const success = loginReq.username === 'admin' && loginReq.password === 'password'; + + // Create response + const loginResp = WebUiLoginResponse.create({ + success, + token: success ? 'mock_token' : undefined, + error: success ? undefined : 'Invalid credentials' + }); + + // Encode response + const responseData = WebUiLoginResponse.encode(loginResp).finish(); + + return new Response(responseData, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); + } + + // Mock settings endpoints + if (event.url.pathname === '/api/settings') { + if (event.request.method === 'GET') { + // Mock get settings using global mockSettings + const mockDeviceInfo = DeviceInfo.create({ + deviceId: 'ESP32-001', + firmwareVersion: '1.0.0-mock', + hardwareVersion: 'ESP32-v1', + uptimeSeconds: 123 + }); + + const getResp = GetSettingsResponse.create({ + settings: mockSettings + }); + + const responseData = GetSettingsResponse.encode(getResp).finish(); + + return new Response(responseData, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); + } else if (event.request.method === 'POST') { + // Mock update settings + const buffer = await event.request.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + + // Decode the protobuf request + const updateReq = UpdateSettingsRequest.decode(uint8Array); + + // Update the global mock settings + mockSettings = SettingsData.create({ + ...mockSettings, + ...updateReq.settings, + updatedAt: Date.now(), + version: mockSettings.version + 1 + }); + + console.log('Mock updated settings:', mockSettings); + + // Create success response + const updateResp = UpdateSettingsResponse.create({ + success: true + }); + + const responseData = UpdateSettingsResponse.encode(updateResp).finish(); + + return new Response(responseData, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); + } + } + + // Mock user endpoints + if (event.url.pathname === '/api/users') { + if (event.request.method === 'GET') { + // Get users + const usernames = mockUsers.map((u) => u.username); + const getResp = GetUsersResponse.create({ usernames }); + const responseData = GetUsersResponse.encode(getResp).finish(); + + return new Response(responseData, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); + } else if (event.request.method === 'POST') { + // Add user + const buffer = await event.request.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + const addReq = AddUserRequest.decode(uint8Array); + + // Check if user already exists + const existingUser = mockUsers.find((u) => u.username === addReq.username); + if (existingUser) { + const addResp = AddUserResponse.create({ success: false, error: 'User already exists' }); + const responseData = AddUserResponse.encode(addResp).finish(); + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } + + // Check if table is full (max 5 users) + if (mockUsers.length >= 5) { + const addResp = AddUserResponse.create({ success: false, error: 'User table full' }); + const responseData = AddUserResponse.encode(addResp).finish(); + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } + + // Add user + mockUsers.push( + WebUiLoginRequest.create({ username: addReq.username, password: addReq.password }) + ); + const addResp = AddUserResponse.create({ success: true }); + const responseData = AddUserResponse.encode(addResp).finish(); + + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } else if (event.request.method === 'DELETE') { + // Delete user + const buffer = await event.request.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + const deleteReq = DeleteUserRequest.decode(uint8Array); + + const userIndex = mockUsers.findIndex((u) => u.username === deleteReq.username); + if (userIndex === -1) { + const deleteResp = DeleteUserResponse.create({ success: false, error: 'User not found' }); + const responseData = DeleteUserResponse.encode(deleteResp).finish(); + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } + + mockUsers.splice(userIndex, 1); + const deleteResp = DeleteUserResponse.create({ success: true }); + const responseData = DeleteUserResponse.encode(deleteResp).finish(); + + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } + } + + // Mock user password update endpoint + if (event.url.pathname === '/api/users/password' && event.request.method === 'POST') { + const buffer = await event.request.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + const updateReq = UpdateUserPasswordRequest.decode(uint8Array); + + const user = mockUsers.find((u) => u.username === updateReq.username); + if (!user) { + const updateResp = UpdateUserPasswordResponse.create({ + success: false, + error: 'User not found' + }); + const responseData = UpdateUserPasswordResponse.encode(updateResp).finish(); + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } + + user.password = updateReq.newPassword; + const updateResp = UpdateUserPasswordResponse.create({ success: true }); + const responseData = UpdateUserPasswordResponse.encode(updateResp).finish(); + + return new Response(responseData, { + headers: { 'Content-Type': 'application/octet-stream' } + }); + } + + // For other requests, continue normally + return resolve(event); +}; diff --git a/ui/src/lib/api/constants.ts b/ui/src/lib/api/constants.ts new file mode 100644 index 0000000..058e26b --- /dev/null +++ b/ui/src/lib/api/constants.ts @@ -0,0 +1 @@ +export const BASE_URL = window.location.origin; diff --git a/ui/src/lib/api/fetch.ts b/ui/src/lib/api/fetch.ts new file mode 100644 index 0000000..55b2171 --- /dev/null +++ b/ui/src/lib/api/fetch.ts @@ -0,0 +1,26 @@ +import { goto } from '$app/navigation'; +import { get } from 'svelte/store'; +import { authStore } from '../stores/auth'; + +export async function authenticatedFetch( + input: RequestInfo | URL, + init?: RequestInit +): Promise { + const authState = get(authStore); + const headers = new Headers(init?.headers); + + if (authState.token && !headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${authState.token}`); + } + + const updatedInit = { ...init, headers }; + + const response = await fetch(input, updatedInit); + + if (response.status === 401) { + authStore.logout(); + goto('/login'); + } + + return response; +} diff --git a/ui/src/lib/api/webui.ts b/ui/src/lib/api/webui.ts new file mode 100644 index 0000000..fb042c0 --- /dev/null +++ b/ui/src/lib/api/webui.ts @@ -0,0 +1,402 @@ +import { + GetSettingsResponse, + SettingsData, + UpdateSettingsRequest, + UpdateSettingsResponse +} from '../gen/settings'; +import { HardwareConfig, SensorOTAEnable } from '../gen/hardware'; +import { + AddUserRequest, + AddUserResponse, + DeleteUserRequest, + DeleteUserResponse, + GetUsersResponse, + UpdateUserPasswordRequest, + UpdateUserPasswordResponse, + WebUiAuthCheckResponse, + WebUiLoginRequest, + WebUiLoginResponse +} from '../gen/webui'; +import { DeviceStatus } from '../gen/device'; +import { BASE_URL } from './constants'; +import { authenticatedFetch } from './fetch'; + +export class WebUIApi { + private baseUrl: string; + private username: string | null = null; + + constructor(baseUrl: string = BASE_URL) { + this.baseUrl = baseUrl; + } + + async login(username: string, password: string): Promise { + const request = WebUiLoginRequest.create({ username, password }); + const requestData = WebUiLoginRequest.encode(request).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const loginResponse = WebUiLoginResponse.decode(new Uint8Array(responseData)); + + if (loginResponse.token) { + this.username = username; + } + + return loginResponse; + } + + async authchk(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/authchk`, { + method: 'GET' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const authResponse = WebUiAuthCheckResponse.decode(new Uint8Array(responseData)); + + if (authResponse.authenticated) { + this.username = authResponse.username || null; + } + + return authResponse; + } + + async getStatus(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/status`, { + method: 'GET' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const statusResponse = DeviceStatus.decode(new Uint8Array(responseData)); + return statusResponse; + } + + async getSettings(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/settings`, { + method: 'GET' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const settingsResponse = GetSettingsResponse.decode(new Uint8Array(responseData)); + return settingsResponse; + } + + async updateSettings(settings: SettingsData): Promise { + const request = UpdateSettingsRequest.create({ settings }); + const requestData = UpdateSettingsRequest.encode(request).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/settings`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const updateResponse = UpdateSettingsResponse.decode(new Uint8Array(responseData)); + + return updateResponse; + } + + async getHardware(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/hardware`, { + method: 'GET' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const hardwareConfig = HardwareConfig.decode(new Uint8Array(responseData)); + return hardwareConfig; + } + + async updateHardware(config: HardwareConfig): Promise { + const requestData = HardwareConfig.encode(config).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/hardware`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } + + async getUsers(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/users`, { + method: 'GET', + headers: { + 'Content-Type': 'application/octet-stream' + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const usersResponse = GetUsersResponse.decode(new Uint8Array(responseData)); + + return usersResponse; + } + + async addUser(username: string, password: string): Promise { + const request = AddUserRequest.create({ username, password }); + const requestData = AddUserRequest.encode(request).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/users`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const addResponse = AddUserResponse.decode(new Uint8Array(responseData)); + + return addResponse; + } + + async deleteUser(username: string): Promise { + const request = DeleteUserRequest.create({ username }); + const requestData = DeleteUserRequest.encode(request).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/users`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const deleteResponse = DeleteUserResponse.decode(new Uint8Array(responseData)); + + return deleteResponse; + } + + async updateUserPassword( + username: string, + newPassword: string + ): Promise { + const request = UpdateUserPasswordRequest.create({ username, newPassword }); + const requestData = UpdateUserPasswordRequest.encode(request).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/users/password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.arrayBuffer(); + const updateResponse = UpdateUserPasswordResponse.decode(new Uint8Array(responseData)); + + return updateResponse; + } + + get getUsername(): string | null { + return this.username; + } + + async getRfidDatabase(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/db`, { + method: 'GET' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const binaryData = await response.arrayBuffer(); + const dataView = new DataView(binaryData); + + // Read uint32 values as big endian + const rfidIds = new Uint32Array(binaryData.byteLength / 4); + for (let i = 0; i < rfidIds.length; i++) { + rfidIds[i] = dataView.getUint32(i * 4, false); // false = big endian + } + + return rfidIds; + } + + async updateRfidDatabase(rfidIds: Uint32Array): Promise { + // Sort the RFID IDs + const sortedIds = new Uint32Array(rfidIds); + sortedIds.sort((a, b) => a - b); + + // Create a buffer and write values in big endian format + const buffer = new ArrayBuffer(sortedIds.length * 4); + const dataView = new DataView(buffer); + + for (let i = 0; i < sortedIds.length; i++) { + dataView.setUint32(i * 4, sortedIds[i], false); // false = big endian + } + + const response = await authenticatedFetch(`${this.baseUrl}/api/db`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: buffer + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } + + async sync(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/sync`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } + + async fullSync(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/sync/full`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } + + async enableOTA(otaConfig: SensorOTAEnable): Promise { + const requestData = SensorOTAEnable.encode(otaConfig).finish(); + + const response = await authenticatedFetch(`${this.baseUrl}/api/ota`, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: requestData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } + + async restartSensor(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/restart-sensor`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } + + async enableESPOTA(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/esp-ota-enable`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } + + async disableESPOTA(): Promise { + const response = await authenticatedFetch(`${this.baseUrl}/api/esp-ota-disable`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } + + async getESPOTAStatus(): Promise<{ enabled: boolean; status: string }> { + const response = await authenticatedFetch(`${this.baseUrl}/api/esp-ota-status`, { + method: 'GET' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } + + async uploadESPOTAFirmware(file: File): Promise { + const formData = new FormData(); + formData.append('firmware', file); + + const response = await authenticatedFetch(`${this.baseUrl}/api/esp-ota-upload`, { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.text(); + } +} + +export const webUIApi = new WebUIApi(); diff --git a/ui/src/lib/assets/favicon.svg b/ui/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/ui/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/ui/src/lib/components/dashboard/DeviceInfo.svelte b/ui/src/lib/components/dashboard/DeviceInfo.svelte new file mode 100644 index 0000000..b423a3a --- /dev/null +++ b/ui/src/lib/components/dashboard/DeviceInfo.svelte @@ -0,0 +1,88 @@ + + + + + Device Information + Read-only information about this device + + + {#if deviceInfo} +
+
+ +

{deviceInfo.deviceId || 'N/A'}

+
+
+ +

{deviceInfo.firmwareVersion || 'N/A'}

+
+
+ +

{deviceInfo.hardwareVersion || 'N/A'}

+
+
+ +

{formatUptime(deviceInfo.uptimeSeconds)}

+
+
+ +
+

WiFi Status

+
+
+ +

{getWifiStatus(deviceInfo.staConnectionState)}

+ {#if deviceInfo.staConnectionState === 1} +

IP: {deviceInfo.staIp}

+

Signal: {deviceInfo.staSignalStrength} dBm

+ {/if} +
+
+ +

{getWifiStatus(deviceInfo.apConnectionState)}

+ {#if deviceInfo.apConnectionState === 1} +

IP: {deviceInfo.apIp}

+

Clients: {deviceInfo.apClientCount}

+ {/if} +
+
+
+ {:else} +

Loading device information...

+ {/if} +
+
diff --git a/ui/src/lib/components/settings/HardwareConfig.svelte b/ui/src/lib/components/settings/HardwareConfig.svelte new file mode 100644 index 0000000..121e2a5 --- /dev/null +++ b/ui/src/lib/components/settings/HardwareConfig.svelte @@ -0,0 +1,369 @@ + + +
+ + + + + 🔒 + Door Relay + + + Configure the door lock mechanism. The relay controls the door opening duration and which + GPIO pin is used. + + + +
+
+ + +

+ How long the door stays unlocked after valid RFID scan +

+
+
+ + +

GPIO pin connected to the door relay

+
+
+
+
+ + + + + + 📡 + Sensor Unit (RFID Reader) + + + The RFID reader unit with integrated RGB LED. Communication happens via serial connection + (UART). The reader scans RFID cards and communicates card data to the main controller. + + + +
+ + +
+

Enable or disable the serial sensor unit.

+ {#if enableSerialSensor} +
+
+ + +

+ UART RX pin for receiving data from RFID reader +

+
+
+ + +

+ UART TX pin for sending commands to RFID reader +

+
+
+ {/if} +
+
+ + + + + + 🔓 + Permanent Unlock + + + Override mode allows keeping the door permanently unlocked. Use with caution - this bypasses + all access control. + + + +
+ + +
+

+ When enabled, the door relay stays activated permanently, ignoring RFID access control. +

+
+
+ + + + + + 💡 + LED Configuration + + + Configure RGB LED animations for different access states. Each LED can have custom colors, + brightness, and animation patterns. + + + + +
+
+

On Open LED

+

+ {onOpenLed + ? `Animation: ${getAnimationName(getCurrentAnimation(onOpenLed))}` + : 'Not configured'} +

+

Shows when door is unlocked after valid RFID scan

+
+
+ + {#if onOpenLed} + + {/if} +
+
+ + +
+
+

Default LED

+

+ {defaultLed + ? `Animation: ${getAnimationName(getCurrentAnimation(defaultLed))}` + : 'Not configured'} +

+

Default idle state when system is ready

+
+
+ + {#if defaultLed} + + {/if} +
+
+ + +
+
+

On Invalid LED

+

+ {onInvalidLed + ? `Animation: ${getAnimationName(getCurrentAnimation(onInvalidLed))}` + : 'Not configured'} +

+

Shows when RFID scan is rejected

+
+
+ + {#if onInvalidLed} + + {/if} +
+
+ + +
+
+

On Override Open LED

+

+ {onOverrideOpenLed + ? `Animation: ${getAnimationName(getCurrentAnimation(onOverrideOpenLed))}` + : 'Not configured'} +

+

Shows when door is in permanent unlock mode

+
+
+ + {#if onOverrideOpenLed} + + {/if} +
+
+ + +
+
+

On Interception LED

+

+ {onInterceptionLed + ? `Animation: ${getAnimationName(getCurrentAnimation(onInterceptionLed))}` + : 'Not configured'} +

+

Shows when RFID card is read to the ui

+
+
+ + {#if onInterceptionLed} + + {/if} +
+
+
+
+
+ + + (dialogOpen = false)} +/> diff --git a/ui/src/lib/components/settings/LedConfigDialog.svelte b/ui/src/lib/components/settings/LedConfigDialog.svelte new file mode 100644 index 0000000..64b3683 --- /dev/null +++ b/ui/src/lib/components/settings/LedConfigDialog.svelte @@ -0,0 +1,271 @@ + + + + + + Configure {ledType} LED + Set the LED animation and parameters. + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + {#if tempConfig.staticParams} +
+ + { + staticColor = color; + updateStaticColor(); + }} + /> +
+ {:else if tempConfig.pulseParams} +
+
+ + { + pulseColor = color; + updatePulseColor(); + }} + /> +
+
+ + +
+
+ {:else if tempConfig.fadeParams} +
+
+ + {#each fadeColors as color, i} +
+ { + fadeColors[i] = newColor; + updateFadeColor(i); + }} + class="flex-1" + /> + +
+ {/each} + +
+
+ + +
+
+ {:else if tempConfig.flickerParams} +
+
+ + { + flickerColor = color; + updateFlickerColor(); + }} + /> +
+
+ + +
+
+ {/if} +
+ +
+ + +
+
+
diff --git a/ui/src/lib/components/settings/SyncServerConfig.svelte b/ui/src/lib/components/settings/SyncServerConfig.svelte new file mode 100644 index 0000000..837ba74 --- /dev/null +++ b/ui/src/lib/components/settings/SyncServerConfig.svelte @@ -0,0 +1,83 @@ + + + + + Sync Server Configuration + Configure the server URL and synchronization settings + + +
+ + +

The URL of the synchronization server for data exchange

+
+ +
+ + +

API key for device authentication with the sync server

+
+ + + +
+
+ + +
+ + {#if autoSync} +
+ + +

+ How often to synchronize data with the server (5-86400 seconds) +

+
+ {/if} +
+
+
diff --git a/ui/src/lib/components/settings/WifiSettings.svelte b/ui/src/lib/components/settings/WifiSettings.svelte new file mode 100644 index 0000000..51fe3bb --- /dev/null +++ b/ui/src/lib/components/settings/WifiSettings.svelte @@ -0,0 +1,129 @@ + + + + + WiFi Settings + Configure WiFi connection modes and access point settings + + +
+
+ + +

+ Choose how the device connects to and provides WiFi networks +

+
+ + {#if [WifiMode.WIFI_MODE_STATION, WifiMode.WIFI_MODE_AP_STATION].includes(wifiMode)} +
+

Station Settings (Connect to WiFi)

+
+
+ + +
+
+ + +
+
+
+ {/if} + + {#if [WifiMode.WIFI_MODE_STATION, WifiMode.WIFI_MODE_AP, WifiMode.WIFI_MODE_AP_STATION].includes(wifiMode)} +
+ {#if wifiMode === WifiMode.WIFI_MODE_STATION} +
+
+ + +
+

+ If station connection fails, create AP for configuration +

+
+ {/if} +

Access Point Settings (Create WiFi Network)

+ {#if [WifiMode.WIFI_MODE_STATION, WifiMode.WIFI_MODE_AP_STATION].includes(wifiMode) || (wifiMode === WifiMode.WIFI_MODE_STATION && enableFallbackAp)} +
+
+ + +
+
+ + +

Password must be 8-63 characters long

+
+
+ + +
+
+ {/if} +
+ {/if} +
+
+
diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..a005691 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..a7b0cf7 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..6c3c604 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..2ec67dc --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..f78b97a --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..c8fa762 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..a64ee76 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7ef2b5f --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/ui/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000..b22d1d5 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/alert-dialog/index.ts b/ui/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..cc281c5 --- /dev/null +++ b/ui/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,39 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Portal = AlertDialogPrimitive.Portal; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/ui/src/lib/components/ui/alert/alert-description.svelte b/ui/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..8b56aed --- /dev/null +++ b/ui/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/alert/alert-title.svelte b/ui/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..77e45ad --- /dev/null +++ b/ui/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/alert/alert.svelte b/ui/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..2b2eff9 --- /dev/null +++ b/ui/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/ui/src/lib/components/ui/alert/index.ts b/ui/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..97e21b4 --- /dev/null +++ b/ui/src/lib/components/ui/alert/index.ts @@ -0,0 +1,14 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; +export { alertVariants, type AlertVariant } from "./alert.svelte"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, +}; diff --git a/ui/src/lib/components/ui/button/button.svelte b/ui/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..4daf453 --- /dev/null +++ b/ui/src/lib/components/ui/button/button.svelte @@ -0,0 +1,80 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/ui/src/lib/components/ui/button/index.ts b/ui/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/ui/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/ui/src/lib/components/ui/card/card-action.svelte b/ui/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-content.svelte b/ui/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-description.svelte b/ui/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/ui/src/lib/components/ui/card/card-footer.svelte b/ui/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..cf43353 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-header.svelte b/ui/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..8a91abb --- /dev/null +++ b/ui/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-title.svelte b/ui/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..22586e6 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card.svelte b/ui/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..99448cc --- /dev/null +++ b/ui/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/index.ts b/ui/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/ui/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/ui/src/lib/components/ui/checkbox/checkbox.svelte b/ui/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..1622e05 --- /dev/null +++ b/ui/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
+ {#if checked} + + {:else if indeterminate} + + {/if} +
+ {/snippet} +
diff --git a/ui/src/lib/components/ui/checkbox/index.ts b/ui/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/ui/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/ui/src/lib/components/ui/color-picker-2d.svelte b/ui/src/lib/components/ui/color-picker-2d.svelte new file mode 100644 index 0000000..21450c7 --- /dev/null +++ b/ui/src/lib/components/ui/color-picker-2d.svelte @@ -0,0 +1,205 @@ + + +
+
+ +
+
+
{value.toUpperCase()}
+
+
+
diff --git a/ui/src/lib/components/ui/color-picker-sliders.svelte b/ui/src/lib/components/ui/color-picker-sliders.svelte new file mode 100644 index 0000000..3c097ae --- /dev/null +++ b/ui/src/lib/components/ui/color-picker-sliders.svelte @@ -0,0 +1,110 @@ + + +
+
+
+
{value.toUpperCase()}
+
+ +
+
+
+ Red + {rgb.r} +
+ (rgb = { ...rgb, r: newValue })} + class="w-full" + /> +
+ +
+
+ Green + {rgb.g} +
+ (rgb = { ...rgb, g: newValue })} + class="w-full" + /> +
+ +
+
+ Blue + {rgb.b} +
+ (rgb = { ...rgb, b: newValue })} + class="w-full" + /> +
+
+
diff --git a/ui/src/lib/components/ui/color-picker.svelte b/ui/src/lib/components/ui/color-picker.svelte new file mode 100644 index 0000000..2447e43 --- /dev/null +++ b/ui/src/lib/components/ui/color-picker.svelte @@ -0,0 +1,421 @@ + + +
+ + + + + + + + Color Picker - {value.toUpperCase()} + + +
+ +
+ Color Wheel + +
+ + +
+ RGB Sliders + +
+
+ Red + {rgb.r} +
+ (rgb = { ...rgb, r: newValue })} + class="w-full" + /> +
+ +
+
+ Green + {rgb.g} +
+ (rgb = { ...rgb, g: newValue })} + class="w-full" + /> +
+ +
+
+ Blue + {rgb.b} +
+ (rgb = { ...rgb, b: newValue })} + class="w-full" + /> +
+
+
+ + +
+
+
+ + + Close + +
+
+
diff --git a/ui/src/lib/components/ui/input/index.ts b/ui/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..f47b6d3 --- /dev/null +++ b/ui/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input, +}; diff --git a/ui/src/lib/components/ui/input/input.svelte b/ui/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..19c6dae --- /dev/null +++ b/ui/src/lib/components/ui/input/input.svelte @@ -0,0 +1,51 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/ui/src/lib/components/ui/label/index.ts b/ui/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..8bfca0b --- /dev/null +++ b/ui/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/ui/src/lib/components/ui/label/label.svelte b/ui/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..d0afda3 --- /dev/null +++ b/ui/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/components/ui/popover/index.ts b/ui/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000..9f30922 --- /dev/null +++ b/ui/src/lib/components/ui/popover/index.ts @@ -0,0 +1,17 @@ +import { Popover as PopoverPrimitive } from "bits-ui"; +import Content from "./popover-content.svelte"; +import Trigger from "./popover-trigger.svelte"; +const Root = PopoverPrimitive.Root; +const Close = PopoverPrimitive.Close; + +export { + Root, + Content, + Trigger, + Close, + // + Root as Popover, + Content as PopoverContent, + Trigger as PopoverTrigger, + Close as PopoverClose, +}; diff --git a/ui/src/lib/components/ui/popover/popover-content.svelte b/ui/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000..9bced7a --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/ui/src/lib/components/ui/popover/popover-trigger.svelte b/ui/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000..586323c --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/select/index.ts b/ui/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..9e8d3e9 --- /dev/null +++ b/ui/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import { Select as SelectPrimitive } from "bits-ui"; + +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import GroupHeading from "./select-group-heading.svelte"; + +const Root = SelectPrimitive.Root; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, +}; diff --git a/ui/src/lib/components/ui/select/select-content.svelte b/ui/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..dc16d65 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + + + diff --git a/ui/src/lib/components/ui/select/select-group-heading.svelte b/ui/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..1fab5f0 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/select/select-group.svelte b/ui/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..5454fdb --- /dev/null +++ b/ui/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/select/select-item.svelte b/ui/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..49dbbd7 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/ui/src/lib/components/ui/select/select-label.svelte b/ui/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..4696025 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/select/select-scroll-down-button.svelte b/ui/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..3629205 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/ui/src/lib/components/ui/select/select-scroll-up-button.svelte b/ui/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..1aa2300 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/ui/src/lib/components/ui/select/select-separator.svelte b/ui/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..0eac3eb --- /dev/null +++ b/ui/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/ui/src/lib/components/ui/select/select-trigger.svelte b/ui/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..d405187 --- /dev/null +++ b/ui/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/ui/src/lib/components/ui/separator/index.ts b/ui/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/ui/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/ui/src/lib/components/ui/separator/separator.svelte b/ui/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..09d88f4 --- /dev/null +++ b/ui/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/components/ui/sheet/index.ts b/ui/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..01d40c8 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,36 @@ +import { Dialog as SheetPrimitive } from "bits-ui"; +import Trigger from "./sheet-trigger.svelte"; +import Close from "./sheet-close.svelte"; +import Overlay from "./sheet-overlay.svelte"; +import Content from "./sheet-content.svelte"; +import Header from "./sheet-header.svelte"; +import Footer from "./sheet-footer.svelte"; +import Title from "./sheet-title.svelte"; +import Description from "./sheet-description.svelte"; + +const Root = SheetPrimitive.Root; +const Portal = SheetPrimitive.Portal; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription, +}; diff --git a/ui/src/lib/components/ui/sheet/sheet-close.svelte b/ui/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000..ae382c1 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/sheet/sheet-content.svelte b/ui/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000..856922e --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,58 @@ + + + + + + + + {@render children?.()} + + + Close + + + diff --git a/ui/src/lib/components/ui/sheet/sheet-description.svelte b/ui/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000..333b17a --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/sheet/sheet-footer.svelte b/ui/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000..dd9ed84 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sheet/sheet-header.svelte b/ui/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000..757a6a5 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sheet/sheet-overlay.svelte b/ui/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000..345e197 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/components/ui/sheet/sheet-title.svelte b/ui/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000..9fda327 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/sheet/sheet-trigger.svelte b/ui/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000..e266975 --- /dev/null +++ b/ui/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/sidebar/constants.ts b/ui/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..4de4435 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar:state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/ui/src/lib/components/ui/sidebar/context.svelte.ts b/ui/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..15248ad --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,81 @@ +import { IsMobile } from "$lib/hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current + ? (this.openMobile = !this.openMobile) + : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/ui/src/lib/components/ui/sidebar/index.ts b/ui/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..318a341 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar, +}; diff --git a/ui/src/lib/components/ui/sidebar/sidebar-content.svelte b/ui/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000..f121800 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-footer.svelte b/ui/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000..6259cb9 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/ui/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000..fb84e4a --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,36 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000..415255f --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000..e292945 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/ui/src/lib/components/ui/sidebar/sidebar-group.svelte b/ui/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000..ec18a69 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-header.svelte b/ui/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000..a1b2db1 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-input.svelte b/ui/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000..19b3666 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte b/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000..d862761 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000..fa3fb0c --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000..69e5a3c --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,29 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000..4bef683 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,103 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000..4db4453 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000..cc63b04 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000..987f104 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000..681d0f1 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000..8ab1111 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,25 @@ + + +
      + {@render children?.()} +
    diff --git a/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte b/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000..946ccce --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte b/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000..5b0d0aa --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,53 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte b/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000..c180cf5 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte b/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000..5a7deda --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000..1825182 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/ui/src/lib/components/ui/sidebar/sidebar.svelte b/ui/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000..3e9eba9 --- /dev/null +++ b/ui/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,104 @@ + + +{#if collapsible === "none"} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} + {...restProps} + > + + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/ui/src/lib/components/ui/skeleton/index.ts b/ui/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/ui/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/ui/src/lib/components/ui/skeleton/skeleton.svelte b/ui/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..c7e3d26 --- /dev/null +++ b/ui/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/ui/src/lib/components/ui/slider.svelte b/ui/src/lib/components/ui/slider.svelte new file mode 100644 index 0000000..23e1ac3 --- /dev/null +++ b/ui/src/lib/components/ui/slider.svelte @@ -0,0 +1,78 @@ + + +
    + {#if label} +
    {label}
    + {/if} +
    + +
    + {min} + {value} + {max} +
    +
    +
    + + diff --git a/ui/src/lib/components/ui/sonner/index.ts b/ui/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..1ad9f4a --- /dev/null +++ b/ui/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/ui/src/lib/components/ui/sonner/sonner.svelte b/ui/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..1f50e1e --- /dev/null +++ b/ui/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,13 @@ + + + diff --git a/ui/src/lib/components/ui/switch/index.ts b/ui/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000..f5533db --- /dev/null +++ b/ui/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch, +}; diff --git a/ui/src/lib/components/ui/switch/switch.svelte b/ui/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000..78477dd --- /dev/null +++ b/ui/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/ui/src/lib/components/ui/tabs/index.ts b/ui/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000..12d4327 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,16 @@ +import Root from "./tabs.svelte"; +import Content from "./tabs-content.svelte"; +import List from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger, +}; diff --git a/ui/src/lib/components/ui/tabs/tabs-content.svelte b/ui/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000..340d65c --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/tabs/tabs-list.svelte b/ui/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000..08932b6 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/components/ui/tabs/tabs-trigger.svelte b/ui/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000..dced992 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/lib/components/ui/tabs/tabs.svelte b/ui/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 0000000..ef6cada --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/ui/src/lib/components/ui/tooltip/index.ts b/ui/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..313a7f0 --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,21 @@ +import { Tooltip as TooltipPrimitive } from "bits-ui"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; + +const Root = TooltipPrimitive.Root; +const Provider = TooltipPrimitive.Provider; +const Portal = TooltipPrimitive.Portal; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/ui/src/lib/components/ui/tooltip/tooltip-content.svelte b/ui/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..e495efe --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,47 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/gen/control_communication.ts b/ui/src/lib/gen/control_communication.ts new file mode 100644 index 0000000..986f9e3 --- /dev/null +++ b/ui/src/lib/gen/control_communication.ts @@ -0,0 +1,290 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.7.7 +// protoc v6.32.1 +// source: control_communication.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "control_communication"; + +export interface RfidId { + value: number; +} + +export interface SyncResponse { + currentTime: number; + pendingChanges: boolean; +} + +export interface SyncRequest { + lastSync?: number | undefined; + accessLogs: { [key: number]: number }; +} + +export interface SyncRequest_AccessLogsEntry { + key: number; + value: number; +} + +function createBaseRfidId(): RfidId { + return { value: 0 }; +} + +export const RfidId: MessageFns = { + encode(message: RfidId, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.value !== 0) { + writer.uint32(8).uint32(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RfidId { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRfidId(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.value = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): RfidId { + return RfidId.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): RfidId { + const message = createBaseRfidId(); + message.value = object.value ?? 0; + return message; + }, +}; + +function createBaseSyncResponse(): SyncResponse { + return { currentTime: 0, pendingChanges: false }; +} + +export const SyncResponse: MessageFns = { + encode(message: SyncResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.currentTime !== 0) { + writer.uint32(8).int64(message.currentTime); + } + if (message.pendingChanges !== false) { + writer.uint32(16).bool(message.pendingChanges); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SyncResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSyncResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.currentTime = longToNumber(reader.int64()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.pendingChanges = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SyncResponse { + return SyncResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SyncResponse { + const message = createBaseSyncResponse(); + message.currentTime = object.currentTime ?? 0; + message.pendingChanges = object.pendingChanges ?? false; + return message; + }, +}; + +function createBaseSyncRequest(): SyncRequest { + return { lastSync: undefined, accessLogs: {} }; +} + +export const SyncRequest: MessageFns = { + encode(message: SyncRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.lastSync !== undefined) { + writer.uint32(8).int64(message.lastSync); + } + Object.entries(message.accessLogs).forEach(([key, value]) => { + SyncRequest_AccessLogsEntry.encode({ key: key as any, value }, writer.uint32(18).fork()).join(); + }); + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SyncRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSyncRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.lastSync = longToNumber(reader.int64()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + const entry2 = SyncRequest_AccessLogsEntry.decode(reader, reader.uint32()); + if (entry2.value !== undefined) { + message.accessLogs[entry2.key] = entry2.value; + } + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SyncRequest { + return SyncRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SyncRequest { + const message = createBaseSyncRequest(); + message.lastSync = object.lastSync ?? undefined; + message.accessLogs = Object.entries(object.accessLogs ?? {}).reduce<{ [key: number]: number }>( + (acc, [key, value]) => { + if (value !== undefined) { + acc[globalThis.Number(key)] = globalThis.Number(value); + } + return acc; + }, + {}, + ); + return message; + }, +}; + +function createBaseSyncRequest_AccessLogsEntry(): SyncRequest_AccessLogsEntry { + return { key: 0, value: 0 }; +} + +export const SyncRequest_AccessLogsEntry: MessageFns = { + encode(message: SyncRequest_AccessLogsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== 0) { + writer.uint32(8).uint64(message.key); + } + if (message.value !== 0) { + writer.uint32(16).uint32(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SyncRequest_AccessLogsEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSyncRequest_AccessLogsEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.key = longToNumber(reader.uint64()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.value = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SyncRequest_AccessLogsEntry { + return SyncRequest_AccessLogsEntry.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SyncRequest_AccessLogsEntry { + const message = createBaseSyncRequest_AccessLogsEntry(); + message.key = object.key ?? 0; + message.value = object.value ?? 0; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/ui/src/lib/gen/device.ts b/ui/src/lib/gen/device.ts new file mode 100644 index 0000000..4067c4c --- /dev/null +++ b/ui/src/lib/gen/device.ts @@ -0,0 +1,395 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.7.7 +// protoc v6.32.1 +// source: device.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "device"; + +/** WiFi connection state enumeration */ +export enum WifiConnectionState { + WIFI_DISCONNECTED = 0, + WIFI_CONNECTED = 1, + WIFI_CONNECTING = 2, + WIFI_FAILED = 3, + UNRECOGNIZED = -1, +} + +export enum DeviceLogLevel { + LOG_LEVEL_DEBUG = 0, + LOG_LEVEL_INFO = 1, + LOG_LEVEL_WARN = 2, + LOG_LEVEL_ERROR = 3, + UNRECOGNIZED = -1, +} + +/** Device information */ +export interface DeviceInfo { + deviceId: string; + firmwareVersion: string; + hardwareVersion: string; + uptimeSeconds: number; + /** STA mode info */ + staConnectionState: WifiConnectionState; + staIp: string; + staGateway: string; + /** Signal strength in dBm */ + staSignalStrength: number; + /** AP mode info */ + apConnectionState: WifiConnectionState; + apIp: string; + apClientCount: number; +} + +export interface DeviceLogEntry { + /** Unix timestamp in milliseconds */ + timestamp: number; + level: DeviceLogLevel; + message: string; +} + +export interface DeviceStatus { + info?: DeviceInfo | undefined; + logs: DeviceLogEntry[]; +} + +function createBaseDeviceInfo(): DeviceInfo { + return { + deviceId: "", + firmwareVersion: "", + hardwareVersion: "", + uptimeSeconds: 0, + staConnectionState: 0, + staIp: "", + staGateway: "", + staSignalStrength: 0, + apConnectionState: 0, + apIp: "", + apClientCount: 0, + }; +} + +export const DeviceInfo: MessageFns = { + encode(message: DeviceInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.deviceId !== "") { + writer.uint32(10).string(message.deviceId); + } + if (message.firmwareVersion !== "") { + writer.uint32(18).string(message.firmwareVersion); + } + if (message.hardwareVersion !== "") { + writer.uint32(26).string(message.hardwareVersion); + } + if (message.uptimeSeconds !== 0) { + writer.uint32(32).uint32(message.uptimeSeconds); + } + if (message.staConnectionState !== 0) { + writer.uint32(40).int32(message.staConnectionState); + } + if (message.staIp !== "") { + writer.uint32(50).string(message.staIp); + } + if (message.staGateway !== "") { + writer.uint32(58).string(message.staGateway); + } + if (message.staSignalStrength !== 0) { + writer.uint32(64).int32(message.staSignalStrength); + } + if (message.apConnectionState !== 0) { + writer.uint32(72).int32(message.apConnectionState); + } + if (message.apIp !== "") { + writer.uint32(82).string(message.apIp); + } + if (message.apClientCount !== 0) { + writer.uint32(88).uint32(message.apClientCount); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeviceInfo { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeviceInfo(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.deviceId = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.firmwareVersion = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.hardwareVersion = reader.string(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.uptimeSeconds = reader.uint32(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.staConnectionState = reader.int32() as any; + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.staIp = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.staGateway = reader.string(); + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.staSignalStrength = reader.int32(); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + + message.apConnectionState = reader.int32() as any; + continue; + } + case 10: { + if (tag !== 82) { + break; + } + + message.apIp = reader.string(); + continue; + } + case 11: { + if (tag !== 88) { + break; + } + + message.apClientCount = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): DeviceInfo { + return DeviceInfo.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): DeviceInfo { + const message = createBaseDeviceInfo(); + message.deviceId = object.deviceId ?? ""; + message.firmwareVersion = object.firmwareVersion ?? ""; + message.hardwareVersion = object.hardwareVersion ?? ""; + message.uptimeSeconds = object.uptimeSeconds ?? 0; + message.staConnectionState = object.staConnectionState ?? 0; + message.staIp = object.staIp ?? ""; + message.staGateway = object.staGateway ?? ""; + message.staSignalStrength = object.staSignalStrength ?? 0; + message.apConnectionState = object.apConnectionState ?? 0; + message.apIp = object.apIp ?? ""; + message.apClientCount = object.apClientCount ?? 0; + return message; + }, +}; + +function createBaseDeviceLogEntry(): DeviceLogEntry { + return { timestamp: 0, level: 0, message: "" }; +} + +export const DeviceLogEntry: MessageFns = { + encode(message: DeviceLogEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.timestamp !== 0) { + writer.uint32(8).int64(message.timestamp); + } + if (message.level !== 0) { + writer.uint32(16).int32(message.level); + } + if (message.message !== "") { + writer.uint32(26).string(message.message); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeviceLogEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeviceLogEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.timestamp = longToNumber(reader.int64()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.level = reader.int32() as any; + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.message = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): DeviceLogEntry { + return DeviceLogEntry.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): DeviceLogEntry { + const message = createBaseDeviceLogEntry(); + message.timestamp = object.timestamp ?? 0; + message.level = object.level ?? 0; + message.message = object.message ?? ""; + return message; + }, +}; + +function createBaseDeviceStatus(): DeviceStatus { + return { info: undefined, logs: [] }; +} + +export const DeviceStatus: MessageFns = { + encode(message: DeviceStatus, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.info !== undefined) { + DeviceInfo.encode(message.info, writer.uint32(10).fork()).join(); + } + for (const v of message.logs) { + DeviceLogEntry.encode(v!, writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeviceStatus { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeviceStatus(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.info = DeviceInfo.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.logs.push(DeviceLogEntry.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): DeviceStatus { + return DeviceStatus.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): DeviceStatus { + const message = createBaseDeviceStatus(); + message.info = (object.info !== undefined && object.info !== null) + ? DeviceInfo.fromPartial(object.info) + : undefined; + message.logs = object.logs?.map((e) => DeviceLogEntry.fromPartial(e)) || []; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/ui/src/lib/gen/device_communication.ts b/ui/src/lib/gen/device_communication.ts new file mode 100644 index 0000000..941a39a --- /dev/null +++ b/ui/src/lib/gen/device_communication.ts @@ -0,0 +1,323 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.7.7 +// protoc v6.32.1 +// source: control_communication.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire'; + +export const protobufPackage = 'control_communication'; + +export interface RfidId { + value: number; +} + +export interface DeviceCommunicationSyncResponse { + currentTime: number; + pendingChanges: boolean; +} + +export interface DeviceCommunicationSyncRequest { + lastSync?: number | undefined; + logs: { [key: string]: string }; +} + +export interface DeviceCommunicationSyncRequest_LogsEntry { + key: string; + value: string; +} + +function createBaseRfidId(): RfidId { + return { value: 0 }; +} + +export const RfidId: MessageFns = { + encode(message: RfidId, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.value !== 0) { + writer.uint32(8).uint32(message.value); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RfidId { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRfidId(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.value = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): RfidId { + return RfidId.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): RfidId { + const message = createBaseRfidId(); + message.value = object.value ?? 0; + return message; + } +}; + +function createBaseDeviceCommunicationSyncResponse(): DeviceCommunicationSyncResponse { + return { currentTime: 0, pendingChanges: false }; +} + +export const DeviceCommunicationSyncResponse: MessageFns = { + encode( + message: DeviceCommunicationSyncResponse, + writer: BinaryWriter = new BinaryWriter() + ): BinaryWriter { + if (message.currentTime !== 0) { + writer.uint32(8).int64(message.currentTime); + } + if (message.pendingChanges !== false) { + writer.uint32(16).bool(message.pendingChanges); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeviceCommunicationSyncResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeviceCommunicationSyncResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.currentTime = longToNumber(reader.int64()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.pendingChanges = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>( + base?: I + ): DeviceCommunicationSyncResponse { + return DeviceCommunicationSyncResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I + ): DeviceCommunicationSyncResponse { + const message = createBaseDeviceCommunicationSyncResponse(); + message.currentTime = object.currentTime ?? 0; + message.pendingChanges = object.pendingChanges ?? false; + return message; + } +}; + +function createBaseDeviceCommunicationSyncRequest(): DeviceCommunicationSyncRequest { + return { lastSync: undefined, logs: {} }; +} + +export const DeviceCommunicationSyncRequest: MessageFns = { + encode( + message: DeviceCommunicationSyncRequest, + writer: BinaryWriter = new BinaryWriter() + ): BinaryWriter { + if (message.lastSync !== undefined) { + writer.uint32(8).int64(message.lastSync); + } + Object.entries(message.logs).forEach(([key, value]) => { + DeviceCommunicationSyncRequest_LogsEntry.encode( + { key: key as any, value }, + writer.uint32(18).fork() + ).join(); + }); + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeviceCommunicationSyncRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeviceCommunicationSyncRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.lastSync = longToNumber(reader.int64()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + const entry2 = DeviceCommunicationSyncRequest_LogsEntry.decode(reader, reader.uint32()); + if (entry2.value !== undefined) { + message.logs[entry2.key] = entry2.value; + } + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>( + base?: I + ): DeviceCommunicationSyncRequest { + return DeviceCommunicationSyncRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I + ): DeviceCommunicationSyncRequest { + const message = createBaseDeviceCommunicationSyncRequest(); + message.lastSync = object.lastSync ?? undefined; + message.logs = Object.entries(object.logs ?? {}).reduce<{ [key: string]: string }>( + (acc, [key, value]) => { + if (value !== undefined) { + acc[key] = globalThis.String(value); + } + return acc; + }, + {} + ); + return message; + } +}; + +function createBaseDeviceCommunicationSyncRequest_LogsEntry(): DeviceCommunicationSyncRequest_LogsEntry { + return { key: '', value: '' }; +} + +export const DeviceCommunicationSyncRequest_LogsEntry: MessageFns = + { + encode( + message: DeviceCommunicationSyncRequest_LogsEntry, + writer: BinaryWriter = new BinaryWriter() + ): BinaryWriter { + if (message.key !== '') { + writer.uint32(10).string(message.key); + } + if (message.value !== '') { + writer.uint32(18).string(message.value); + } + return writer; + }, + + decode( + input: BinaryReader | Uint8Array, + length?: number + ): DeviceCommunicationSyncRequest_LogsEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeviceCommunicationSyncRequest_LogsEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>( + base?: I + ): DeviceCommunicationSyncRequest_LogsEntry { + return DeviceCommunicationSyncRequest_LogsEntry.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I + ): DeviceCommunicationSyncRequest_LogsEntry { + const message = createBaseDeviceCommunicationSyncRequest_LogsEntry(); + message.key = object.key ?? ''; + message.value = object.value ?? ''; + return message; + } + }; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error('Value is larger than Number.MAX_SAFE_INTEGER'); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error('Value is smaller than Number.MIN_SAFE_INTEGER'); + } + return num; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/ui/src/lib/gen/hardware.ts b/ui/src/lib/gen/hardware.ts new file mode 100644 index 0000000..c5db5a5 --- /dev/null +++ b/ui/src/lib/gen/hardware.ts @@ -0,0 +1,1126 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.7.7 +// protoc v6.32.1 +// source: hardware.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "hardware"; + +/** LED animation types */ +export enum LedAnimation { + LED_ANIMATION_STATIC = 0, + LED_ANIMATION_PULSE = 1, + LED_ANIMATION_FADE = 2, + LED_ANIMATION_FLICKER = 3, + UNRECOGNIZED = -1, +} + +/** Hardware configuration */ +export interface HardwareConfig { + holdDurationMs: number; + override: boolean; + relayPin: number; + sensorRxPin: number; + sensorTxPin: number; + onOpenLed?: LedConfig | undefined; + defaultLed?: LedConfig | undefined; + onInvalidLed?: LedConfig | undefined; + enableSerialSensor: boolean; + /** + * repeated char sensor_api_key = 10; + * bool enable_ws_sensor = 11; + */ + onOverrideOpenLed?: LedConfig | undefined; + onInterceptionLed?: LedConfig | undefined; +} + +/** LED configuration */ +export interface LedConfig { + /** General properties that apply to all animations */ + brightness: number; + /** 0 for indefinite */ + durationMs: number; + staticParams?: StaticParams | undefined; + pulseParams?: PulseParams | undefined; + fadeParams?: FadeParams | undefined; + flickerParams?: FlickerParams | undefined; +} + +/** Define the specific parameters for each animation type */ +export interface StaticParams { + color: number; +} + +export interface PulseParams { + color: number; + speedMs: number; +} + +export interface FadeParams { + /** Fade between these colors */ + colors: number[]; + speedMs: number; +} + +export interface FlickerParams { + color: number; + /** e.g., 0-100 */ + intensity: number; +} + +export interface RfidReading { + cardId: number; +} + +export interface SensorToControlMessage { + sensorId: number; + rfidReading?: + | RfidReading + | undefined; + /** Add other sensor message types as needed */ + otaResponse?: SensorOTAEnableResponse | undefined; +} + +export interface SensorOTAEnable { + ssid: string; + password: string; + timeoutSeconds: number; + asStationMode: boolean; + useStaticIp: boolean; + staticIp: string; + netmask: string; + gateway: string; +} + +export interface SensorRestart { +} + +export interface SensorOTAEnableResponse { + success: boolean; + ipAddress: string; + errorMessage: string; +} + +export interface ControlToSensorMessage { + controlId: number; + ledConfig?: LedConfig | undefined; + otaEnable?: + | SensorOTAEnable + | undefined; + /** Add other control message types as needed */ + restart?: SensorRestart | undefined; +} + +function createBaseHardwareConfig(): HardwareConfig { + return { + holdDurationMs: 0, + override: false, + relayPin: 0, + sensorRxPin: 0, + sensorTxPin: 0, + onOpenLed: undefined, + defaultLed: undefined, + onInvalidLed: undefined, + enableSerialSensor: false, + onOverrideOpenLed: undefined, + onInterceptionLed: undefined, + }; +} + +export const HardwareConfig: MessageFns = { + encode(message: HardwareConfig, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.holdDurationMs !== 0) { + writer.uint32(8).uint32(message.holdDurationMs); + } + if (message.override !== false) { + writer.uint32(16).bool(message.override); + } + if (message.relayPin !== 0) { + writer.uint32(24).uint32(message.relayPin); + } + if (message.sensorRxPin !== 0) { + writer.uint32(32).uint32(message.sensorRxPin); + } + if (message.sensorTxPin !== 0) { + writer.uint32(40).uint32(message.sensorTxPin); + } + if (message.onOpenLed !== undefined) { + LedConfig.encode(message.onOpenLed, writer.uint32(50).fork()).join(); + } + if (message.defaultLed !== undefined) { + LedConfig.encode(message.defaultLed, writer.uint32(58).fork()).join(); + } + if (message.onInvalidLed !== undefined) { + LedConfig.encode(message.onInvalidLed, writer.uint32(66).fork()).join(); + } + if (message.enableSerialSensor !== false) { + writer.uint32(72).bool(message.enableSerialSensor); + } + if (message.onOverrideOpenLed !== undefined) { + LedConfig.encode(message.onOverrideOpenLed, writer.uint32(98).fork()).join(); + } + if (message.onInterceptionLed !== undefined) { + LedConfig.encode(message.onInterceptionLed, writer.uint32(106).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): HardwareConfig { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseHardwareConfig(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.holdDurationMs = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.override = reader.bool(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.relayPin = reader.uint32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.sensorRxPin = reader.uint32(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.sensorTxPin = reader.uint32(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.onOpenLed = LedConfig.decode(reader, reader.uint32()); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.defaultLed = LedConfig.decode(reader, reader.uint32()); + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.onInvalidLed = LedConfig.decode(reader, reader.uint32()); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + + message.enableSerialSensor = reader.bool(); + continue; + } + case 12: { + if (tag !== 98) { + break; + } + + message.onOverrideOpenLed = LedConfig.decode(reader, reader.uint32()); + continue; + } + case 13: { + if (tag !== 106) { + break; + } + + message.onInterceptionLed = LedConfig.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): HardwareConfig { + return HardwareConfig.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): HardwareConfig { + const message = createBaseHardwareConfig(); + message.holdDurationMs = object.holdDurationMs ?? 0; + message.override = object.override ?? false; + message.relayPin = object.relayPin ?? 0; + message.sensorRxPin = object.sensorRxPin ?? 0; + message.sensorTxPin = object.sensorTxPin ?? 0; + message.onOpenLed = (object.onOpenLed !== undefined && object.onOpenLed !== null) + ? LedConfig.fromPartial(object.onOpenLed) + : undefined; + message.defaultLed = (object.defaultLed !== undefined && object.defaultLed !== null) + ? LedConfig.fromPartial(object.defaultLed) + : undefined; + message.onInvalidLed = (object.onInvalidLed !== undefined && object.onInvalidLed !== null) + ? LedConfig.fromPartial(object.onInvalidLed) + : undefined; + message.enableSerialSensor = object.enableSerialSensor ?? false; + message.onOverrideOpenLed = (object.onOverrideOpenLed !== undefined && object.onOverrideOpenLed !== null) + ? LedConfig.fromPartial(object.onOverrideOpenLed) + : undefined; + message.onInterceptionLed = (object.onInterceptionLed !== undefined && object.onInterceptionLed !== null) + ? LedConfig.fromPartial(object.onInterceptionLed) + : undefined; + return message; + }, +}; + +function createBaseLedConfig(): LedConfig { + return { + brightness: 0, + durationMs: 0, + staticParams: undefined, + pulseParams: undefined, + fadeParams: undefined, + flickerParams: undefined, + }; +} + +export const LedConfig: MessageFns = { + encode(message: LedConfig, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.brightness !== 0) { + writer.uint32(8).uint32(message.brightness); + } + if (message.durationMs !== 0) { + writer.uint32(16).uint32(message.durationMs); + } + if (message.staticParams !== undefined) { + StaticParams.encode(message.staticParams, writer.uint32(26).fork()).join(); + } + if (message.pulseParams !== undefined) { + PulseParams.encode(message.pulseParams, writer.uint32(34).fork()).join(); + } + if (message.fadeParams !== undefined) { + FadeParams.encode(message.fadeParams, writer.uint32(42).fork()).join(); + } + if (message.flickerParams !== undefined) { + FlickerParams.encode(message.flickerParams, writer.uint32(50).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): LedConfig { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLedConfig(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.brightness = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.durationMs = reader.uint32(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.staticParams = StaticParams.decode(reader, reader.uint32()); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.pulseParams = PulseParams.decode(reader, reader.uint32()); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.fadeParams = FadeParams.decode(reader, reader.uint32()); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.flickerParams = FlickerParams.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): LedConfig { + return LedConfig.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): LedConfig { + const message = createBaseLedConfig(); + message.brightness = object.brightness ?? 0; + message.durationMs = object.durationMs ?? 0; + message.staticParams = (object.staticParams !== undefined && object.staticParams !== null) + ? StaticParams.fromPartial(object.staticParams) + : undefined; + message.pulseParams = (object.pulseParams !== undefined && object.pulseParams !== null) + ? PulseParams.fromPartial(object.pulseParams) + : undefined; + message.fadeParams = (object.fadeParams !== undefined && object.fadeParams !== null) + ? FadeParams.fromPartial(object.fadeParams) + : undefined; + message.flickerParams = (object.flickerParams !== undefined && object.flickerParams !== null) + ? FlickerParams.fromPartial(object.flickerParams) + : undefined; + return message; + }, +}; + +function createBaseStaticParams(): StaticParams { + return { color: 0 }; +} + +export const StaticParams: MessageFns = { + encode(message: StaticParams, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.color !== 0) { + writer.uint32(8).uint32(message.color); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): StaticParams { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseStaticParams(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.color = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): StaticParams { + return StaticParams.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): StaticParams { + const message = createBaseStaticParams(); + message.color = object.color ?? 0; + return message; + }, +}; + +function createBasePulseParams(): PulseParams { + return { color: 0, speedMs: 0 }; +} + +export const PulseParams: MessageFns = { + encode(message: PulseParams, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.color !== 0) { + writer.uint32(8).uint32(message.color); + } + if (message.speedMs !== 0) { + writer.uint32(16).uint32(message.speedMs); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): PulseParams { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePulseParams(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.color = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.speedMs = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): PulseParams { + return PulseParams.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): PulseParams { + const message = createBasePulseParams(); + message.color = object.color ?? 0; + message.speedMs = object.speedMs ?? 0; + return message; + }, +}; + +function createBaseFadeParams(): FadeParams { + return { colors: [], speedMs: 0 }; +} + +export const FadeParams: MessageFns = { + encode(message: FadeParams, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + writer.uint32(10).fork(); + for (const v of message.colors) { + writer.uint32(v); + } + writer.join(); + if (message.speedMs !== 0) { + writer.uint32(16).uint32(message.speedMs); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FadeParams { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFadeParams(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag === 8) { + message.colors.push(reader.uint32()); + + continue; + } + + if (tag === 10) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.colors.push(reader.uint32()); + } + + continue; + } + + break; + } + case 2: { + if (tag !== 16) { + break; + } + + message.speedMs = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): FadeParams { + return FadeParams.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): FadeParams { + const message = createBaseFadeParams(); + message.colors = object.colors?.map((e) => e) || []; + message.speedMs = object.speedMs ?? 0; + return message; + }, +}; + +function createBaseFlickerParams(): FlickerParams { + return { color: 0, intensity: 0 }; +} + +export const FlickerParams: MessageFns = { + encode(message: FlickerParams, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.color !== 0) { + writer.uint32(8).uint32(message.color); + } + if (message.intensity !== 0) { + writer.uint32(16).uint32(message.intensity); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FlickerParams { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFlickerParams(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.color = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.intensity = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): FlickerParams { + return FlickerParams.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): FlickerParams { + const message = createBaseFlickerParams(); + message.color = object.color ?? 0; + message.intensity = object.intensity ?? 0; + return message; + }, +}; + +function createBaseRfidReading(): RfidReading { + return { cardId: 0 }; +} + +export const RfidReading: MessageFns = { + encode(message: RfidReading, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.cardId !== 0) { + writer.uint32(8).uint32(message.cardId); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RfidReading { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRfidReading(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.cardId = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): RfidReading { + return RfidReading.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): RfidReading { + const message = createBaseRfidReading(); + message.cardId = object.cardId ?? 0; + return message; + }, +}; + +function createBaseSensorToControlMessage(): SensorToControlMessage { + return { sensorId: 0, rfidReading: undefined, otaResponse: undefined }; +} + +export const SensorToControlMessage: MessageFns = { + encode(message: SensorToControlMessage, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.sensorId !== 0) { + writer.uint32(8).uint32(message.sensorId); + } + if (message.rfidReading !== undefined) { + RfidReading.encode(message.rfidReading, writer.uint32(18).fork()).join(); + } + if (message.otaResponse !== undefined) { + SensorOTAEnableResponse.encode(message.otaResponse, writer.uint32(26).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SensorToControlMessage { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSensorToControlMessage(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.sensorId = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.rfidReading = RfidReading.decode(reader, reader.uint32()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.otaResponse = SensorOTAEnableResponse.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SensorToControlMessage { + return SensorToControlMessage.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SensorToControlMessage { + const message = createBaseSensorToControlMessage(); + message.sensorId = object.sensorId ?? 0; + message.rfidReading = (object.rfidReading !== undefined && object.rfidReading !== null) + ? RfidReading.fromPartial(object.rfidReading) + : undefined; + message.otaResponse = (object.otaResponse !== undefined && object.otaResponse !== null) + ? SensorOTAEnableResponse.fromPartial(object.otaResponse) + : undefined; + return message; + }, +}; + +function createBaseSensorOTAEnable(): SensorOTAEnable { + return { + ssid: "", + password: "", + timeoutSeconds: 0, + asStationMode: false, + useStaticIp: false, + staticIp: "", + netmask: "", + gateway: "", + }; +} + +export const SensorOTAEnable: MessageFns = { + encode(message: SensorOTAEnable, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.ssid !== "") { + writer.uint32(10).string(message.ssid); + } + if (message.password !== "") { + writer.uint32(18).string(message.password); + } + if (message.timeoutSeconds !== 0) { + writer.uint32(24).uint32(message.timeoutSeconds); + } + if (message.asStationMode !== false) { + writer.uint32(32).bool(message.asStationMode); + } + if (message.useStaticIp !== false) { + writer.uint32(40).bool(message.useStaticIp); + } + if (message.staticIp !== "") { + writer.uint32(50).string(message.staticIp); + } + if (message.netmask !== "") { + writer.uint32(58).string(message.netmask); + } + if (message.gateway !== "") { + writer.uint32(66).string(message.gateway); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SensorOTAEnable { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSensorOTAEnable(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.ssid = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.password = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.timeoutSeconds = reader.uint32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.asStationMode = reader.bool(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.useStaticIp = reader.bool(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.staticIp = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.netmask = reader.string(); + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.gateway = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SensorOTAEnable { + return SensorOTAEnable.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SensorOTAEnable { + const message = createBaseSensorOTAEnable(); + message.ssid = object.ssid ?? ""; + message.password = object.password ?? ""; + message.timeoutSeconds = object.timeoutSeconds ?? 0; + message.asStationMode = object.asStationMode ?? false; + message.useStaticIp = object.useStaticIp ?? false; + message.staticIp = object.staticIp ?? ""; + message.netmask = object.netmask ?? ""; + message.gateway = object.gateway ?? ""; + return message; + }, +}; + +function createBaseSensorRestart(): SensorRestart { + return {}; +} + +export const SensorRestart: MessageFns = { + encode(_: SensorRestart, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SensorRestart { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSensorRestart(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SensorRestart { + return SensorRestart.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(_: I): SensorRestart { + const message = createBaseSensorRestart(); + return message; + }, +}; + +function createBaseSensorOTAEnableResponse(): SensorOTAEnableResponse { + return { success: false, ipAddress: "", errorMessage: "" }; +} + +export const SensorOTAEnableResponse: MessageFns = { + encode(message: SensorOTAEnableResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.ipAddress !== "") { + writer.uint32(18).string(message.ipAddress); + } + if (message.errorMessage !== "") { + writer.uint32(26).string(message.errorMessage); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SensorOTAEnableResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSensorOTAEnableResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.ipAddress = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.errorMessage = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SensorOTAEnableResponse { + return SensorOTAEnableResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SensorOTAEnableResponse { + const message = createBaseSensorOTAEnableResponse(); + message.success = object.success ?? false; + message.ipAddress = object.ipAddress ?? ""; + message.errorMessage = object.errorMessage ?? ""; + return message; + }, +}; + +function createBaseControlToSensorMessage(): ControlToSensorMessage { + return { controlId: 0, ledConfig: undefined, otaEnable: undefined, restart: undefined }; +} + +export const ControlToSensorMessage: MessageFns = { + encode(message: ControlToSensorMessage, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.controlId !== 0) { + writer.uint32(8).uint32(message.controlId); + } + if (message.ledConfig !== undefined) { + LedConfig.encode(message.ledConfig, writer.uint32(18).fork()).join(); + } + if (message.otaEnable !== undefined) { + SensorOTAEnable.encode(message.otaEnable, writer.uint32(26).fork()).join(); + } + if (message.restart !== undefined) { + SensorRestart.encode(message.restart, writer.uint32(34).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ControlToSensorMessage { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseControlToSensorMessage(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.controlId = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.ledConfig = LedConfig.decode(reader, reader.uint32()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.otaEnable = SensorOTAEnable.decode(reader, reader.uint32()); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.restart = SensorRestart.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): ControlToSensorMessage { + return ControlToSensorMessage.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): ControlToSensorMessage { + const message = createBaseControlToSensorMessage(); + message.controlId = object.controlId ?? 0; + message.ledConfig = (object.ledConfig !== undefined && object.ledConfig !== null) + ? LedConfig.fromPartial(object.ledConfig) + : undefined; + message.otaEnable = (object.otaEnable !== undefined && object.otaEnable !== null) + ? SensorOTAEnable.fromPartial(object.otaEnable) + : undefined; + message.restart = (object.restart !== undefined && object.restart !== null) + ? SensorRestart.fromPartial(object.restart) + : undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/ui/src/lib/gen/settings.ts b/ui/src/lib/gen/settings.ts new file mode 100644 index 0000000..411858a --- /dev/null +++ b/ui/src/lib/gen/settings.ts @@ -0,0 +1,503 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.7.7 +// protoc v6.32.1 +// source: settings.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "settings"; + +/** WiFi mode enumeration */ +export enum WifiMode { + WIFI_MODE_UNSPECIFIED = 0, + WIFI_MODE_STATION = 1, + WIFI_MODE_AP = 2, + WIFI_MODE_AP_STATION = 3, + UNRECOGNIZED = -1, +} + +/** Log level enumeration */ +export enum LogLevel { + LOG_LEVEL_DEBUG = 0, + LOG_LEVEL_INFO = 1, + LOG_LEVEL_WARN = 2, + LOG_LEVEL_ERROR = 3, + UNRECOGNIZED = -1, +} + +/** Settings data structure */ +export interface SettingsData { + syncServerUrl: string; + deviceApiKey: string; + syncIntervalSeconds: number; + autoSync: boolean; + wifiMode: WifiMode; + stationSsid: string; + stationPassword: string; + apSsid: string; + apPassword: string; + apChannel: number; + enableFallbackAp: boolean; + /** Unix timestamp in milliseconds */ + updatedAt: number; + /** Settings version for change tracking */ + version: number; + logLevel: LogLevel; +} + +/** Request to get current settings */ +export interface GetSettingsRequest { +} + +/** Response with current settings */ +export interface GetSettingsResponse { + settings?: SettingsData | undefined; +} + +/** Request to update settings */ +export interface UpdateSettingsRequest { + settings?: SettingsData | undefined; +} + +/** Response to settings update */ +export interface UpdateSettingsResponse { + success: boolean; + error?: string | undefined; +} + +function createBaseSettingsData(): SettingsData { + return { + syncServerUrl: "", + deviceApiKey: "", + syncIntervalSeconds: 0, + autoSync: false, + wifiMode: 0, + stationSsid: "", + stationPassword: "", + apSsid: "", + apPassword: "", + apChannel: 0, + enableFallbackAp: false, + updatedAt: 0, + version: 0, + logLevel: 0, + }; +} + +export const SettingsData: MessageFns = { + encode(message: SettingsData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.syncServerUrl !== "") { + writer.uint32(10).string(message.syncServerUrl); + } + if (message.deviceApiKey !== "") { + writer.uint32(18).string(message.deviceApiKey); + } + if (message.syncIntervalSeconds !== 0) { + writer.uint32(24).uint32(message.syncIntervalSeconds); + } + if (message.autoSync !== false) { + writer.uint32(32).bool(message.autoSync); + } + if (message.wifiMode !== 0) { + writer.uint32(40).int32(message.wifiMode); + } + if (message.stationSsid !== "") { + writer.uint32(50).string(message.stationSsid); + } + if (message.stationPassword !== "") { + writer.uint32(58).string(message.stationPassword); + } + if (message.apSsid !== "") { + writer.uint32(66).string(message.apSsid); + } + if (message.apPassword !== "") { + writer.uint32(74).string(message.apPassword); + } + if (message.apChannel !== 0) { + writer.uint32(80).uint32(message.apChannel); + } + if (message.enableFallbackAp !== false) { + writer.uint32(88).bool(message.enableFallbackAp); + } + if (message.updatedAt !== 0) { + writer.uint32(96).int64(message.updatedAt); + } + if (message.version !== 0) { + writer.uint32(104).uint32(message.version); + } + if (message.logLevel !== 0) { + writer.uint32(112).int32(message.logLevel); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SettingsData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSettingsData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.syncServerUrl = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.deviceApiKey = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.syncIntervalSeconds = reader.uint32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.autoSync = reader.bool(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.wifiMode = reader.int32() as any; + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.stationSsid = reader.string(); + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.stationPassword = reader.string(); + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.apSsid = reader.string(); + continue; + } + case 9: { + if (tag !== 74) { + break; + } + + message.apPassword = reader.string(); + continue; + } + case 10: { + if (tag !== 80) { + break; + } + + message.apChannel = reader.uint32(); + continue; + } + case 11: { + if (tag !== 88) { + break; + } + + message.enableFallbackAp = reader.bool(); + continue; + } + case 12: { + if (tag !== 96) { + break; + } + + message.updatedAt = longToNumber(reader.int64()); + continue; + } + case 13: { + if (tag !== 104) { + break; + } + + message.version = reader.uint32(); + continue; + } + case 14: { + if (tag !== 112) { + break; + } + + message.logLevel = reader.int32() as any; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): SettingsData { + return SettingsData.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SettingsData { + const message = createBaseSettingsData(); + message.syncServerUrl = object.syncServerUrl ?? ""; + message.deviceApiKey = object.deviceApiKey ?? ""; + message.syncIntervalSeconds = object.syncIntervalSeconds ?? 0; + message.autoSync = object.autoSync ?? false; + message.wifiMode = object.wifiMode ?? 0; + message.stationSsid = object.stationSsid ?? ""; + message.stationPassword = object.stationPassword ?? ""; + message.apSsid = object.apSsid ?? ""; + message.apPassword = object.apPassword ?? ""; + message.apChannel = object.apChannel ?? 0; + message.enableFallbackAp = object.enableFallbackAp ?? false; + message.updatedAt = object.updatedAt ?? 0; + message.version = object.version ?? 0; + message.logLevel = object.logLevel ?? 0; + return message; + }, +}; + +function createBaseGetSettingsRequest(): GetSettingsRequest { + return {}; +} + +export const GetSettingsRequest: MessageFns = { + encode(_: GetSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetSettingsRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetSettingsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): GetSettingsRequest { + return GetSettingsRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(_: I): GetSettingsRequest { + const message = createBaseGetSettingsRequest(); + return message; + }, +}; + +function createBaseGetSettingsResponse(): GetSettingsResponse { + return { settings: undefined }; +} + +export const GetSettingsResponse: MessageFns = { + encode(message: GetSettingsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.settings !== undefined) { + SettingsData.encode(message.settings, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetSettingsResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetSettingsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.settings = SettingsData.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): GetSettingsResponse { + return GetSettingsResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): GetSettingsResponse { + const message = createBaseGetSettingsResponse(); + message.settings = (object.settings !== undefined && object.settings !== null) + ? SettingsData.fromPartial(object.settings) + : undefined; + return message; + }, +}; + +function createBaseUpdateSettingsRequest(): UpdateSettingsRequest { + return { settings: undefined }; +} + +export const UpdateSettingsRequest: MessageFns = { + encode(message: UpdateSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.settings !== undefined) { + SettingsData.encode(message.settings, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): UpdateSettingsRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUpdateSettingsRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.settings = SettingsData.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): UpdateSettingsRequest { + return UpdateSettingsRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): UpdateSettingsRequest { + const message = createBaseUpdateSettingsRequest(); + message.settings = (object.settings !== undefined && object.settings !== null) + ? SettingsData.fromPartial(object.settings) + : undefined; + return message; + }, +}; + +function createBaseUpdateSettingsResponse(): UpdateSettingsResponse { + return { success: false, error: undefined }; +} + +export const UpdateSettingsResponse: MessageFns = { + encode(message: UpdateSettingsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.error !== undefined) { + writer.uint32(18).string(message.error); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): UpdateSettingsResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUpdateSettingsResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.error = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): UpdateSettingsResponse { + return UpdateSettingsResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): UpdateSettingsResponse { + const message = createBaseUpdateSettingsResponse(); + message.success = object.success ?? false; + message.error = object.error ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/ui/src/lib/gen/webui.ts b/ui/src/lib/gen/webui.ts new file mode 100644 index 0000000..163d3aa --- /dev/null +++ b/ui/src/lib/gen/webui.ts @@ -0,0 +1,885 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.7.7 +// protoc v6.32.1 +// source: webui.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "webui"; + +/** Login request from Web UI */ +export interface WebUiLoginRequest { + /** 1-32 chars */ + username: string; + /** 1-64 chars (plain or hashed) */ + password: string; +} + +/** Login response */ +export interface WebUiLoginResponse { + success: boolean; + /** session/JWT when success */ + token?: + | string + | undefined; + /** error message when failed */ + error?: string | undefined; +} + +/** Logout response */ +export interface WebUiLogoutResponse { + success: boolean; + error: string; +} + +/** User data structure */ +export interface WebUiUser { + /** 1-32 chars */ + username: string; + /** 32 bytes (SHA-256) */ + passwordHash: Uint8Array; +} + +/** User list (max 5 users) */ +export interface WebUiUserList { + users: WebUiUser[]; +} + +/** Request to get users */ +export interface GetUsersRequest { +} + +/** Response with users */ +export interface GetUsersResponse { + /** List of usernames */ + usernames: string[]; +} + +/** Request to add user */ +export interface AddUserRequest { + username: string; + password: string; +} + +/** Response to add user */ +export interface AddUserResponse { + success: boolean; + /** e.g., "table full", "user exists" */ + error?: string | undefined; +} + +/** Request to delete user */ +export interface DeleteUserRequest { + username: string; +} + +/** Response to delete user */ +export interface DeleteUserResponse { + success: boolean; + /** e.g., "user not found" */ + error?: string | undefined; +} + +/** Request to update user password */ +export interface UpdateUserPasswordRequest { + username: string; + newPassword: string; +} + +/** Response to update user password */ +export interface UpdateUserPasswordResponse { + success: boolean; + /** e.g., "user not found" */ + error?: string | undefined; +} + +/** Auth check response */ +export interface WebUiAuthCheckResponse { + authenticated: boolean; + username?: string | undefined; +} + +function createBaseWebUiLoginRequest(): WebUiLoginRequest { + return { username: "", password: "" }; +} + +export const WebUiLoginRequest: MessageFns = { + encode(message: WebUiLoginRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.username !== "") { + writer.uint32(10).string(message.username); + } + if (message.password !== "") { + writer.uint32(18).string(message.password); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WebUiLoginRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWebUiLoginRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.username = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.password = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): WebUiLoginRequest { + return WebUiLoginRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): WebUiLoginRequest { + const message = createBaseWebUiLoginRequest(); + message.username = object.username ?? ""; + message.password = object.password ?? ""; + return message; + }, +}; + +function createBaseWebUiLoginResponse(): WebUiLoginResponse { + return { success: false, token: undefined, error: undefined }; +} + +export const WebUiLoginResponse: MessageFns = { + encode(message: WebUiLoginResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.token !== undefined) { + writer.uint32(18).string(message.token); + } + if (message.error !== undefined) { + writer.uint32(26).string(message.error); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WebUiLoginResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWebUiLoginResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.token = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.error = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): WebUiLoginResponse { + return WebUiLoginResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): WebUiLoginResponse { + const message = createBaseWebUiLoginResponse(); + message.success = object.success ?? false; + message.token = object.token ?? undefined; + message.error = object.error ?? undefined; + return message; + }, +}; + +function createBaseWebUiLogoutResponse(): WebUiLogoutResponse { + return { success: false, error: "" }; +} + +export const WebUiLogoutResponse: MessageFns = { + encode(message: WebUiLogoutResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.error !== "") { + writer.uint32(18).string(message.error); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WebUiLogoutResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWebUiLogoutResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.error = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): WebUiLogoutResponse { + return WebUiLogoutResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): WebUiLogoutResponse { + const message = createBaseWebUiLogoutResponse(); + message.success = object.success ?? false; + message.error = object.error ?? ""; + return message; + }, +}; + +function createBaseWebUiUser(): WebUiUser { + return { username: "", passwordHash: new Uint8Array(0) }; +} + +export const WebUiUser: MessageFns = { + encode(message: WebUiUser, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.username !== "") { + writer.uint32(10).string(message.username); + } + if (message.passwordHash.length !== 0) { + writer.uint32(18).bytes(message.passwordHash); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WebUiUser { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWebUiUser(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.username = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.passwordHash = reader.bytes(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): WebUiUser { + return WebUiUser.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): WebUiUser { + const message = createBaseWebUiUser(); + message.username = object.username ?? ""; + message.passwordHash = object.passwordHash ?? new Uint8Array(0); + return message; + }, +}; + +function createBaseWebUiUserList(): WebUiUserList { + return { users: [] }; +} + +export const WebUiUserList: MessageFns = { + encode(message: WebUiUserList, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.users) { + WebUiUser.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WebUiUserList { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWebUiUserList(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.users.push(WebUiUser.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): WebUiUserList { + return WebUiUserList.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): WebUiUserList { + const message = createBaseWebUiUserList(); + message.users = object.users?.map((e) => WebUiUser.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseGetUsersRequest(): GetUsersRequest { + return {}; +} + +export const GetUsersRequest: MessageFns = { + encode(_: GetUsersRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetUsersRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetUsersRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): GetUsersRequest { + return GetUsersRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(_: I): GetUsersRequest { + const message = createBaseGetUsersRequest(); + return message; + }, +}; + +function createBaseGetUsersResponse(): GetUsersResponse { + return { usernames: [] }; +} + +export const GetUsersResponse: MessageFns = { + encode(message: GetUsersResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.usernames) { + writer.uint32(10).string(v!); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetUsersResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetUsersResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.usernames.push(reader.string()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): GetUsersResponse { + return GetUsersResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): GetUsersResponse { + const message = createBaseGetUsersResponse(); + message.usernames = object.usernames?.map((e) => e) || []; + return message; + }, +}; + +function createBaseAddUserRequest(): AddUserRequest { + return { username: "", password: "" }; +} + +export const AddUserRequest: MessageFns = { + encode(message: AddUserRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.username !== "") { + writer.uint32(10).string(message.username); + } + if (message.password !== "") { + writer.uint32(18).string(message.password); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): AddUserRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAddUserRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.username = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.password = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): AddUserRequest { + return AddUserRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): AddUserRequest { + const message = createBaseAddUserRequest(); + message.username = object.username ?? ""; + message.password = object.password ?? ""; + return message; + }, +}; + +function createBaseAddUserResponse(): AddUserResponse { + return { success: false, error: undefined }; +} + +export const AddUserResponse: MessageFns = { + encode(message: AddUserResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.error !== undefined) { + writer.uint32(18).string(message.error); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): AddUserResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAddUserResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.error = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): AddUserResponse { + return AddUserResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): AddUserResponse { + const message = createBaseAddUserResponse(); + message.success = object.success ?? false; + message.error = object.error ?? undefined; + return message; + }, +}; + +function createBaseDeleteUserRequest(): DeleteUserRequest { + return { username: "" }; +} + +export const DeleteUserRequest: MessageFns = { + encode(message: DeleteUserRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.username !== "") { + writer.uint32(10).string(message.username); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeleteUserRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeleteUserRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.username = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): DeleteUserRequest { + return DeleteUserRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): DeleteUserRequest { + const message = createBaseDeleteUserRequest(); + message.username = object.username ?? ""; + return message; + }, +}; + +function createBaseDeleteUserResponse(): DeleteUserResponse { + return { success: false, error: undefined }; +} + +export const DeleteUserResponse: MessageFns = { + encode(message: DeleteUserResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.error !== undefined) { + writer.uint32(18).string(message.error); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): DeleteUserResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDeleteUserResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.error = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): DeleteUserResponse { + return DeleteUserResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): DeleteUserResponse { + const message = createBaseDeleteUserResponse(); + message.success = object.success ?? false; + message.error = object.error ?? undefined; + return message; + }, +}; + +function createBaseUpdateUserPasswordRequest(): UpdateUserPasswordRequest { + return { username: "", newPassword: "" }; +} + +export const UpdateUserPasswordRequest: MessageFns = { + encode(message: UpdateUserPasswordRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.username !== "") { + writer.uint32(10).string(message.username); + } + if (message.newPassword !== "") { + writer.uint32(18).string(message.newPassword); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): UpdateUserPasswordRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUpdateUserPasswordRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.username = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.newPassword = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): UpdateUserPasswordRequest { + return UpdateUserPasswordRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): UpdateUserPasswordRequest { + const message = createBaseUpdateUserPasswordRequest(); + message.username = object.username ?? ""; + message.newPassword = object.newPassword ?? ""; + return message; + }, +}; + +function createBaseUpdateUserPasswordResponse(): UpdateUserPasswordResponse { + return { success: false, error: undefined }; +} + +export const UpdateUserPasswordResponse: MessageFns = { + encode(message: UpdateUserPasswordResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.success !== false) { + writer.uint32(8).bool(message.success); + } + if (message.error !== undefined) { + writer.uint32(18).string(message.error); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): UpdateUserPasswordResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUpdateUserPasswordResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.success = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.error = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): UpdateUserPasswordResponse { + return UpdateUserPasswordResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): UpdateUserPasswordResponse { + const message = createBaseUpdateUserPasswordResponse(); + message.success = object.success ?? false; + message.error = object.error ?? undefined; + return message; + }, +}; + +function createBaseWebUiAuthCheckResponse(): WebUiAuthCheckResponse { + return { authenticated: false, username: undefined }; +} + +export const WebUiAuthCheckResponse: MessageFns = { + encode(message: WebUiAuthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.authenticated !== false) { + writer.uint32(8).bool(message.authenticated); + } + if (message.username !== undefined) { + writer.uint32(18).string(message.username); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WebUiAuthCheckResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWebUiAuthCheckResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.authenticated = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.username = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + create, I>>(base?: I): WebUiAuthCheckResponse { + return WebUiAuthCheckResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): WebUiAuthCheckResponse { + const message = createBaseWebUiAuthCheckResponse(); + message.authenticated = object.authenticated ?? false; + message.username = object.username ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; + create, I>>(base?: I): T; + fromPartial, I>>(object: I): T; +} diff --git a/ui/src/lib/hooks/is-mobile.svelte.ts b/ui/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/ui/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/ui/src/lib/index.ts b/ui/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/ui/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/ui/src/lib/stores/auth.ts b/ui/src/lib/stores/auth.ts new file mode 100644 index 0000000..58b3640 --- /dev/null +++ b/ui/src/lib/stores/auth.ts @@ -0,0 +1,49 @@ +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; + +export interface AuthState { + authenticated: boolean; + username: string | null; + token: string | null; +} + +function createAuthStore() { + const initialState: AuthState = { + authenticated: false, + username: null, + token: localStorage.getItem('token') + }; + + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + set, + update, + login: (username: string, token: string) => { + const newState: AuthState = { + authenticated: true, + username, + token + }; + set(newState); + localStorage.setItem('token', token); + }, + logout: () => { + set(initialState); + if (browser) { + localStorage.removeItem('token'); + } + }, + setFromLocalStorage: () => { + if (browser) { + const token = localStorage.getItem('token'); + if (token) { + update((state) => ({ ...state, token })); + } + } + } + }; +} + +export const authStore = createAuthStore(); diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/ui/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/ui/src/routes/(dashboard)/+layout.svelte b/ui/src/routes/(dashboard)/+layout.svelte new file mode 100644 index 0000000..cc89c6f --- /dev/null +++ b/ui/src/routes/(dashboard)/+layout.svelte @@ -0,0 +1,118 @@ + + + + + +

    RFID Master

    +
    + + + Navigation + + + + goto('/')}> + Dashboard + + + + goto('/users')}> + Users + + + + goto('/rfid')}> + RFID Database + + + + goto('/settings')} + > + Settings + + + + goto('/settings/hardware')} + > + Hardware Config + + + + + + + + {#if username} +

    Logged in as {username}

    + {/if} + {#if showLogout} + + {:else} +

    Auth disabled

    + {/if} +
    +
    +
    +
    +
    +
    + +

    + {#if currentPath === '/'} + Dashboard + {:else if currentPath === '/users'} + User Management + {:else if currentPath === '/rfid'} + RFID Database + {:else if currentPath === '/settings'} + Settings + {:else if currentPath === '/settings/hardware'} + Hardware Configuration + {:else} + Dashboard + {/if} +

    +
    +
    +
    +
    + {@render children()} +
    +
    +
    diff --git a/ui/src/routes/(dashboard)/+layout.ts b/ui/src/routes/(dashboard)/+layout.ts new file mode 100644 index 0000000..5981f22 --- /dev/null +++ b/ui/src/routes/(dashboard)/+layout.ts @@ -0,0 +1,27 @@ +import { redirect } from '@sveltejs/kit'; +import { webUIApi } from '$lib/api/webui'; +import { authStore } from '$lib/stores/auth'; + +export async function load() { + try { + const response = await webUIApi.authchk(); + if (response.authenticated) { + if (response && response.username) { + authStore.update((state) => ({ + ...state, + username: response.username || null, + authenticated: true + })); + } + + return { + username: response.username + }; + } else { + throw Error('Not authenticated'); + } + } catch { + authStore.logout(); + throw redirect(302, '/login'); + } +} diff --git a/ui/src/routes/(dashboard)/+page.svelte b/ui/src/routes/(dashboard)/+page.svelte new file mode 100644 index 0000000..3fe1255 --- /dev/null +++ b/ui/src/routes/(dashboard)/+page.svelte @@ -0,0 +1,282 @@ + + +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    +

    System Status

    +
    +
    + WiFi Station + + {deviceStatus?.info?.staConnectionState === 1 + ? 'Connected' + : deviceStatus?.info?.staConnectionState === 2 + ? 'Connecting' + : 'Disconnected'} + +
    +
    + WiFi AP + + {deviceStatus?.info?.apConnectionState === 1 ? 'Active' : 'Inactive'} + +
    +
    +
    + +
    +

    Sync Operations

    +
    + + +
    +
    +
    + +
    +

    Recent Activity

    +
    + {#if deviceStatus?.logs && deviceStatus.logs.length > 0} + {#each deviceStatus.logs.slice(-10) as log} + +
    +
    +
    +
    +
    +

    {log.message}

    +

    {new Date(log.timestamp).toLocaleString()}

    +
    +
    + {/each} + {:else} +

    No recent activity

    + {/if} +
    +
    + + +
    diff --git a/ui/src/routes/(dashboard)/rfid/+page.svelte b/ui/src/routes/(dashboard)/rfid/+page.svelte new file mode 100644 index 0000000..0fd2f97 --- /dev/null +++ b/ui/src/routes/(dashboard)/rfid/+page.svelte @@ -0,0 +1,237 @@ + + +
    +
    +

    RFID Database

    +

    View and manage RFID card IDs in the database

    +
    + +
    + + +
    +
    + RFID IDs ({rfidHexValues.length}) + Hexadecimal representation of stored RFID card IDs +
    +
    + + +
    + + +
    +
    +
    +
    + + +
    +
    + + { + if (e.key === 'Enter') { + addRfidId(); + } + }} + /> +
    +
    + +
    +
    + + +
    + {#if rfidHexValues.length === 0} +

    No RFID IDs in database

    + {:else} +
    + {#each rfidHexValues as hexValue (hexValue)} +
    +
    + {formatHexDisplay(hexValue)} + ({hexValue}) +
    + +
    + {/each} +
    + {/if} +
    +
    +
    +
    +
    diff --git a/ui/src/routes/(dashboard)/settings/+page.svelte b/ui/src/routes/(dashboard)/settings/+page.svelte new file mode 100644 index 0000000..c3e0c16 --- /dev/null +++ b/ui/src/routes/(dashboard)/settings/+page.svelte @@ -0,0 +1,140 @@ + + +
    +
    +

    Settings

    +

    Configure system settings and synchronization options

    +
    + +
    + + + + + Logging + Configure the logging level for system messages + + +
    + + +

    + Choose the minimum log level to display. Lower levels include higher ones. +

    +
    +
    +
    +
    +
    + For hardware configuration (pins, timing, LEDs), go to{' '} + +
    + +
    +
    +
    diff --git a/ui/src/routes/(dashboard)/settings/hardware/+page.svelte b/ui/src/routes/(dashboard)/settings/hardware/+page.svelte new file mode 100644 index 0000000..a80a701 --- /dev/null +++ b/ui/src/routes/(dashboard)/settings/hardware/+page.svelte @@ -0,0 +1,347 @@ + + +
    +
    +

    Hardware Configuration

    +

    Configure hardware pins, timing, and LED settings

    +
    + + {#if loading} +
    +
    +
    + {:else} +
    + +
    + +
    + + + Sensor OTA Update + + Enable Over-The-Air updates on the connected sensor. Configure the WiFi settings that + the sensor will use. + + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + + {#if otaConfig.useStaticIp} +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + {/if} + +
    + + +
    +
    +
    + + + + ESP32 OTA Update + + Enable Over-The-Air firmware updates for the ESP32 controller itself. Use Arduino IDE or + PlatformIO to upload firmware when enabled. + + + + {#if espOTAStatus} +
    +

    + Status: + {espOTAStatus.status} + +

    +
    + {/if} + +
    + + { + const target = e.target as HTMLInputElement; + selectedFirmwareFile = target.files?.[0] || null; + }} + class="block w-full text-sm text-gray-500 file:mr-4 file:rounded-full file:border-0 file:bg-blue-50 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-blue-700 hover:file:bg-blue-100" + /> +
    + +
    + + + +
    +
    +
    + + {#if error} +
    +

    {error}

    +
    + {/if} +
    + {/if} +
    diff --git a/ui/src/routes/(dashboard)/users/+page.svelte b/ui/src/routes/(dashboard)/users/+page.svelte new file mode 100644 index 0000000..080c7de --- /dev/null +++ b/ui/src/routes/(dashboard)/users/+page.svelte @@ -0,0 +1,276 @@ + + +
    +
    +
    +

    WebUI User Management

    +

    Manage webui users and their passwords

    +
    +
    + + + + + + + + + Add New User + Create a new webui user account. + +
    +
    + + +
    +
    + + +
    +
    + + Cancel + Add User + +
    +
    +
    + + + + + Change Password + Enter a new password for the user. + +
    +
    + + +
    +
    + + Cancel + Change Password + +
    +
    + + + + + Delete User + + {#if users.length === 1} + This is the last user. Deleting it will disable web UI authentication. Are you sure? + {:else} + Are you sure you want to delete this user? + {/if} + + + + Cancel + Delete + + + +
    + {#if users.length === 0 && !loading} + + + Warning: No Users Configured + + Authentication is disabled. Add at least one user to enable web UI authentication. + + + {:else if loading} +

    Loading users...

    + {:else} + + + Users ({users.length}) + Current registered webui users + + +
    + + + + + + + + + {#each users as user} + + + + + {/each} + +
    UsernameActions
    {user} +
    + + +
    +
    +
    +
    +
    + {/if} +
    diff --git a/ui/src/routes/+error.svelte b/ui/src/routes/+error.svelte new file mode 100644 index 0000000..2d2ae3d --- /dev/null +++ b/ui/src/routes/+error.svelte @@ -0,0 +1,44 @@ + + +
    + + +
    + {status || '404'} +
    + + {#if status === 404} + Page Not Found + {:else} + Something went wrong + {/if} + + + {#if status === 404} + The page you're looking for doesn't exist. + {:else} + {error?.message || 'An unexpected error occurred.'} + {/if} + +
    + + + + +
    +
    diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte new file mode 100644 index 0000000..4e660d6 --- /dev/null +++ b/ui/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + + + + + + +{@render children?.()} diff --git a/ui/src/routes/+layout.ts b/ui/src/routes/+layout.ts new file mode 100644 index 0000000..e4c61e4 --- /dev/null +++ b/ui/src/routes/+layout.ts @@ -0,0 +1,9 @@ +import { authStore } from '$lib/stores/auth'; + +export const ssr = false; +export const prerender = true; + +export const load = async () => { + authStore.setFromLocalStorage(); + return {}; +}; diff --git a/ui/src/routes/login/+layout.ts b/ui/src/routes/login/+layout.ts new file mode 100644 index 0000000..8c0f113 --- /dev/null +++ b/ui/src/routes/login/+layout.ts @@ -0,0 +1,11 @@ +import { redirect } from '@sveltejs/kit'; +import { webUIApi } from '$lib/api/webui'; + +export async function load() { + const response = await webUIApi.authchk(); + if (response.authenticated) { + throw redirect(302, '/'); + } else { + localStorage.removeItem('token'); + } +} diff --git a/ui/src/routes/login/+page.svelte b/ui/src/routes/login/+page.svelte new file mode 100644 index 0000000..6aefd1c --- /dev/null +++ b/ui/src/routes/login/+page.svelte @@ -0,0 +1,88 @@ + + +
    + + + Login + Enter your credentials to access the system + +
    + +
    + + +
    +
    + + +
    + {#if error} +

    {error}

    + {/if} + +
    +
    +
    +
    diff --git a/ui/static/robots.txt b/ui/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/ui/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/ui/svelte.config.js b/ui/svelte.config.js new file mode 100644 index 0000000..13c04c9 --- /dev/null +++ b/ui/svelte.config.js @@ -0,0 +1,24 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + csp: { + //mode: 'hash', + + directives: { + 'script-src': ['self','unsafe-inline','*'], + 'style-src': ['self','unsafe-inline','*'], + } + }, + output: { + bundleStrategy: 'inline', + } + }, + +}; + +export default config; diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..a5567ee --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..510c896 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + port: 3001 + }, + + plugins: [tailwindcss(), sveltekit()] +});