Compare commits
	
		
			52 Commits
		
	
	
		
			feat/web-c
			...
			93f0c71a6c
		
	
	| Author | SHA256 | Date | |
|---|---|---|---|
| 93f0c71a6c | |||
| 5e4fd59148 | |||
| 62a2dc2c4a | |||
| 59dc57a618 | |||
| 452589d11d | |||
| a745aaf9d0 | |||
| 49fe1b4ce3 | |||
| 4e5b1bfad0 | |||
| f03a98aadd | |||
| 2ae6d49b72 | |||
| de15b67115 | |||
| 7b9c6ae5b8 | |||
| 5a5dcb6334 | |||
| df9c84e9df | |||
| f6db27a225 | |||
| 6d9626eaa2 | |||
| dca88c26a4 | |||
| e43cb76e4d | |||
| c2e0fe94a4 | |||
| 2c94b7fb7e | |||
| 41ab137270 | |||
| 503bb22ea3 | |||
| 4896c63b1a | |||
| 62847f569d | |||
| ffdb644700 | |||
| 5319b38338 | |||
| bc0695626f | |||
| 283482b361 | |||
| ad32baa844 | |||
| 3f3c47d629 | |||
| fc8e4ca486 | |||
| 7e42d3b8c9 | |||
| 64b77c33b5 | |||
| 66b245e6ab | |||
| e3aebb041f | |||
| 755f26a93c | |||
| 6d20f4e54c | |||
| f341e6039f | |||
| 718e093d3d | |||
| 6300004ec3 | |||
| 50721114e3 | |||
| 2ed915601b | |||
| dae4403eaf | |||
| 16d49c9940 | |||
| 68e3121f41 | |||
| 4994b8a246 | |||
| c27763fc11 | |||
| 393eab2b45 | |||
| 097cb44649 | |||
| 95adba8e9a | |||
| a4a8b6c3c1 | |||
| aa3c250c2e | 
							
								
								
									
										87
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								README.md
									
									
									
									
									
								
							| @ -1,19 +1,92 @@ | |||||||
| # LocationHub | # LocationHub | ||||||
|  |  | ||||||
| TODO | **Self-hosted backend for LoRaWAN-based location tracking, supporting multiple location providers such as GNSS, Gateway-Triangulation, WiFi-Triangulation, and BLE-Triangulation.**   | ||||||
|  | We recommend using [The Things Network](https://www.thethingsnetwork.org/) (TTN) as middleware for relaying LoRaWAN payloads. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Table of Contents | ||||||
|  | 1. [Setup](#setup) | ||||||
|  |     - [Prerequisites](#prerequisites) | ||||||
|  |     - [Database](#database) | ||||||
|  |     - [Configuration](#configuration) | ||||||
|  | 2. [TTN Integration](#ttn-integration) | ||||||
|  | 3. [Add a Location Tracker](#add-a-location-tracker) | ||||||
|  |     - [Onboard SenseCAP T1000-B](#onboard-sensecap-t1000-b) | ||||||
|  |     - [Register SenseCAP T1000-B](#register-sensecap-t1000-b) | ||||||
|  | 4. [Testing](#testing) | ||||||
|  |     - [Testing Webhook](#testing-webhook) | ||||||
|  |     - [Emulating Wigle API](#emulating-wigle-api) | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
| ## Setup | ## Setup | ||||||
|  |  | ||||||
| ### Prerequisites | ### Prerequisites | ||||||
| - Node.js >= 22.11.0 | Ensure the following dependencies are installed: | ||||||
| - Maria DB >= 11.6.2 | - **Node.js** >= 22.11.0 | ||||||
|  | - **MariaDB** >= 11.6.2 | ||||||
|  | - A web server, such as **nginx** | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
| ### Database | ### Database | ||||||
| **Change name of database and credentials as you like!** | **Customize the database name and credentials to your preference.**   | ||||||
|  | Follow these steps to set up the database: | ||||||
| - Create new database: `CREATE DATABASE locationhub;` | - Create new database: `CREATE DATABASE dev_locationhub;` | ||||||
| - Create new user for database: `GRANT ALL PRIVILEGES ON dev_locationhub.* TO 'dbuser'@'localhost' IDENTIFIED BY '1234';` | - Create new user for database: `GRANT ALL PRIVILEGES ON dev_locationhub.* TO 'dbuser'@'localhost' IDENTIFIED BY '1234';` | ||||||
| - Import tables: `/usr/bin/mariadb -u dbuser -p1234 dev_locationhub < server/sql/tables.sql` | - Import tables: `/usr/bin/mariadb -u dbuser -p1234 dev_locationhub < server/sql/tables.sql` | ||||||
|  |  | ||||||
|  | ### Configuration | ||||||
|  | - Copy the .env.template file and rename it to .env: | ||||||
|  | - Use a strong token/secret for the webhook. | ||||||
|  | - Add your [Wigle](http://wigle.net) API token to translate MAC addresses to coordinates | ||||||
|  | - Use [systemd](server/scripts/locationhub.service) to start the server. | ||||||
|  |  | ||||||
|  | ### TTN Integration | ||||||
|  | Create new Webhook for your application. Set base url to `https://your.domain.tld` and enable "Uplink message" to api `/api/ttn/webhook`. | ||||||
|  | Add a addidtional header:  | ||||||
|  | - Type: `authorization`  | ||||||
|  | - Value: `Bearer your-very-secure-token-from-the-env-file` | ||||||
|  |  | ||||||
|  | ## Add a Location Tracker | ||||||
|  | We use the [SenseCAP T1000-B](https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-B-p-5698.html) from seeedstudio because of the fair price and multiple location providers. However, you can use any LoRaWAN-enabled tracker that is compatible with TTN and supports the required payload fields. | ||||||
|  |  | ||||||
|  | ### Onboard SenseCAP T1000-B | ||||||
|  | 1. Download and install the App [SenseCraft](https://play.google.com/store/apps/details?id=cc.seeed.sensecapmate)  | ||||||
|  | 2. Skip the user account at startup with `Skip` in the upper right corner | ||||||
|  | 3. Select the `Tracker T1000` | ||||||
|  | 4. Connect to your SenseCAP T1000-B | ||||||
|  | 5. In the `Settings` tab select `The Things Network` as Platform under `LoRa` | ||||||
|  | 6. Copy `Device EUI`, `AppEUI` and `AppKey` | ||||||
|  | 7. Save LoRa settings | ||||||
|  | 8. Under `Geolocation` select Geolocation Strategy as `GNSS+Wi-Fi` | ||||||
|  | 9. Set `GNSS Max Scan Time (s)` to 120 | ||||||
|  | 10. Save Geolocation settings | ||||||
|  | 11. Exit App with return arrow to trigger re-init of SenseCAP T1000-B | ||||||
|  |  | ||||||
|  | ### Register SenseCAP T1000-B | ||||||
|  | 1. Open your Application in TTN and navigate to `End devices` | ||||||
|  | 2. Click `Register end device` in the upper right corner | ||||||
|  | 3. Set input method to `Enter end device specifics manually` | ||||||
|  |  - Frequency plan: `Europe 863-870 MHz (SF9 for RX2 - recommended)` | ||||||
|  |  - LoRaWAN version: `LoRaWAN Specification 1.0.4` | ||||||
|  |  - Regional Parameters version: `RP002 Regional Parameters 1.0.3` | ||||||
|  | 4. Place as `JoinEUI` the `AppEUI` | ||||||
|  | 5. Place the `DevEUI` | ||||||
|  | 6. Place the `AppKey` | ||||||
|  | 7. Set a name for end device | ||||||
|  | 8. Click `Register end device` | ||||||
|  | 9. Check if new end device joins the TTN | ||||||
|  | 10. Place the [Uplink Payload Formatter](TTN/sensecap_payload_formater.js) as `Custom Javascript formatter` for new end device | ||||||
|  |  | ||||||
|  | ## Testing | ||||||
| ### Testing Webhook | ### Testing Webhook | ||||||
| - To test the webhook use the python script `ttn-webhook-dummy.py` to send prerecorded TTN Uplinks. | - To test the webhook use the python script [ttn-webhook-dummy.py](server/scripts/ttn-webhook-dummy.py) to send prerecorded TTN Uplinks. | ||||||
| - To test the script you can use `while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nSuccess"; nc -l -p 8080 -q 1; done` | - To test the script you can use `while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nSuccess"; nc -l -p 8080 -q 1; done` | ||||||
|  |  | ||||||
|  | ### Emulating Wigle API | ||||||
|  | - To emulate the Wigle API use the python script [wigle-dummy.py](server/scripts/wigle-dummy.py) to translate MAC addresses to coordinates.  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Happy tracking! 🎉 | ||||||
							
								
								
									
										911
									
								
								TTN/sensecap_payload_formater.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										911
									
								
								TTN/sensecap_payload_formater.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,911 @@ | |||||||
|  | function decodeUplink (input) { | ||||||
|  |     const bytes = input['bytes'] | ||||||
|  |     const fport = parseInt(input['fPort']) | ||||||
|  |     const bytesString = bytes2HexString(bytes) | ||||||
|  |     const originMessage = bytesString.toLocaleUpperCase() | ||||||
|  |     const decoded = { | ||||||
|  |         valid: true, | ||||||
|  |         err: 0, | ||||||
|  |         payload: bytesString, | ||||||
|  |         messages: [] | ||||||
|  |     } | ||||||
|  |     if (fport === 199 || fport === 192) { | ||||||
|  |         decoded.messages.push({fport: fport, payload: bytesString}) | ||||||
|  |         return { data: decoded } | ||||||
|  |     } | ||||||
|  |     let measurement = messageAnalyzed(originMessage) | ||||||
|  |     if (measurement.length === 0) { | ||||||
|  |         decoded.valid = false | ||||||
|  |         return { data: decoded } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (let message of measurement) { | ||||||
|  |         if (message.length === 0) { | ||||||
|  |             continue | ||||||
|  |         } | ||||||
|  |         let elements = [] | ||||||
|  |         for (let element of message) { | ||||||
|  |             if (element.errorCode) { | ||||||
|  |                 decoded.err = element.errorCode | ||||||
|  |                 decoded.errMessage = element.error | ||||||
|  |             } else { | ||||||
|  |                 elements.push(element) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (elements.length > 0) { | ||||||
|  |             decoded.messages.push(elements) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     // decoded.messages = measurement | ||||||
|  |     return { data: decoded } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function messageAnalyzed (messageValue) { | ||||||
|  |     try { | ||||||
|  |         let frames = unpack(messageValue) | ||||||
|  |         let measurementResultArray = [] | ||||||
|  |         for (let i = 0; i < frames.length; i++) { | ||||||
|  |             let item = frames[i] | ||||||
|  |             let dataId = item.dataId | ||||||
|  |             let dataValue = item.dataValue | ||||||
|  |             let measurementArray = deserialize(dataId, dataValue) | ||||||
|  |             measurementResultArray.push(measurementArray) | ||||||
|  |         } | ||||||
|  |         return measurementResultArray | ||||||
|  |     } catch (e) { | ||||||
|  |         return e.toString() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function unpack (messageValue) { | ||||||
|  |     let frameArray = [] | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < messageValue.length; i++) { | ||||||
|  |         let remainMessage = messageValue | ||||||
|  |         let dataId = remainMessage.substring(0, 2).toUpperCase() | ||||||
|  |         let dataValue | ||||||
|  |         let dataObj = {} | ||||||
|  |         let packageLen | ||||||
|  |         switch (dataId) { | ||||||
|  |             case '01': | ||||||
|  |                 dataValue = remainMessage.substring(2, 94) | ||||||
|  |                 messageValue = remainMessage.substring(94) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '02': | ||||||
|  |                 dataValue = remainMessage.substring(2, 32) | ||||||
|  |                 messageValue = remainMessage.substring(32) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '03': | ||||||
|  |                 dataValue = remainMessage.substring(2, 64) | ||||||
|  |                 messageValue = remainMessage.substring(64) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '04': | ||||||
|  |                 dataValue = remainMessage.substring(2, 20) | ||||||
|  |                 messageValue = remainMessage.substring(20) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '05': | ||||||
|  |                 dataValue = remainMessage.substring(2, 10) | ||||||
|  |                 messageValue = remainMessage.substring(10) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '06': | ||||||
|  |                 dataValue = remainMessage.substring(2, 44) | ||||||
|  |                 messageValue = remainMessage.substring(44) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '07': | ||||||
|  |                 dataValue = remainMessage.substring(2, 84) | ||||||
|  |                 messageValue = remainMessage.substring(84) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '08': | ||||||
|  |                 dataValue = remainMessage.substring(2, 70) | ||||||
|  |                 messageValue = remainMessage.substring(70) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '09': | ||||||
|  |                 dataValue = remainMessage.substring(2, 36) | ||||||
|  |                 messageValue = remainMessage.substring(36) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '0A': | ||||||
|  |                 dataValue = remainMessage.substring(2, 76) | ||||||
|  |                 messageValue = remainMessage.substring(76) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '0B': | ||||||
|  |                 dataValue = remainMessage.substring(2, 62) | ||||||
|  |                 messageValue = remainMessage.substring(62) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '0C': | ||||||
|  |                 break | ||||||
|  |             case '0D': | ||||||
|  |                 dataValue = remainMessage.substring(2, 10) | ||||||
|  |                 messageValue = remainMessage.substring(10) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '0E': | ||||||
|  |                 packageLen = getInt(remainMessage.substring(8, 10)) * 2 + 10 | ||||||
|  |                 dataValue = remainMessage.substring(2, 8) + remainMessage.substring(10, packageLen) | ||||||
|  |                 messageValue = remainMessage.substring(packageLen) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '0F': | ||||||
|  |                 dataValue = remainMessage.substring(2, 34) | ||||||
|  |                 messageValue = remainMessage.substring(34) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '10': | ||||||
|  |                 dataValue = remainMessage.substring(2, 26) | ||||||
|  |                 messageValue = remainMessage.substring(26) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '11': | ||||||
|  |                 dataValue = remainMessage.substring(2, 28) | ||||||
|  |                 messageValue = remainMessage.substring(28) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '1A': | ||||||
|  |                 dataValue = remainMessage.substring(2, 56) | ||||||
|  |                 messageValue = remainMessage.substring(56) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '1B': | ||||||
|  |                 dataValue = remainMessage.substring(2, 96) | ||||||
|  |                 messageValue = remainMessage.substring(96) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '1C': | ||||||
|  |                 dataValue = remainMessage.substring(2, 82) | ||||||
|  |                 messageValue = remainMessage.substring(82) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case '1D': | ||||||
|  |                 dataValue = remainMessage.substring(2, 40) | ||||||
|  |                 messageValue = remainMessage.substring(40) | ||||||
|  |                 dataObj = { | ||||||
|  |                     'dataId': dataId, 'dataValue': dataValue | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             default: | ||||||
|  |                 return frameArray | ||||||
|  |         } | ||||||
|  |         if (dataValue.length < 2) { | ||||||
|  |             break | ||||||
|  |         } | ||||||
|  |         frameArray.push(dataObj) | ||||||
|  |     } | ||||||
|  |     return frameArray | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function deserialize (dataId, dataValue) { | ||||||
|  |     let measurementArray = [] | ||||||
|  |     let eventList = [] | ||||||
|  |     let measurement = {} | ||||||
|  |     let collectTime = 0 | ||||||
|  |     let groupId = 0 | ||||||
|  |     let shardFlag = {} | ||||||
|  |     let payload = '' | ||||||
|  |     let motionId = '' | ||||||
|  |     switch (dataId) { | ||||||
|  |         case '01': | ||||||
|  |             measurementArray = getUpShortInfo(dataValue) | ||||||
|  |             measurementArray.push(...getMotionSetting(dataValue.substring(30, 40))) | ||||||
|  |             measurementArray.push(...getStaticSetting(dataValue.substring(40, 46))) | ||||||
|  |             measurementArray.push(...getShockSetting(dataValue.substring(46, 52))) | ||||||
|  |             measurementArray.push(...getTempSetting(dataValue.substring(52, 72))) | ||||||
|  |             measurementArray.push(...getLightSetting(dataValue.substring(72, 92))) | ||||||
|  |             break | ||||||
|  |         case '02': | ||||||
|  |             measurementArray = getUpShortInfo(dataValue) | ||||||
|  |             break | ||||||
|  |         case '03': | ||||||
|  |             measurementArray.push(...getMotionSetting(dataValue.substring(0, 10))) | ||||||
|  |             measurementArray.push(...getStaticSetting(dataValue.substring(10, 16))) | ||||||
|  |             measurementArray.push(...getShockSetting(dataValue.substring(16, 22))) | ||||||
|  |             measurementArray.push(...getTempSetting(dataValue.substring(22, 42))) | ||||||
|  |             measurementArray.push(...getLightSetting(dataValue.substring(42, 62))) | ||||||
|  |             break | ||||||
|  |         case '04': | ||||||
|  |             let interval = 0 | ||||||
|  |             let workMode = getInt(dataValue.substring(0, 2)) | ||||||
|  |             let heartbeatInterval = getMinsByMin(dataValue.substring(4, 8)) | ||||||
|  |             let periodicInterval = getMinsByMin(dataValue.substring(8, 12)) | ||||||
|  |             let eventInterval = getMinsByMin(dataValue.substring(12, 16)) | ||||||
|  |             switch (workMode) { | ||||||
|  |                 case 0: | ||||||
|  |                     interval = heartbeatInterval | ||||||
|  |                     break | ||||||
|  |                 case 1: | ||||||
|  |                     interval = periodicInterval | ||||||
|  |                     break | ||||||
|  |                 case 2: | ||||||
|  |                     interval = eventInterval | ||||||
|  |                     break | ||||||
|  |             } | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '3940', type: 'Work Mode', measurementValue: workMode}, | ||||||
|  |                 {measurementId: '3942', type: 'Heartbeat Interval', measurementValue: heartbeatInterval}, | ||||||
|  |                 {measurementId: '3943', type: 'Periodic Interval', measurementValue: periodicInterval}, | ||||||
|  |                 {measurementId: '3944', type: 'Event Interval', measurementValue: eventInterval}, | ||||||
|  |                 {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(16, 18))}, | ||||||
|  |                 {measurementId: '3900', type: 'Uplink Interval', measurementValue: interval} | ||||||
|  |             ] | ||||||
|  |             break; | ||||||
|  |         case '05': | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(0, 2))}, | ||||||
|  |                 {measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(dataValue.substring(2, 4))}, | ||||||
|  |                 {measurementId: '3965', type: 'Positioning Strategy', measurementValue: getPositioningStrategy(dataValue.substring(4, 6))}, | ||||||
|  |                 {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(6, 8))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '06': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '4197', timestamp: collectTime, motionId: motionId, type: 'Longitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(16, 24), 1000000))}, | ||||||
|  |                 {measurementId: '4198', timestamp: collectTime, motionId: motionId, type: 'Latitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(24, 32), 1000000))}, | ||||||
|  |                 {measurementId: '4097', timestamp: collectTime, motionId: motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(32, 36), 10)}, | ||||||
|  |                 {measurementId: '4199', timestamp: collectTime, motionId: motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(36, 40))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(40, 42))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '07': | ||||||
|  |             eventList = getEventStatus(dataValue.substring(0, 6)) | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '5001', timestamp: collectTime, motionId: motionId, type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, | ||||||
|  |                 {measurementId: '4097', timestamp: collectTime, motionId: motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(72, 76), 10)}, | ||||||
|  |                 {measurementId: '4199', timestamp: collectTime, motionId: motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(76, 80))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(80, 82))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '08': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '5002', timestamp: collectTime, motionId: motionId, type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, | ||||||
|  |                 {measurementId: '4097', timestamp: collectTime, motionId: motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(58, 62), 10)}, | ||||||
|  |                 {measurementId: '4199', timestamp: collectTime, motionId: motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(62, 66))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(66, 68))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '09': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '4197', timestamp: collectTime, motionId: motionId, type: 'Longitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(16, 24), 1000000))}, | ||||||
|  |                 {measurementId: '4198', timestamp: collectTime, motionId: motionId, type: 'Latitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(24, 32), 1000000))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(32, 34))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '0A': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '5001', timestamp: collectTime, motionId, type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(72, 74))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '0B': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '5002', timestamp: collectTime, motionId, type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(58, 60))}, | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         case '0D': | ||||||
|  |             let errorCode = getInt(dataValue) | ||||||
|  |             let error = '' | ||||||
|  |             switch (errorCode) { | ||||||
|  |                 case 1: | ||||||
|  |                     error = 'FAILED TO OBTAIN THE UTC TIMESTAMP' | ||||||
|  |                     break | ||||||
|  |                 case 2: | ||||||
|  |                     error = 'ALMANAC TOO OLD' | ||||||
|  |                     break | ||||||
|  |                 case 3: | ||||||
|  |                     error = 'DOPPLER ERROR' | ||||||
|  |                     break | ||||||
|  |             } | ||||||
|  |             measurementArray.push({errorCode, error}) | ||||||
|  |             break | ||||||
|  |         case '0E': | ||||||
|  |             shardFlag = getShardFlag(dataValue.substring(0, 2)) | ||||||
|  |             groupId = getInt(dataValue.substring(2, 6)) | ||||||
|  |             payload = dataValue.substring(6) | ||||||
|  |             measurement = { | ||||||
|  |                 measurementId: '6152', | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'gnss-ng payload', | ||||||
|  |                 measurementValue: payload | ||||||
|  |             } | ||||||
|  |             measurementArray.push(measurement) | ||||||
|  |             break | ||||||
|  |         case '0F': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             shardFlag = getShardFlag(dataValue.substring(26, 28)) | ||||||
|  |             groupId = getInt(dataValue.substring(28, 32)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '4200', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 motionId, | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'Event Status', | ||||||
|  |                 measurementValue: getEventStatus(dataValue.substring(0, 6)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '4097', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 motionId, | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'Air Temperature', | ||||||
|  |                 measurementValue: getSensorValue(dataValue.substring(16, 20), 10) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '4199', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 motionId, | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'Light', | ||||||
|  |                 measurementValue: getSensorValue(dataValue.substring(20, 24)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '3000', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 motionId, | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'Battery', | ||||||
|  |                 measurementValue: getBattery(dataValue.substring(24, 26)) | ||||||
|  |             }) | ||||||
|  |             break | ||||||
|  |         case '10': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             shardFlag = getShardFlag(dataValue.substring(18, 20)) | ||||||
|  |             groupId = getInt(dataValue.substring(20, 24)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '4200', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 motionId, | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'Event Status', | ||||||
|  |                 measurementValue: getEventStatus(dataValue.substring(0, 6)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '3000', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 motionId, | ||||||
|  |                 groupId: groupId, | ||||||
|  |                 index: shardFlag.index, | ||||||
|  |                 count: shardFlag.count, | ||||||
|  |                 type: 'Battery', | ||||||
|  |                 measurementValue: getBattery(dataValue.substring(16, 18)) | ||||||
|  |             }) | ||||||
|  |             break | ||||||
|  |         case '11': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '3576', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 type: 'Positioning Status', | ||||||
|  |                 measurementValue: getPositingStatus(dataValue.substring(0, 2)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '4200', | ||||||
|  |                 type: 'Event Status', | ||||||
|  |                 measurementValue: getEventStatus(dataValue.substring(2, 8)) | ||||||
|  |             }) | ||||||
|  |             if (!isNaN(parseFloat(getSensorValue(dataValue.substring(16, 20), 10)))) { | ||||||
|  |                 measurementArray.push({ | ||||||
|  |                     timestamp: collectTime, | ||||||
|  |                     measurementId: '4097', | ||||||
|  |                     type: 'Air Temperature', | ||||||
|  |                     measurementValue: getSensorValue(dataValue.substring(16, 20), 10) | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             if (!isNaN(parseFloat(getSensorValue(dataValue.substring(20, 24))))) { | ||||||
|  |                 measurementArray.push({ | ||||||
|  |                     timestamp: collectTime, | ||||||
|  |                     measurementId: '4199', | ||||||
|  |                     type: 'Light', | ||||||
|  |                     measurementValue: getSensorValue(dataValue.substring(20, 24)) | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '3000', | ||||||
|  |                 type: 'Battery', | ||||||
|  |                 measurementValue: getBattery(dataValue.substring(24, 26)) | ||||||
|  |             }) | ||||||
|  |             break | ||||||
|  |         case '1A': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '4197', timestamp: collectTime, motionId, type: 'Longitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(16, 24), 1000000))}, | ||||||
|  |                 {measurementId: '4198', timestamp: collectTime, motionId, type: 'Latitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(24, 32), 1000000))}, | ||||||
|  |                 {measurementId: '4097', timestamp: collectTime, motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(32, 36), 10)}, | ||||||
|  |                 {measurementId: '4199', timestamp: collectTime, motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(36, 40))}, | ||||||
|  |                 {measurementId: '4210', timestamp: collectTime, motionId, type: 'AccelerometerX', measurementValue: getSensorValue(dataValue.substring(40, 44))}, | ||||||
|  |                 {measurementId: '4211', timestamp: collectTime, motionId, type: 'AccelerometerY', measurementValue: getSensorValue(dataValue.substring(44, 48))}, | ||||||
|  |                 {measurementId: '4212', timestamp: collectTime, motionId, type: 'AccelerometerZ', measurementValue: getSensorValue(dataValue.substring(48, 52))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(52, 54))}, | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         // WIFI定位数据+sensor+三轴+电量 | ||||||
|  |         case '1B': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '5001', timestamp: collectTime, motionId, type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, | ||||||
|  |                 {measurementId: '4097', timestamp: collectTime, motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(72, 76), 10)}, | ||||||
|  |                 {measurementId: '4199', timestamp: collectTime, motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(76, 80))}, | ||||||
|  |                 {measurementId: '4210', timestamp: collectTime, motionId, type: 'AccelerometerX', measurementValue: getSensorValue(dataValue.substring(80, 84))}, | ||||||
|  |                 {measurementId: '4211', timestamp: collectTime, motionId, type: 'AccelerometerY', measurementValue: getSensorValue(dataValue.substring(84, 88))}, | ||||||
|  |                 {measurementId: '4212', timestamp: collectTime, motionId, type: 'AccelerometerZ', measurementValue: getSensorValue(dataValue.substring(88, 92))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(92, 94))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         // BLE定位数据+sensor+三轴+电量 | ||||||
|  |         case '1C': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             motionId = getMotionId(dataValue.substring(6, 8)) | ||||||
|  |             measurementArray = [ | ||||||
|  |                 {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, | ||||||
|  |                 {measurementId: '5002', timestamp: collectTime, motionId, type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, | ||||||
|  |                 {measurementId: '4097', timestamp: collectTime, motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(58, 62), 10)}, | ||||||
|  |                 {measurementId: '4199', timestamp: collectTime, motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(62, 66))}, | ||||||
|  |                 {measurementId: '4210', timestamp: collectTime, motionId, type: 'AccelerometerX', measurementValue: getSensorValue(dataValue.substring(66, 70))}, | ||||||
|  |                 {measurementId: '4211', timestamp: collectTime, motionId, type: 'AccelerometerY', measurementValue: getSensorValue(dataValue.substring(70, 74))}, | ||||||
|  |                 {measurementId: '4212', timestamp: collectTime, motionId, type: 'AccelerometerZ', measurementValue: getSensorValue(dataValue.substring(74, 78))}, | ||||||
|  |                 {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(78, 80))} | ||||||
|  |             ] | ||||||
|  |             break | ||||||
|  |         // 定位状态 + sensor+三轴数据上报 | ||||||
|  |         case '1D': | ||||||
|  |             collectTime = getUTCTimestamp(dataValue.substring(8, 16)) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 measurementId: '3576', | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 type: 'Positioning Status', | ||||||
|  |                 measurementValue: getPositingStatus(dataValue.substring(0, 2)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '4200', | ||||||
|  |                 type: 'Event Status', | ||||||
|  |                 measurementValue: getEventStatus(dataValue.substring(2, 8)) | ||||||
|  |             }) | ||||||
|  |             if (!isNaN(parseFloat(getSensorValue(dataValue.substring(16, 20), 10)))) { | ||||||
|  |                 measurementArray.push({ | ||||||
|  |                     timestamp: collectTime, | ||||||
|  |                     measurementId: '4097', | ||||||
|  |                     type: 'Air Temperature', | ||||||
|  |                     measurementValue: getSensorValue(dataValue.substring(16, 20), 10) | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             if (!isNaN(parseFloat(getSensorValue(dataValue.substring(20, 24))))) { | ||||||
|  |                 measurementArray.push({ | ||||||
|  |                     timestamp: collectTime, | ||||||
|  |                     measurementId: '4199', | ||||||
|  |                     type: 'Light', | ||||||
|  |                     measurementValue: getSensorValue(dataValue.substring(20, 24)) | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '4210', | ||||||
|  |                 type: 'AccelerometerX', | ||||||
|  |                 measurementValue: getSensorValue(dataValue.substring(24, 28)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '4211', | ||||||
|  |                 type: 'AccelerometerY', | ||||||
|  |                 measurementValue: getSensorValue(dataValue.substring(28, 32)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '4212', | ||||||
|  |                 type: 'AccelerometerZ', | ||||||
|  |                 measurementValue: getSensorValue(dataValue.substring(32, 36)) | ||||||
|  |             }) | ||||||
|  |             measurementArray.push({ | ||||||
|  |                 timestamp: collectTime, | ||||||
|  |                 measurementId: '3000', | ||||||
|  |                 type: 'Battery', | ||||||
|  |                 measurementValue: getBattery(dataValue.substring(36, 38)) | ||||||
|  |             }) | ||||||
|  |             break | ||||||
|  |     } | ||||||
|  |     return measurementArray | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMotionId (str) { | ||||||
|  |     return getInt(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getPositingStatus (str) { | ||||||
|  |     let status = getInt(str) | ||||||
|  |     switch (status) { | ||||||
|  |         case 0: | ||||||
|  |             return {id:status, statusName:"Positioning successful."} | ||||||
|  |         case 1: | ||||||
|  |             return {id:status, statusName:"The GNSS scan timed out and failed to obtain the location."} | ||||||
|  |         case 2: | ||||||
|  |             return {id:status, statusName:"The Wi-Fi scan timed out and failed to obtain the location."} | ||||||
|  |         case 3: | ||||||
|  |             return {id:status, statusName:"The Wi-Fi + GNSS scan timed out and failed to obtain the location."} | ||||||
|  |         case 4: | ||||||
|  |             return {id:status, statusName:"The GNSS + Wi-Fi scan timed out and failed to obtain the location."} | ||||||
|  |         case 5: | ||||||
|  |             return {id:status, statusName:"The Bluetooth scan timed out and failed to obtain the location."} | ||||||
|  |         case 6: | ||||||
|  |             return {id:status, statusName:"The Bluetooth + Wi-Fi scan timed out and failed to obtain the location."} | ||||||
|  |         case 7: | ||||||
|  |             return {id:status, statusName:"The Bluetooth + GNSS scan timed out and failed to obtain the location."} | ||||||
|  |         case 8: | ||||||
|  |             return {id:status, statusName:"The Bluetooth + Wi-Fi + GNSS scan timed out and failed to obtain the location."} | ||||||
|  |         case 9: | ||||||
|  |             return {id:status, statusName:"Location Server failed to parse the GNSS location."} | ||||||
|  |         case 10: | ||||||
|  |             return {id:status, statusName:"Location Server failed to parse the Wi-Fi location."} | ||||||
|  |         case 11: | ||||||
|  |             return {id:status, statusName:"Location Server failed to parse the Bluetooth location."} | ||||||
|  |         case 12: | ||||||
|  |             return {id:status, statusName:"Failed to parse the GNSS location due to the poor accuracy."} | ||||||
|  |         case 13: | ||||||
|  |             return {id:status, statusName:"Time synchronization failed."} | ||||||
|  |         case 14: | ||||||
|  |             return {id:status, statusName:"Failed to obtain location due to the old Almanac."} | ||||||
|  |     } | ||||||
|  |     return getInt(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getUpShortInfo (messageValue) { | ||||||
|  |     return [ | ||||||
|  |         { | ||||||
|  |             measurementId: '3000', type: 'Battery', measurementValue: getBattery(messageValue.substring(0, 2)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3502', type: 'Firmware Version', measurementValue: getSoftVersion(messageValue.substring(2, 6)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3001', type: 'Hardware Version', measurementValue: getHardVersion(messageValue.substring(6, 10)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(messageValue.substring(10, 12)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3965', type: 'Positioning Strategy', measurementValue: getPositioningStrategy(messageValue.substring(12, 14)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getMinsByMin(messageValue.substring(14, 18)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3943', type: 'Periodic Interval', measurementValue: getMinsByMin(messageValue.substring(18, 22)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3944', type: 'Event Interval', measurementValue: getMinsByMin(messageValue.substring(22, 26)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3945', type: 'Sensor Enable', measurementValue: getInt(messageValue.substring(26, 28)) | ||||||
|  |         }, { | ||||||
|  |             measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(messageValue.substring(28, 30)) | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMotionSetting (str) { | ||||||
|  |     return [ | ||||||
|  |         {measurementId: '3946', type: 'Motion Enable', measurementValue: getInt(str.substring(0, 2))}, | ||||||
|  |         {measurementId: '3947', type: 'Any Motion Threshold', measurementValue: getSensorValue(str.substring(2, 6), 1)}, | ||||||
|  |         {measurementId: '3948', type: 'Motion Start Interval', measurementValue: getMinsByMin(str.substring(6, 10))}, | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getStaticSetting (str) { | ||||||
|  |     return [ | ||||||
|  |         {measurementId: '3949', type: 'Static Enable', measurementValue: getInt(str.substring(0, 2))}, | ||||||
|  |         {measurementId: '3950', type: 'Device Static Timeout', measurementValue: getMinsByMin(str.substring(2, 6))} | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getShockSetting (str) { | ||||||
|  |     return [ | ||||||
|  |         {measurementId: '3951', type: 'Shock Enable', measurementValue: getInt(str.substring(0, 2))}, | ||||||
|  |         {measurementId: '3952', type: 'Shock Threshold', measurementValue: getInt(str.substring(2, 6))} | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getTempSetting (str) { | ||||||
|  |     return [ | ||||||
|  |         {measurementId: '3953', type: 'Temp Enable', measurementValue: getInt(str.substring(0, 2))}, | ||||||
|  |         {measurementId: '3954', type: 'Event Temp Interval', measurementValue: getMinsByMin(str.substring(2, 6))}, | ||||||
|  |         {measurementId: '3955', type: 'Event Temp Sample Interval', measurementValue: getSecondsByInt(str.substring(6, 10))}, | ||||||
|  |         {measurementId: '3956', type: 'Temp ThMax', measurementValue: getSensorValue(str.substring(10, 14), 10)}, | ||||||
|  |         {measurementId: '3957', type: 'Temp ThMin', measurementValue: getSensorValue(str.substring(14, 18), 10)}, | ||||||
|  |         {measurementId: '3958', type: 'Temp Warning Type', measurementValue: getInt(str.substring(18, 20))} | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getLightSetting (str) { | ||||||
|  |     return [ | ||||||
|  |         {measurementId: '3959', type: 'Light Enable', measurementValue: getInt(str.substring(0, 2))}, | ||||||
|  |         {measurementId: '3960', type: 'Event Light Interval', measurementValue: getMinsByMin(str.substring(2, 6))}, | ||||||
|  |         {measurementId: '3961', type: 'Event Light Sample Interval', measurementValue: getSecondsByInt(str.substring(6, 10))}, | ||||||
|  |         {measurementId: '3962', type: 'Light ThMax', measurementValue: getSensorValue(str.substring(10, 14), 10)}, | ||||||
|  |         {measurementId: '3963', type: 'Light ThMin', measurementValue: getSensorValue(str.substring(14, 18), 10)}, | ||||||
|  |         {measurementId: '3964', type: 'Light Warning Type', measurementValue: getInt(str.substring(18, 20))} | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getShardFlag (str) { | ||||||
|  |     let bitStr = getByteArray(str) | ||||||
|  |     return { | ||||||
|  |         count: parseInt(bitStr.substring(0, 4), 2), | ||||||
|  |         index: parseInt(bitStr.substring(4), 2) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getBattery (batteryStr) { | ||||||
|  |     return loraWANV2DataFormat(batteryStr) | ||||||
|  | } | ||||||
|  | function getSoftVersion (softVersion) { | ||||||
|  |     return `${loraWANV2DataFormat(softVersion.substring(0, 2))}.${loraWANV2DataFormat(softVersion.substring(2, 4))}` | ||||||
|  | } | ||||||
|  | function getHardVersion (hardVersion) { | ||||||
|  |     return `${loraWANV2DataFormat(hardVersion.substring(0, 2))}.${loraWANV2DataFormat(hardVersion.substring(2, 4))}` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getSecondsByInt (str) { | ||||||
|  |     return getInt(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMinsByMin (str) { | ||||||
|  |     return getInt(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getSensorValue (str, dig) { | ||||||
|  |     if (str === '8000') { | ||||||
|  |         return null | ||||||
|  |     } else { | ||||||
|  |         return loraWANV2DataFormat(str, dig) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function bytes2HexString (arrBytes) { | ||||||
|  |     var str = '' | ||||||
|  |     for (var i = 0; i < arrBytes.length; i++) { | ||||||
|  |         var tmp | ||||||
|  |         var num = arrBytes[i] | ||||||
|  |         if (num < 0) { | ||||||
|  |             tmp = (255 + num + 1).toString(16) | ||||||
|  |         } else { | ||||||
|  |             tmp = num.toString(16) | ||||||
|  |         } | ||||||
|  |         if (tmp.length === 1) { | ||||||
|  |             tmp = '0' + tmp | ||||||
|  |         } | ||||||
|  |         str += tmp | ||||||
|  |     } | ||||||
|  |     return str | ||||||
|  | } | ||||||
|  | function loraWANV2DataFormat (str, divisor = 1) { | ||||||
|  |     let strReverse = bigEndianTransform(str) | ||||||
|  |     let str2 = toBinary(strReverse) | ||||||
|  |     if (str2.substring(0, 1) === '1') { | ||||||
|  |         let arr = str2.split('') | ||||||
|  |         let reverseArr = arr.map((item) => { | ||||||
|  |             if (parseInt(item) === 1) { | ||||||
|  |                 return 0 | ||||||
|  |             } else { | ||||||
|  |                 return 1 | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         str2 = parseInt(reverseArr.join(''), 2) + 1 | ||||||
|  |         return parseFloat('-' + str2 / divisor) | ||||||
|  |     } | ||||||
|  |     return parseInt(str2, 2) / divisor | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function bigEndianTransform (data) { | ||||||
|  |     let dataArray = [] | ||||||
|  |     for (let i = 0; i < data.length; i += 2) { | ||||||
|  |         dataArray.push(data.substring(i, i + 2)) | ||||||
|  |     } | ||||||
|  |     return dataArray | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function toBinary (arr) { | ||||||
|  |     let binaryData = arr.map((item) => { | ||||||
|  |         let data = parseInt(item, 16) | ||||||
|  |             .toString(2) | ||||||
|  |         let dataLength = data.length | ||||||
|  |         if (data.length !== 8) { | ||||||
|  |             for (let i = 0; i < 8 - dataLength; i++) { | ||||||
|  |                 data = `0` + data | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return data | ||||||
|  |     }) | ||||||
|  |     return binaryData.toString().replace(/,/g, '') | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getSOSMode (str) { | ||||||
|  |     return loraWANV2DataFormat(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMacAndRssiObj (pair) { | ||||||
|  |     let pairs = [] | ||||||
|  |     if (pair.length % 14 === 0) { | ||||||
|  |         for (let i = 0; i < pair.length; i += 14) { | ||||||
|  |             let mac = getMacAddress(pair.substring(i, i + 12)) | ||||||
|  |             if (mac) { | ||||||
|  |                 let rssi = getInt8RSSI(pair.substring(i + 12, i + 14)) | ||||||
|  |                 pairs.push({mac: mac, rssi: rssi}) | ||||||
|  |             } else { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return pairs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMacAddress (str) { | ||||||
|  |     if (str.toLowerCase() === 'ffffffffffff') { | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  |     let macArr = [] | ||||||
|  |     for (let i = 1; i < str.length; i++) { | ||||||
|  |         if (i % 2 === 1) { | ||||||
|  |             macArr.push(str.substring(i - 1, i + 1)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     let mac = '' | ||||||
|  |     for (let i = 0; i < macArr.length; i++) { | ||||||
|  |         mac = mac + macArr[i] | ||||||
|  |         if (i < macArr.length - 1) { | ||||||
|  |             mac = mac + ':' | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return mac | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getInt8RSSI (str) { | ||||||
|  |     return loraWANV2DataFormat(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getInt (str) { | ||||||
|  |     return parseInt(str, 16) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getEventStatus (str) { | ||||||
|  |     // return getInt(str) | ||||||
|  |     let bitStr = getByteArray(str) | ||||||
|  |     let bitArr = [] | ||||||
|  |     for (let i = 0; i < bitStr.length; i++) { | ||||||
|  |         bitArr[i] = bitStr.substring(i, i + 1) | ||||||
|  |     } | ||||||
|  |     bitArr = bitArr.reverse() | ||||||
|  |     let event = [] | ||||||
|  |     for (let i = 0; i < bitArr.length; i++) { | ||||||
|  |         if (bitArr[i] !== '1') { | ||||||
|  |             continue | ||||||
|  |         } | ||||||
|  |         switch (i){ | ||||||
|  |             case 0: | ||||||
|  |                 event.push({id:1, eventName:"Start moving event."}) | ||||||
|  |                 break | ||||||
|  |             case 1: | ||||||
|  |                 event.push({id:2, eventName:"End movement event."}) | ||||||
|  |                 break | ||||||
|  |             case 2: | ||||||
|  |                 event.push({id:3, eventName:"Motionless event."}) | ||||||
|  |                 break | ||||||
|  |             case 3: | ||||||
|  |                 event.push({id:4, eventName:"Shock event."}) | ||||||
|  |                 break | ||||||
|  |             case 4: | ||||||
|  |                 event.push({id:5, eventName:"Temperature event."}) | ||||||
|  |                 break | ||||||
|  |             case 5: | ||||||
|  |                 event.push({id:6, eventName:"Light event."}) | ||||||
|  |                 break | ||||||
|  |             case 6: | ||||||
|  |                 event.push({id:7, eventName:"SOS event."}) | ||||||
|  |                 break | ||||||
|  |             case 7: | ||||||
|  |                 event.push({id:8, eventName:"Press once event."}) | ||||||
|  |                 break | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return event | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getByteArray (str) { | ||||||
|  |     let bytes = [] | ||||||
|  |     for (let i = 0; i < str.length; i += 2) { | ||||||
|  |         bytes.push(str.substring(i, i + 2)) | ||||||
|  |     } | ||||||
|  |     return toBinary(bytes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getWorkingMode (workingMode) { | ||||||
|  |     return getInt(workingMode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getPositioningStrategy (strategy) { | ||||||
|  |     return getInt(strategy) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getUTCTimestamp(str){ | ||||||
|  |     return parseInt(loraWANV2PositiveDataFormat(str)) * 1000 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function loraWANV2PositiveDataFormat (str, divisor = 1) { | ||||||
|  |     let strReverse = bigEndianTransform(str) | ||||||
|  |     let str2 = toBinary(strReverse) | ||||||
|  |     return parseInt(str2, 2) / divisor | ||||||
|  | } | ||||||
| @ -1,12 +1,18 @@ | |||||||
|  | # Database  | ||||||
| DB_NAME="" | DB_NAME="" | ||||||
| DB_USER="" | DB_USER="" | ||||||
| DB_PASSWORD="" | DB_PASSWORD="" | ||||||
| DB_HOST="" | DB_HOST="localhost" | ||||||
| DB_DIALECT="" | DB_DIALECT="mariadb" | ||||||
| DB_PORT="" | DB_PORT="3306" | ||||||
| WIGLE_TOKEN="" |  | ||||||
|  | # Server | ||||||
|  | PORT="3000" #Port the server runs on, match with your reverse proxy config | ||||||
|  |  | ||||||
|  | # TTN Webhook | ||||||
|  | WEBHOOK_TOKEN="" #Token that is placed a the TTN Webhook auth | ||||||
|  |  | ||||||
|  | # Wigle API | ||||||
|  | WIGLE_TOKEN="" # Go to account and generate token "Encoded for use" | ||||||
| WIGLE_BASE_URL="https://api.wigle.net" | WIGLE_BASE_URL="https://api.wigle.net" | ||||||
| WIGLE_NETWORK_SEARCH="/api/v2/network/search" | WIGLE_NETWORK_SEARCH="/api/v2/network/search" | ||||||
| GET_LOCATION_WIFI_MAX_AGE=1209600000 # 14 Tage in Millisekunden (14 * 24 * 60 * 60 * 1000) |  | ||||||
| GET_LOCATION_WIFI_MAX=10000           |  | ||||||
| GET_LOCATION_WIFI_PRIMITIVE=true   |  | ||||||
							
								
								
									
										169
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										169
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -7,14 +7,13 @@ | |||||||
|     "": { |     "": { | ||||||
|       "name": "locationhub", |       "name": "locationhub", | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "license": "ISC", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "cors": "^2.8.5", |         "cors": "^2.8.5", | ||||||
|         "dotenv": "^16.4.7", |         "dotenv": "^16.4.7", | ||||||
|         "express": "^4.21.2", |         "express": "^4.21.2", | ||||||
|         "http-status-codes": "^2.3.0", |         "http-status-codes": "^2.3.0", | ||||||
|         "mariadb": "^3.4.0", |         "mariadb": "^3.4.0", | ||||||
|         "memoizee": "^0.4.17", |  | ||||||
|         "reflect-metadata": "^0.2.2", |         "reflect-metadata": "^0.2.2", | ||||||
|         "sequelize": "^6.37.5", |         "sequelize": "^6.37.5", | ||||||
|         "swagger-jsdoc": "^6.2.8", |         "swagger-jsdoc": "^6.2.8", | ||||||
| @ -24,7 +23,6 @@ | |||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@types/express": "^5.0.0", |         "@types/express": "^5.0.0", | ||||||
|         "@types/memoizee": "^0.4.11", |  | ||||||
|         "@types/node": "^22.10.2", |         "@types/node": "^22.10.2", | ||||||
|         "nodemon": "^3.1.9", |         "nodemon": "^3.1.9", | ||||||
|         "ts-node": "^10.9.2", |         "ts-node": "^10.9.2", | ||||||
| @ -232,13 +230,6 @@ | |||||||
|       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", |       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/memoizee": { |  | ||||||
|       "version": "0.4.11", |  | ||||||
|       "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.11.tgz", |  | ||||||
|       "integrity": "sha512-2gyorIBZu8GoDr9pYjROkxWWcFtHCquF7TVbN2I+/OvgZhnIGQS0vX5KJz4lXNKb8XOSfxFOSG5OLru1ESqLUg==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT" |  | ||||||
|     }, |  | ||||||
|     "node_modules/@types/mime": { |     "node_modules/@types/mime": { | ||||||
|       "version": "1.3.5", |       "version": "1.3.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", |       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", | ||||||
| @ -582,19 +573,6 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "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": { |     "node_modules/debug": { | ||||||
|       "version": "2.6.9", |       "version": "2.6.9", | ||||||
|       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", |       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", | ||||||
| @ -731,79 +709,12 @@ | |||||||
|         "node": ">= 0.4" |         "node": ">= 0.4" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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/escape-html": { |     "node_modules/escape-html": { | ||||||
|       "version": "1.0.3", |       "version": "1.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", |       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", | ||||||
|       "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", |       "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", | ||||||
|       "license": "MIT" |       "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/esutils": { |     "node_modules/esutils": { | ||||||
|       "version": "2.0.3", |       "version": "2.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", |       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", | ||||||
| @ -822,16 +733,6 @@ | |||||||
|         "node": ">= 0.6" |         "node": ">= 0.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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/express": { |     "node_modules/express": { | ||||||
|       "version": "4.21.2", |       "version": "4.21.2", | ||||||
|       "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", |       "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", | ||||||
| @ -878,15 +779,6 @@ | |||||||
|         "url": "https://opencollective.com/express" |         "url": "https://opencollective.com/express" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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/fill-range": { |     "node_modules/fill-range": { | ||||||
|       "version": "7.1.1", |       "version": "7.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", |       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", | ||||||
| @ -1192,12 +1084,6 @@ | |||||||
|         "node": ">=0.12.0" |         "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/js-yaml": { |     "node_modules/js-yaml": { | ||||||
|       "version": "4.1.0", |       "version": "4.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", |       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", | ||||||
| @ -1240,15 +1126,6 @@ | |||||||
|       "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", |       "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", | ||||||
|       "license": "ISC" |       "license": "ISC" | ||||||
|     }, |     }, | ||||||
|     "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/make-error": { |     "node_modules/make-error": { | ||||||
|       "version": "1.3.6", |       "version": "1.3.6", | ||||||
|       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", |       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", | ||||||
| @ -1302,25 +1179,6 @@ | |||||||
|         "node": ">= 0.6" |         "node": ">= 0.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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/merge-descriptors": { |     "node_modules/merge-descriptors": { | ||||||
|       "version": "1.0.3", |       "version": "1.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", |       "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", | ||||||
| @ -1420,12 +1278,6 @@ | |||||||
|         "node": ">= 0.6" |         "node": ">= 0.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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/nodemon": { |     "node_modules/nodemon": { | ||||||
|       "version": "3.1.9", |       "version": "3.1.9", | ||||||
|       "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", |       "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", | ||||||
| @ -2021,19 +1873,6 @@ | |||||||
|         "express": ">=4.0.0 || >=5.0.0-beta" |         "express": ">=4.0.0 || >=5.0.0-beta" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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/to-regex-range": { |     "node_modules/to-regex-range": { | ||||||
|       "version": "5.0.1", |       "version": "5.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", |       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||||
| @ -2134,12 +1973,6 @@ | |||||||
|         "node": ">= 6.0.0" |         "node": ">= 6.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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-is": { |     "node_modules/type-is": { | ||||||
|       "version": "1.6.18", |       "version": "1.6.18", | ||||||
|       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", |       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", | ||||||
|  | |||||||
| @ -10,14 +10,13 @@ | |||||||
|   }, |   }, | ||||||
|   "keywords": [], |   "keywords": [], | ||||||
|   "author": "Hendrik Schutter, Philipp Schweizer", |   "author": "Hendrik Schutter, Philipp Schweizer", | ||||||
|   "license": "ISC", |   "license": "MIT", | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@types/express": "^5.0.0", |     "@types/express": "^5.0.0", | ||||||
|     "@types/node": "^22.10.2", |     "@types/node": "^22.10.2", | ||||||
|     "nodemon": "^3.1.9", |     "nodemon": "^3.1.9", | ||||||
|     "ts-node": "^10.9.2", |     "ts-node": "^10.9.2", | ||||||
|     "typescript": "^5.7.2", |     "typescript": "^5.7.2" | ||||||
|     "@types/memoizee": "^0.4.11" |  | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "cors": "^2.8.5", |     "cors": "^2.8.5", | ||||||
| @ -25,7 +24,6 @@ | |||||||
|     "express": "^4.21.2", |     "express": "^4.21.2", | ||||||
|     "http-status-codes": "^2.3.0", |     "http-status-codes": "^2.3.0", | ||||||
|     "mariadb": "^3.4.0", |     "mariadb": "^3.4.0", | ||||||
|     "memoizee": "^0.4.17", |  | ||||||
|     "reflect-metadata": "^0.2.2", |     "reflect-metadata": "^0.2.2", | ||||||
|     "sequelize": "^6.37.5", |     "sequelize": "^6.37.5", | ||||||
|     "swagger-jsdoc": "^6.2.8", |     "swagger-jsdoc": "^6.2.8", | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								server/scripts/locationhub.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								server/scripts/locationhub.service
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | [Unit] | ||||||
|  | Description=LocationHub | ||||||
|  | After=network.target systemd-networkd-wait-online.service mysqld.service | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | Type=simple | ||||||
|  | User=locationhub | ||||||
|  | WorkingDirectory=/home/locationhub/git/LocationHub/server/ | ||||||
|  | ExecStart=/usr/bin/npm run dev | ||||||
|  | Restart=on-failure | ||||||
|  | StandardOutput=append:/var/log/LocationHub.log | ||||||
|  | StandardError=append:/var/log/LocationHub.log | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
|  | ``` | ||||||
|  | Activate Systemd Job | ||||||
|  | ``` | ||||||
|  | systemctl daemon-reload | ||||||
|  | systemctl enable locationhub.service | ||||||
|  | systemctl start locationhub.service | ||||||
|  | systemctl status locationhub.service | ||||||
| @ -9,11 +9,16 @@ import json | |||||||
| import argparse | import argparse | ||||||
| import random | import random | ||||||
|  |  | ||||||
| def send_post_request(uri, data): | def send_post_request(uri, data, token): | ||||||
|  |     headers = { | ||||||
|  |         "Authorization": f"Bearer {token}", | ||||||
|  |         "Content-Type": "application/json", | ||||||
|  |     } | ||||||
|     try: |     try: | ||||||
|         requests.post(uri, json=data, timeout=1) |         response = requests.post(uri, json=data, timeout=1, headers=headers) | ||||||
|  |         print("Return code: " + str(response.status_code)) | ||||||
|     except requests.exceptions.RequestException as e: |     except requests.exceptions.RequestException as e: | ||||||
|         pass |         print(e) | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     parser = argparse.ArgumentParser( |     parser = argparse.ArgumentParser( | ||||||
| @ -24,6 +29,11 @@ def main(): | |||||||
|         type=str, |         type=str, | ||||||
|         help="The URI to send POST requests to (e.g., http://127.0.0.1:8080/api)", |         help="The URI to send POST requests to (e.g., http://127.0.0.1:8080/api)", | ||||||
|     ) |     ) | ||||||
|  |     parser.add_argument( | ||||||
|  |         "token", | ||||||
|  |         type=str, | ||||||
|  |         help="Bearer authorization token)", | ||||||
|  |     ) | ||||||
|     parser.add_argument( |     parser.add_argument( | ||||||
|         "directory", |         "directory", | ||||||
|         type=str, |         type=str, | ||||||
| @ -46,7 +56,7 @@ def main(): | |||||||
|                 try: |                 try: | ||||||
|                     data = json.load(file) |                     data = json.load(file) | ||||||
|                     print(f"Sending {args.directory} to {args.uri}") |                     print(f"Sending {args.directory} to {args.uri}") | ||||||
|                     send_post_request(args.uri, data) |                     send_post_request(args.uri, data, args.token) | ||||||
|                 except json.JSONDecodeError as e: |                 except json.JSONDecodeError as e: | ||||||
|                     print(f"Error reading {args.directory}: {e}") |                     print(f"Error reading {args.directory}: {e}") | ||||||
|             return |             return | ||||||
| @ -67,7 +77,7 @@ def main(): | |||||||
|                 try: |                 try: | ||||||
|                     data = json.load(file) |                     data = json.load(file) | ||||||
|                     print(f"Sending {filename} to {args.uri}") |                     print(f"Sending {filename} to {args.uri}") | ||||||
|                     send_post_request(args.uri, data) |                     send_post_request(args.uri, data, args.token) | ||||||
|                 except json.JSONDecodeError as e: |                 except json.JSONDecodeError as e: | ||||||
|                     print(f"Error reading {filename}: {e}") |                     print(f"Error reading {filename}: {e}") | ||||||
|  |  | ||||||
| @ -78,7 +88,7 @@ def main(): | |||||||
|                 try: |                 try: | ||||||
|                     data = json.load(file) |                     data = json.load(file) | ||||||
|                     print(f"Sending {filename} to {args.uri}") |                     print(f"Sending {filename} to {args.uri}") | ||||||
|                     send_post_request(args.uri, data) |                     send_post_request(args.uri, data, args.token) | ||||||
|                     input("Press Enter to send the next file...") |                     input("Press Enter to send the next file...") | ||||||
|                 except json.JSONDecodeError as e: |                 except json.JSONDecodeError as e: | ||||||
|                     print(f"Error reading {filename}: {e}") |                     print(f"Error reading {filename}: {e}") | ||||||
| @ -91,11 +101,10 @@ def main(): | |||||||
|                 try: |                 try: | ||||||
|                     data = json.load(file) |                     data = json.load(file) | ||||||
|                     print(f"Sending {filename} to {args.uri}") |                     print(f"Sending {filename} to {args.uri}") | ||||||
|                     send_post_request(args.uri, data) |                     send_post_request(args.uri, data, args.token) | ||||||
|                     input("Press Enter to send another random file...") |                     input("Press Enter to send another random file...") | ||||||
|                 except json.JSONDecodeError as e: |                 except json.JSONDecodeError as e: | ||||||
|                     print(f"Error reading {filename}: {e}") |                     print(f"Error reading {filename}: {e}") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     main() |     main() | ||||||
|  | |||||||
							
								
								
									
										88
									
								
								server/scripts/wigle-dummy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								server/scripts/wigle-dummy.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | """ Author:                     Hendrik Schutter, mail@hendrikschutter.com | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  | import os | ||||||
|  | import json | ||||||
|  | import argparse | ||||||
|  | import random | ||||||
|  | import http.server | ||||||
|  | import json | ||||||
|  | from urllib.parse import urlparse, parse_qs | ||||||
|  |  | ||||||
|  | port = 8000 | ||||||
|  |  | ||||||
|  | def generateDummyResponse(netid): | ||||||
|  |     response_payload = { | ||||||
|  |         "success": True, | ||||||
|  |         "totalResults": 1, | ||||||
|  |         "first": 0, | ||||||
|  |         "last": 0, | ||||||
|  |         "resultCount": 1, | ||||||
|  |         "results": [ | ||||||
|  |             { | ||||||
|  |                 "trilat": random.uniform(-90, 90), | ||||||
|  |                 "trilong": random.uniform(-180, 180), | ||||||
|  |                 "ssid": "Wifi-Name", | ||||||
|  |                 "qos": 0, | ||||||
|  |                 "transid": "string", | ||||||
|  |                 "firsttime": "2025-01-02T16:48:28.368Z", | ||||||
|  |                 "lasttime": "2025-01-02T16:48:28.368Z", | ||||||
|  |                 "lastupdt": "2025-01-02T16:48:28.368Z", | ||||||
|  |                 "netid": netid, | ||||||
|  |                 "name": "string", | ||||||
|  |                 "type": "string", | ||||||
|  |                 "comment": "string", | ||||||
|  |                 "wep": "string", | ||||||
|  |                 "bcninterval": 0, | ||||||
|  |                 "freenet": "string", | ||||||
|  |                 "dhcp": "string", | ||||||
|  |                 "paynet": "string", | ||||||
|  |                 "userfound": False, | ||||||
|  |                 "channel": 0, | ||||||
|  |                 "rcois": "string", | ||||||
|  |                 "encryption": "none", | ||||||
|  |                 "country": "string", | ||||||
|  |                 "region": "string", | ||||||
|  |                 "road": "string", | ||||||
|  |                 "city": "string", | ||||||
|  |                 "housenumber": "string", | ||||||
|  |                 "postalcode": "string", | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |         "searchAfter": "string", | ||||||
|  |         "search_after": 0, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return response_payload | ||||||
|  |  | ||||||
|  | class SimpleHTTPRequestHandler(http.server.BaseHTTPRequestHandler): | ||||||
|  |     def do_GET(self): | ||||||
|  |         # Parse the URL and query parameters | ||||||
|  |         parsed_url = urlparse(self.path) | ||||||
|  |         if parsed_url.path == "/api/v2/network/search": | ||||||
|  |             query_params = parse_qs(parsed_url.query) | ||||||
|  |             netid = query_params.get("netid", [""])[0] | ||||||
|  |  | ||||||
|  |             # Send response headers | ||||||
|  |             self.send_response(200) | ||||||
|  |             self.send_header("Content-Type", "application/json") | ||||||
|  |             self.end_headers() | ||||||
|  |  | ||||||
|  |             # Send the JSON response | ||||||
|  |             self.wfile.write(json.dumps(generateDummyResponse(netid)).encode("utf-8")) | ||||||
|  |         else: | ||||||
|  |             # Handle 404 Not Found | ||||||
|  |             self.send_response(404) | ||||||
|  |             self.end_headers() | ||||||
|  |             self.wfile.write(b"Not Found") | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     server = http.server.HTTPServer(("127.0.0.1", port), SimpleHTTPRequestHandler) | ||||||
|  |     print(f"Server running on http://127.0.0.1:{port}/api/v2/network/search'...") | ||||||
|  |     server.serve_forever() | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
| @ -5,10 +5,8 @@ CREATE TABLE IF NOT EXISTS lp_ttn_end_device_uplinks ( | |||||||
|     dev_eui VARCHAR(255), |     dev_eui VARCHAR(255), | ||||||
|     join_eui VARCHAR(255), |     join_eui VARCHAR(255), | ||||||
|     dev_addr VARCHAR(255), |     dev_addr VARCHAR(255), | ||||||
|     received_at_utc DATE, |     received_at_utc DATE NOT NULL, | ||||||
|     battery NUMERIC, |     battery NUMERIC, | ||||||
|     latitude DOUBLE, |  | ||||||
|     longitude DOUBLE, |  | ||||||
|     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
| ); | ); | ||||||
| @ -18,13 +16,31 @@ CREATE TABLE IF NOT EXISTS wifi_scan ( | |||||||
|     lp_ttn_end_device_uplinks_id UUID, |     lp_ttn_end_device_uplinks_id UUID, | ||||||
|     mac VARCHAR(255), |     mac VARCHAR(255), | ||||||
|     rssi NUMERIC, |     rssi NUMERIC, | ||||||
|     latitude DOUBLE, |     scanned_at_utc TIMESTAMP NOT NULL, | ||||||
|     longitude DOUBLE, |  | ||||||
|     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||||||
|     FOREIGN KEY (lp_ttn_end_device_uplinks_id) REFERENCES lp_ttn_end_device_uplinks(lp_ttn_end_device_uplinks_id) |     FOREIGN KEY (lp_ttn_end_device_uplinks_id) REFERENCES lp_ttn_end_device_uplinks(lp_ttn_end_device_uplinks_id) | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | CREATE TABLE IF NOT EXISTS wifi_location ( | ||||||
|  |     mac VARCHAR(255) PRIMARY KEY, | ||||||
|  |     latitude DOUBLE, | ||||||
|  |     longitude DOUBLE, | ||||||
|  |     request_limit_exceeded boolean NOT NULL, | ||||||
|  |     location_not_resolvable boolean NOT NULL, | ||||||
|  |     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | CREATE TABLE IF NOT EXISTS wifi_location_history ( | ||||||
|  |     wifi_location_history_id UUID PRIMARY KEY, | ||||||
|  |     mac VARCHAR(255), | ||||||
|  |     latitude DOUBLE NOT NULL, | ||||||
|  |     longitude DOUBLE NOT NULL, | ||||||
|  |     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  | ); | ||||||
|  |  | ||||||
| CREATE TABLE IF NOT EXISTS ttn_gateway_reception ( | CREATE TABLE IF NOT EXISTS ttn_gateway_reception ( | ||||||
|     ttn_gateway_reception_id UUID PRIMARY KEY, |     ttn_gateway_reception_id UUID PRIMARY KEY, | ||||||
|     lp_ttn_end_device_uplinks_id UUID, |     lp_ttn_end_device_uplinks_id UUID, | ||||||
| @ -46,6 +62,9 @@ CREATE TABLE IF NOT EXISTS location ( | |||||||
|     wifi_longitude DOUBLE, |     wifi_longitude DOUBLE, | ||||||
|     gnss_latitude DOUBLE, |     gnss_latitude DOUBLE, | ||||||
|     gnss_longitude DOUBLE, |     gnss_longitude DOUBLE, | ||||||
|  |     ttn_gw_latitude DOUBLE, | ||||||
|  |     ttn_gw_longitude DOUBLE, | ||||||
|  |     gnss_location_at_utc TIMESTAMP, | ||||||
|     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |     created_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |     updated_at_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||||||
|     FOREIGN KEY (lp_ttn_end_device_uplinks_id) REFERENCES lp_ttn_end_device_uplinks(lp_ttn_end_device_uplinks_id) |     FOREIGN KEY (lp_ttn_end_device_uplinks_id) REFERENCES lp_ttn_end_device_uplinks(lp_ttn_end_device_uplinks_id) | ||||||
|  | |||||||
| @ -1,12 +1,10 @@ | |||||||
| import express, { Request, Response } from "express"; | import express, { Request, Response } from "express"; | ||||||
|  | import { StatusCodes } from "http-status-codes"; | ||||||
| import { container } from "tsyringe"; | import { container } from "tsyringe"; | ||||||
| import { domainEventEmitter } from "../config/eventEmitter"; | import { authenticateHeader } from "../middleware/authentificationMiddleware"; | ||||||
| import { |  | ||||||
|   TtnMessageReceivedEvent, |  | ||||||
|   TtnMessageReceivedEventName, |  | ||||||
| } from "../event/ttnMessageReceivedEvent"; |  | ||||||
| import { validateData } from "../middleware/validationMiddleware"; | import { validateData } from "../middleware/validationMiddleware"; | ||||||
| import { TtnMessage } from "../models/ttnMessage"; | import { TtnMessage } from "../models/ttnMessage"; | ||||||
|  | import { LocationService } from "../services/locationService"; | ||||||
| import { LpTtnEndDeviceUplinksService } from "../services/lpTtnEndDeviceUplinksService"; | import { LpTtnEndDeviceUplinksService } from "../services/lpTtnEndDeviceUplinksService"; | ||||||
| import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; | import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; | ||||||
| import { WifiScanService } from "../services/wifiScanService"; | import { WifiScanService } from "../services/wifiScanService"; | ||||||
| @ -19,15 +17,17 @@ const ttnGatewayReceptionService = container.resolve( | |||||||
|   TtnGatewayReceptionService |   TtnGatewayReceptionService | ||||||
| ); | ); | ||||||
| const wifiScanService = container.resolve(WifiScanService); | const wifiScanService = container.resolve(WifiScanService); | ||||||
|  | const locationService = container.resolve(LocationService); | ||||||
|  |  | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
|  |  | ||||||
| router.post( | router.post( | ||||||
|   "/webhook", |   "/webhook", | ||||||
|   validateData(ttnMessageValidator), |   [authenticateHeader, validateData(ttnMessageValidator)], | ||||||
|   async (req: Request, res: Response) => { |   async (req: Request, res: Response) => { | ||||||
|     try { |     try { | ||||||
|       const message = req.body as TtnMessage; |       const message = req.body as TtnMessage; | ||||||
|  |  | ||||||
|       const { lp_ttn_end_device_uplinks_id } = |       const { lp_ttn_end_device_uplinks_id } = | ||||||
|         await lpTtnEndDeviceUplinksService.createUplink({ |         await lpTtnEndDeviceUplinksService.createUplink({ | ||||||
|           device_id: message.end_device_ids.device_id, |           device_id: message.end_device_ids.device_id, | ||||||
| @ -40,14 +40,31 @@ router.post( | |||||||
|           battery: message.uplink_message.decoded_payload?.messages[0].find( |           battery: message.uplink_message.decoded_payload?.messages[0].find( | ||||||
|             (e) => e.type === "Battery" |             (e) => e.type === "Battery" | ||||||
|           )?.measurementValue, |           )?.measurementValue, | ||||||
|           latitude: message.uplink_message.decoded_payload?.messages[0].find( |  | ||||||
|             (e) => e.type === "Latitude" |  | ||||||
|           )?.measurementValue, |  | ||||||
|           longitude: message.uplink_message.decoded_payload?.messages[0].find( |  | ||||||
|             (e) => e.type === "Longitude" |  | ||||||
|           )?.measurementValue, |  | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |       const messageData = message.uplink_message.decoded_payload?.messages[0]; | ||||||
|  |  | ||||||
|  |       const latitudeData = messageData?.find((e) => e.type === "Latitude"); | ||||||
|  |       const longitudeData = messageData?.find((e) => e.type === "Longitude"); | ||||||
|  |  | ||||||
|  |       const gnnsLocation = { | ||||||
|  |         latitude: latitudeData?.measurementValue, | ||||||
|  |         longitude: longitudeData?.measurementValue, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const gnssTimestamp = { | ||||||
|  |         timestamp: latitudeData?.timestamp | ||||||
|  |           ? new Date(latitudeData.timestamp) | ||||||
|  |           : longitudeData?.timestamp | ||||||
|  |             ? new Date(longitudeData.timestamp) | ||||||
|  |             : undefined, | ||||||
|  |       }; | ||||||
|  |       const wifiTimestamp = (() => { | ||||||
|  |         const messages = message.uplink_message.decoded_payload?.messages?.[0]; | ||||||
|  |         const wifiScan = messages?.find((e: { type: string }) => e.type === "Wi-Fi Scan"); | ||||||
|  |         return wifiScan?.timestamp ? new Date(wifiScan.timestamp) : undefined; | ||||||
|  |       })(); | ||||||
|  |  | ||||||
|       const wifiScans = |       const wifiScans = | ||||||
|         message.uplink_message.decoded_payload?.messages[0] |         message.uplink_message.decoded_payload?.messages[0] | ||||||
|           .find((e) => e.type === "Wi-Fi Scan") |           .find((e) => e.type === "Wi-Fi Scan") | ||||||
| @ -55,6 +72,7 @@ router.post( | |||||||
|             lp_ttn_end_device_uplinks_id, |             lp_ttn_end_device_uplinks_id, | ||||||
|             mac: w.mac, |             mac: w.mac, | ||||||
|             rssi: w.rssi, |             rssi: w.rssi, | ||||||
|  |             scanned_at_timestamp: wifiTimestamp, | ||||||
|           })) ?? []; |           })) ?? []; | ||||||
|  |  | ||||||
|       const ttnGatewayReceptions = message.uplink_message.rx_metadata.map( |       const ttnGatewayReceptions = message.uplink_message.rx_metadata.map( | ||||||
| @ -63,36 +81,48 @@ router.post( | |||||||
|           gateway_id: g.gateway_ids.gateway_id, |           gateway_id: g.gateway_ids.gateway_id, | ||||||
|           eui: g.gateway_ids.eui, |           eui: g.gateway_ids.eui, | ||||||
|           rssi: g.rssi, |           rssi: g.rssi, | ||||||
|           latitude: g.location.latitude, |           latitude: g.location?.latitude, | ||||||
|           longitude: g.location.longitude, |           longitude: g.location?.longitude, | ||||||
|           altitude: g.location.altitude, |           altitude: g.location?.altitude, | ||||||
|         }) |         }) | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       const event: TtnMessageReceivedEvent = { |       const createDatabaseEntries = async () => { | ||||||
|         lp_ttn_end_device_uplinks_id, |         const [wifiResults, gatewayResults] = await Promise.all([ | ||||||
|         wifis: wifiScans.map((w) => ({ mac: w.mac, rssi: w.rssi })), |  | ||||||
|         ttnGateways: ttnGatewayReceptions.map((g) => ({ |  | ||||||
|           rssi: g.rssi, |  | ||||||
|           altitude: g.altitude, |  | ||||||
|           latitude: g.latitude, |  | ||||||
|           longitude: g.longitude, |  | ||||||
|         })), |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       domainEventEmitter.emit(TtnMessageReceivedEventName, event); |  | ||||||
|  |  | ||||||
|       await Promise.all([ |  | ||||||
|           wifiScanService.createWifiScans(wifiScans), |           wifiScanService.createWifiScans(wifiScans), | ||||||
|         ttnGatewayReceptionService.createGatewayReceptions( |           ttnGatewayReceptionService.filterAndInsertGatewayReception( | ||||||
|             ttnGatewayReceptions |             ttnGatewayReceptions | ||||||
|           ), |           ), | ||||||
|         ]); |         ]); | ||||||
|  |  | ||||||
|       res.status(200); |         locationService.createLocationFromTriangulation({ | ||||||
|  |           lp_ttn_end_device_uplinks_id, | ||||||
|  |           wifi: wifiResults.map(({ mac, rssi }) => ({ | ||||||
|  |             mac, | ||||||
|  |             rssi, | ||||||
|  |           })), | ||||||
|  |           ttn_gw: gatewayResults.map(({ latitude, longitude, rssi }) => ({ | ||||||
|  |             latitude, | ||||||
|  |             longitude, | ||||||
|  |             rssi, | ||||||
|  |           })), | ||||||
|  |           gnss: | ||||||
|  |             gnnsLocation.latitude && gnnsLocation.longitude | ||||||
|  |               ? { | ||||||
|  |                 latitude: gnnsLocation.latitude, | ||||||
|  |                 longitude: gnnsLocation.longitude, | ||||||
|  |               } | ||||||
|  |               : undefined, | ||||||
|  |           gnss_timestamp: gnssTimestamp.timestamp, | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |       createDatabaseEntries().then(); | ||||||
|  |       res.status(StatusCodes.OK).send(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.log(error); |       console.log(error); | ||||||
|       res.status(500).json({ error: "Error creating uplink" }); |       res | ||||||
|  |         .status(StatusCodes.INTERNAL_SERVER_ERROR) | ||||||
|  |         .json({ error: "Error creating uplink" }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import express, { Request, Response } from "express"; | import express, { Request, Response } from "express"; | ||||||
| import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; |  | ||||||
| import { container } from "tsyringe"; | import { container } from "tsyringe"; | ||||||
|  | import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService"; | ||||||
|  |  | ||||||
| const ttnGatewayReceptionService = container.resolve( | const ttnGatewayReceptionService = container.resolve( | ||||||
|   TtnGatewayReceptionService |   TtnGatewayReceptionService | ||||||
| @ -35,7 +35,7 @@ router.get("/:id", async (req: Request, res: Response) => { | |||||||
| router.post("/", async (req: Request, res: Response) => { | router.post("/", async (req: Request, res: Response) => { | ||||||
|   try { |   try { | ||||||
|     const newGatewayReception = |     const newGatewayReception = | ||||||
|       await ttnGatewayReceptionService.createGatewayReception(req.body); |       await ttnGatewayReceptionService.createTtnGatewayReception(req.body); | ||||||
|     res.status(201).json(newGatewayReception); |     res.status(201).json(newGatewayReception); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     res.status(500).json({ error: "Error creating gateway reception" }); |     res.status(500).json({ error: "Error creating gateway reception" }); | ||||||
| @ -46,7 +46,10 @@ router.put("/:id", async (req: Request, res: Response) => { | |||||||
|   try { |   try { | ||||||
|     const { id } = req.params; |     const { id } = req.params; | ||||||
|     const updatedGatewayReception = |     const updatedGatewayReception = | ||||||
|       await ttnGatewayReceptionService.updateGatewayReception(id, req.body); |       await ttnGatewayReceptionService.updateGatewayReception({ | ||||||
|  |         ...req.body, | ||||||
|  |         ttn_gateway_reception_id: id, | ||||||
|  |       }); | ||||||
|     if (!updatedGatewayReception) { |     if (!updatedGatewayReception) { | ||||||
|       res.status(404).json({ error: "Gateway reception not found" }); |       res.status(404).json({ error: "Gateway reception not found" }); | ||||||
|       return; |       return; | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								server/src/controller/wifiLocationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/src/controller/wifiLocationController.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | import express, { Request, Response } from "express"; | ||||||
|  | import { container } from "tsyringe"; | ||||||
|  | import { WifiLocationService } from "../services/wifiLocationService"; | ||||||
|  |  | ||||||
|  | const wifiLocationService = container.resolve(WifiLocationService); | ||||||
|  | const router = express.Router(); | ||||||
|  |  | ||||||
|  | router.get("/", async (req: Request, res: Response) => { | ||||||
|  |   try { | ||||||
|  |     const wifiLocations = await wifiLocationService.getAllWifiLocations(); | ||||||
|  |     res.status(200).json(wifiLocations); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.log(error); | ||||||
|  |     res.status(500).json({ error: "Error retrieving wifi location" }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | router.delete("/:id", async (req: Request, res: Response) => { | ||||||
|  |   try { | ||||||
|  |     const { id } = req.params; | ||||||
|  |     const deleted = await wifiLocationService.deleteWifiLocation(id); | ||||||
|  |     if (!deleted) { | ||||||
|  |       res.status(404).json({ error: "Wifi Location not found" }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     res.status(204).send(); | ||||||
|  |   } catch (error) { | ||||||
|  |     res.status(500).json({ error: "Error deleting wifi location" }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default router; | ||||||
							
								
								
									
										35
									
								
								server/src/controller/wifiLocationHistoryController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								server/src/controller/wifiLocationHistoryController.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | import express, { Request, Response } from "express"; | ||||||
|  | import { container } from "tsyringe"; | ||||||
|  | import { WifiLocationHistoryService } from "../services/wifiLocationHistoryService"; | ||||||
|  |  | ||||||
|  | const wifiLocationHistoryService = container.resolve( | ||||||
|  |   WifiLocationHistoryService | ||||||
|  | ); | ||||||
|  | const router = express.Router(); | ||||||
|  |  | ||||||
|  | router.get("/", async (req: Request, res: Response) => { | ||||||
|  |   try { | ||||||
|  |     const wifiLocationHistory = | ||||||
|  |       await wifiLocationHistoryService.getAllWifiLocationHistories(); | ||||||
|  |     res.status(200).json(wifiLocationHistory); | ||||||
|  |   } catch (error) { | ||||||
|  |     res.status(500).json({ error: "Error retrieving wifi location history" }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | router.get("/:id", async (req: Request, res: Response) => { | ||||||
|  |   try { | ||||||
|  |     const { id } = req.params; | ||||||
|  |     const wifiLocationHistory = | ||||||
|  |       await wifiLocationHistoryService.getWifiLocationHistoryById(id); | ||||||
|  |     if (!wifiLocationHistory) { | ||||||
|  |       res.status(404).json({ error: "Wifi location history not found" }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     res.status(200).json(wifiLocationHistory); | ||||||
|  |   } catch (error) { | ||||||
|  |     res.status(500).json({ error: "Error retrieving wifi location history" }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default router; | ||||||
| @ -40,7 +40,10 @@ router.post("/", async (req: Request, res: Response) => { | |||||||
| router.put("/:id", async (req: Request, res: Response) => { | router.put("/:id", async (req: Request, res: Response) => { | ||||||
|   try { |   try { | ||||||
|     const { id } = req.params; |     const { id } = req.params; | ||||||
|     const updatedWifiScan = await wifiScanService.updateWifiScan(id, req.body); |     const updatedWifiScan = await wifiScanService.updateWifiScan({ | ||||||
|  |       ...req.body, | ||||||
|  |       wifi_scan_id: id, | ||||||
|  |     }); | ||||||
|     if (!updatedWifiScan) { |     if (!updatedWifiScan) { | ||||||
|       res.status(404).json({ error: "Wifi scan not found" }); |       res.status(404).json({ error: "Wifi scan not found" }); | ||||||
|       return; |       return; | ||||||
|  | |||||||
| @ -1,14 +0,0 @@ | |||||||
| export const TtnMessageReceivedEventName = "TtnMessageReceived"; |  | ||||||
| export type TtnMessageReceivedEvent = { |  | ||||||
|   lp_ttn_end_device_uplinks_id: string; |  | ||||||
|   wifis: { |  | ||||||
|     mac: string; |  | ||||||
|     rssi: number; |  | ||||||
|   }[]; |  | ||||||
|   ttnGateways: { |  | ||||||
|     rssi: number; |  | ||||||
|     latitude: number; |  | ||||||
|     longitude: number; |  | ||||||
|     altitude: number; |  | ||||||
|   }[]; |  | ||||||
| }; |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| import { domainEventEmitter } from "../config/eventEmitter"; |  | ||||||
| import { |  | ||||||
|   TtnMessageReceivedEvent, |  | ||||||
|   TtnMessageReceivedEventName, |  | ||||||
| } from "../event/ttnMessageReceivedEvent"; |  | ||||||
|  |  | ||||||
| domainEventEmitter.on( |  | ||||||
|   TtnMessageReceivedEventName, |  | ||||||
|   async (event: TtnMessageReceivedEvent) => { |  | ||||||
|     console.log(event); |  | ||||||
|     // TODO Hendrik 🚀 |  | ||||||
|   } |  | ||||||
| ); |  | ||||||
| @ -1,13 +1,14 @@ | |||||||
| import dotenv from "dotenv"; | import dotenv from "dotenv"; | ||||||
| import express from "express"; | import express from "express"; | ||||||
| import "reflect-metadata"; | import "reflect-metadata"; | ||||||
| import "./eventHandler/ttnMessageReceivedEventHandler"; |  | ||||||
| const cors = require("cors"); | const cors = require("cors"); | ||||||
|  |  | ||||||
| import locationRoutes from "./controller/locationController"; | import locationRoutes from "./controller/locationController"; | ||||||
| import lpTtnEndDeviceUplinksRoutes from "./controller/lpTtnEndDeviceUplinksController"; | import lpTtnEndDeviceUplinksRoutes from "./controller/lpTtnEndDeviceUplinksController"; | ||||||
| import ttnRoutes from "./controller/ttnController"; | import ttnRoutes from "./controller/ttnController"; | ||||||
| import ttnGatewayReceptionRoutes from "./controller/ttnGatewayReceptionController"; | import ttnGatewayReceptionRoutes from "./controller/ttnGatewayReceptionController"; | ||||||
|  | import wifiLocationRoutes from "./controller/wifiLocationController"; | ||||||
|  | import wifiLocationHistoryRoutes from "./controller/wifiLocationHistoryController"; | ||||||
| import wifiScanRoutes from "./controller/wifiScanController"; | import wifiScanRoutes from "./controller/wifiScanController"; | ||||||
|  |  | ||||||
| dotenv.config(); | dotenv.config(); | ||||||
| @ -19,11 +20,13 @@ app.use(cors()); | |||||||
| app.use(express.json()); | app.use(express.json()); | ||||||
|  |  | ||||||
| app.use("/api/lp-ttn-end-device-uplinks", lpTtnEndDeviceUplinksRoutes); | app.use("/api/lp-ttn-end-device-uplinks", lpTtnEndDeviceUplinksRoutes); | ||||||
| app.use("/api/wifi-scans", wifiScanRoutes); |  | ||||||
| app.use("/api/ttn-gateway-receptions", ttnGatewayReceptionRoutes); | app.use("/api/ttn-gateway-receptions", ttnGatewayReceptionRoutes); | ||||||
|  | app.use("/api/wifi-location-history", wifiLocationHistoryRoutes); | ||||||
|  | app.use("/api/wifi-location", wifiLocationRoutes); | ||||||
|  | app.use("/api/wifi-scans", wifiScanRoutes); | ||||||
| app.use("/api/locations", locationRoutes); | app.use("/api/locations", locationRoutes); | ||||||
| app.use("/api/ttn", ttnRoutes); | app.use("/api/ttn", ttnRoutes); | ||||||
|  |  | ||||||
| app.listen(PORT, () => { | app.listen(PORT, () => { | ||||||
|   console.log(`🚀 Server läuft auf http://localhost:${PORT}`); |   console.log(`🚀 Server runs here: http://localhost:${PORT}`); | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										41
									
								
								server/src/middleware/authentificationMiddleware.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								server/src/middleware/authentificationMiddleware.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | import { NextFunction, Request, Response } from "express"; | ||||||
|  | import { StatusCodes } from "http-status-codes"; | ||||||
|  |  | ||||||
|  | const validateBearerToken = (authorizationHeader: string | undefined): boolean => { | ||||||
|  |     if (!authorizationHeader) { | ||||||
|  |         console.log("Authorization header is missing!"); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const token = authorizationHeader.split(' ')[1]; // Extract token after 'Bearer' | ||||||
|  |     if (!token) { | ||||||
|  |         console.log("Bearer token is missing!"); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (token !== process.env.WEBHOOK_TOKEN) { | ||||||
|  |         console.log("Bearer token is incorrect!"); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return true; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function authenticateHeader(req: Request, res: Response, next: NextFunction) { | ||||||
|  |     try { | ||||||
|  |         const authorizationHeader = req.headers['authorization']; | ||||||
|  |  | ||||||
|  |         if (!validateBearerToken(authorizationHeader as string)) { | ||||||
|  |             res.status(StatusCodes.UNAUTHORIZED).json({ error: "Authentication failed" }); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         //console.log("Bearer token is correct!"); | ||||||
|  |         next(); | ||||||
|  |     } catch (error) { | ||||||
|  |         res.status(StatusCodes.INTERNAL_SERVER_ERROR) | ||||||
|  |             .json({ error: "Internal Server Error" }); | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -8,6 +8,9 @@ export class Location extends Model { | |||||||
|   public wifi_longitude!: number; |   public wifi_longitude!: number; | ||||||
|   public gnss_latitude!: number; |   public gnss_latitude!: number; | ||||||
|   public gnss_longitude!: number; |   public gnss_longitude!: number; | ||||||
|  |   public ttn_gw_latitude!: number; | ||||||
|  |   public ttn_gw_longitude!: number; | ||||||
|  |   public gnss_location_at_utc!: Date; | ||||||
|   public created_at_utc!: Date; |   public created_at_utc!: Date; | ||||||
|   public updated_at_utc!: Date; |   public updated_at_utc!: Date; | ||||||
| } | } | ||||||
| @ -18,27 +21,30 @@ Location.init( | |||||||
|       type: DataTypes.UUID, |       type: DataTypes.UUID, | ||||||
|       defaultValue: DataTypes.UUIDV4, |       defaultValue: DataTypes.UUIDV4, | ||||||
|       primaryKey: true, |       primaryKey: true, | ||||||
|       allowNull: false, |  | ||||||
|     }, |     }, | ||||||
|     lp_ttn_end_device_uplinks_id: { |     lp_ttn_end_device_uplinks_id: { | ||||||
|       type: DataTypes.UUID, |       type: DataTypes.UUID, | ||||||
|       allowNull: false, |  | ||||||
|     }, |     }, | ||||||
|     wifi_latitude: { |     wifi_latitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     wifi_longitude: { |     wifi_longitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     gnss_latitude: { |     gnss_latitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     gnss_longitude: { |     gnss_longitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |     }, | ||||||
|  |     ttn_gw_latitude: { | ||||||
|  |       type: DataTypes.NUMBER, | ||||||
|  |     }, | ||||||
|  |     ttn_gw_longitude: { | ||||||
|  |       type: DataTypes.NUMBER, | ||||||
|  |     }, | ||||||
|  |     gnss_location_at_utc: { | ||||||
|  |       type: DataTypes.DATE, | ||||||
|     }, |     }, | ||||||
|     created_at_utc: { |     created_at_utc: { | ||||||
|       type: DataTypes.DATE, |       type: DataTypes.DATE, | ||||||
|  | |||||||
| @ -10,8 +10,6 @@ export class LpTtnEndDeviceUplinks extends Model { | |||||||
|   public dev_addr!: string; |   public dev_addr!: string; | ||||||
|   public received_at_utc!: Date; |   public received_at_utc!: Date; | ||||||
|   public battery!: number; |   public battery!: number; | ||||||
|   public latitude!: number; |  | ||||||
|   public longitude!: number; |  | ||||||
|   public created_at_utc!: Date; |   public created_at_utc!: Date; | ||||||
|   public updated_at_utc!: Date; |   public updated_at_utc!: Date; | ||||||
| } | } | ||||||
| @ -30,35 +28,21 @@ LpTtnEndDeviceUplinks.init( | |||||||
|     }, |     }, | ||||||
|     application_ids: { |     application_ids: { | ||||||
|       type: DataTypes.STRING, |       type: DataTypes.STRING, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     dev_eui: { |     dev_eui: { | ||||||
|       type: DataTypes.STRING, |       type: DataTypes.STRING, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     join_eui: { |     join_eui: { | ||||||
|       type: DataTypes.STRING, |       type: DataTypes.STRING, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     dev_addr: { |     dev_addr: { | ||||||
|       type: DataTypes.STRING, |       type: DataTypes.STRING, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     received_at_utc: { |     received_at_utc: { | ||||||
|       type: DataTypes.DATE, |       type: DataTypes.DATE, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     battery: { |     battery: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |  | ||||||
|     latitude: { |  | ||||||
|       type: DataTypes.NUMBER, |  | ||||||
|       allowNull: true, |  | ||||||
|     }, |  | ||||||
|     longitude: { |  | ||||||
|       type: DataTypes.NUMBER, |  | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     created_at_utc: { |     created_at_utc: { | ||||||
|       type: DataTypes.DATE, |       type: DataTypes.DATE, | ||||||
|  | |||||||
| @ -32,23 +32,18 @@ TtnGatewayReception.init( | |||||||
|     }, |     }, | ||||||
|     eui: { |     eui: { | ||||||
|       type: DataTypes.STRING, |       type: DataTypes.STRING, | ||||||
|       allowNull: false, |  | ||||||
|     }, |     }, | ||||||
|     rssi: { |     rssi: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     latitude: { |     latitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     longitude: { |     longitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     altitude: { |     altitude: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     created_at_utc: { |     created_at_utc: { | ||||||
|       type: DataTypes.DATE, |       type: DataTypes.DATE, | ||||||
|  | |||||||
| @ -6,14 +6,14 @@ export interface TtnMessage { | |||||||
|     }; |     }; | ||||||
|     dev_eui: string; |     dev_eui: string; | ||||||
|     join_eui: string; |     join_eui: string; | ||||||
|     dev_addr: string; |     dev_addr?: string; | ||||||
|   }; |   }; | ||||||
|   correlation_ids: string[]; |   correlation_ids: string[]; | ||||||
|   received_at: string; |   received_at: string; | ||||||
|   uplink_message: { |   uplink_message: { | ||||||
|     session_key_id: string; |     session_key_id?: string; | ||||||
|     f_port?: number; |     f_port?: number; | ||||||
|     f_cnt: number; |     f_cnt?: number; | ||||||
|     frm_payload?: string; |     frm_payload?: string; | ||||||
|     decoded_payload?: { |     decoded_payload?: { | ||||||
|       err: number; |       err: number; | ||||||
| @ -22,8 +22,8 @@ export interface TtnMessage { | |||||||
|           { |           { | ||||||
|             measurementId: "4200"; |             measurementId: "4200"; | ||||||
|             measurementValue: any[]; |             measurementValue: any[]; | ||||||
|             motionId: number; |             motionId?: number; | ||||||
|             timestamp: number; |             timestamp?: number; | ||||||
|             type: "Event Status"; |             type: "Event Status"; | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
| @ -32,29 +32,29 @@ export interface TtnMessage { | |||||||
|               mac: string; |               mac: string; | ||||||
|               rssi: number; |               rssi: number; | ||||||
|             }[]; |             }[]; | ||||||
|             motionId: number; |             motionId?: number; | ||||||
|             timestamp: number; |             timestamp?: number; | ||||||
|             type: "Wi-Fi Scan"; |             type: "Wi-Fi Scan"; | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             measurementId: "3000"; |             measurementId: "3000"; | ||||||
|             measurementValue: number; |             measurementValue: number; | ||||||
|             motionId: number; |             motionId?: number; | ||||||
|             timestamp: number; |             timestamp?: number; | ||||||
|             type: "Battery"; |             type: "Battery"; | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             measurementId: "4197"; |             measurementId: "4197"; | ||||||
|             measurementValue: number; |             measurementValue: number; | ||||||
|             motionId: number; |             motionId?: number; | ||||||
|             timestamp: number; |             timestamp?: number; | ||||||
|             type: "Longitude"; |             type: "Longitude"; | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             measurementId: "4198"; |             measurementId: "4198"; | ||||||
|             measurementValue: number; |             measurementValue: number; | ||||||
|             motionId: number; |             motionId?: number; | ||||||
|             timestamp: number; |             timestamp?: number; | ||||||
|             type: "Latitude"; |             type: "Latitude"; | ||||||
|           } |           } | ||||||
|         ] |         ] | ||||||
| @ -67,44 +67,44 @@ export interface TtnMessage { | |||||||
|         gateway_id: string; |         gateway_id: string; | ||||||
|         eui?: string; |         eui?: string; | ||||||
|       }; |       }; | ||||||
|       time: string; |       time?: string; | ||||||
|       timestamp?: number; |       timestamp?: number; | ||||||
|       rssi: number; |       rssi: number; | ||||||
|       channel_rssi: number; |       channel_rssi: number; | ||||||
|       snr: number; |       snr?: number; | ||||||
|       location: { |       location?: { | ||||||
|         latitude: number; |         latitude: number; | ||||||
|         longitude: number; |         longitude: number; | ||||||
|         altitude: number; |         altitude?: number; | ||||||
|         source?: string; |         source?: string; | ||||||
|       }; |       }; | ||||||
|       uplink_token: string; |       uplink_token?: string; | ||||||
|       channel_index?: number; |       channel_index?: number; | ||||||
|       received_at: string; |       received_at?: string; | ||||||
|     }[]; |     }[]; | ||||||
|     settings: { |     settings: { | ||||||
|       data_rate: { |       data_rate: { | ||||||
|         lora: { |         lora: { | ||||||
|           bandwidth: number; |           bandwidth: number; | ||||||
|           spreading_factor: number; |           spreading_factor: number; | ||||||
|           coding_rate: string; |           coding_rate?: string; | ||||||
|         }; |         }; | ||||||
|       }; |       }; | ||||||
|       frequency: string; |       frequency: string; | ||||||
|       timestamp?: number; |       timestamp?: number; | ||||||
|       time?: Date; |       time?: Date; | ||||||
|     }; |     }; | ||||||
|     received_at: Date; |     received_at?: Date; | ||||||
|     confirmed?: boolean; |     confirmed?: boolean; | ||||||
|     consumed_airtime: string; |     consumed_airtime?: string; | ||||||
|     version_ids: { |     version_ids?: { | ||||||
|       brand_id: string; |       brand_id: string; | ||||||
|       model_id: string; |       model_id: string; | ||||||
|       hardware_version: string; |       hardware_version: string; | ||||||
|       firmware_version: string; |       firmware_version: string; | ||||||
|       band_id: string; |       band_id: string; | ||||||
|     }; |     }; | ||||||
|     network_ids: { |     network_ids?: { | ||||||
|       net_id: string; |       net_id: string; | ||||||
|       ns_id: string; |       ns_id: string; | ||||||
|       tenant_id: string; |       tenant_id: string; | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								server/src/models/wifiLocation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								server/src/models/wifiLocation.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | import { DataTypes, Model } from "sequelize"; | ||||||
|  | import { sequelize } from "../database/database"; | ||||||
|  |  | ||||||
|  | export class WifiLocation extends Model { | ||||||
|  |   public mac!: string; | ||||||
|  |   public latitude!: number; | ||||||
|  |   public longitude!: number; | ||||||
|  |   public request_limit_exceeded!: boolean; | ||||||
|  |   public location_not_resolvable!: boolean; | ||||||
|  |   public created_at_utc!: Date; | ||||||
|  |   public updated_at_utc!: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | WifiLocation.init( | ||||||
|  |   { | ||||||
|  |     mac: { | ||||||
|  |       type: DataTypes.STRING, | ||||||
|  |       primaryKey: true, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     latitude: { | ||||||
|  |       type: DataTypes.NUMBER, | ||||||
|  |     }, | ||||||
|  |     longitude: { | ||||||
|  |       type: DataTypes.NUMBER, | ||||||
|  |     }, | ||||||
|  |     request_limit_exceeded: { | ||||||
|  |       type: DataTypes.BOOLEAN, | ||||||
|  |       defaultValue: false, | ||||||
|  |     }, | ||||||
|  |     location_not_resolvable: { | ||||||
|  |       type: DataTypes.BOOLEAN, | ||||||
|  |       defaultValue: false, | ||||||
|  |     }, | ||||||
|  |     created_at_utc: { | ||||||
|  |       type: DataTypes.DATE, | ||||||
|  |       defaultValue: DataTypes.NOW, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     updated_at_utc: { | ||||||
|  |       type: DataTypes.DATE, | ||||||
|  |       defaultValue: DataTypes.NOW, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     sequelize, | ||||||
|  |     modelName: "WifiLocation", | ||||||
|  |     tableName: "wifi_location", | ||||||
|  |     timestamps: false, | ||||||
|  |   } | ||||||
|  | ); | ||||||
							
								
								
									
										50
									
								
								server/src/models/wifiLocationHistory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								server/src/models/wifiLocationHistory.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | |||||||
|  | import { DataTypes, Model } from "sequelize"; | ||||||
|  | import { sequelize } from "../database/database"; | ||||||
|  |  | ||||||
|  | export class WifiLocationHistory extends Model { | ||||||
|  |   public wifi_location_history_id!: string; | ||||||
|  |   public mac!: string; | ||||||
|  |   public latitude!: number; | ||||||
|  |   public longitude!: number; | ||||||
|  |   public created_at_utc!: Date; | ||||||
|  |   public updated_at_utc!: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | WifiLocationHistory.init( | ||||||
|  |   { | ||||||
|  |     wifi_location_history_id: { | ||||||
|  |       type: DataTypes.UUID, | ||||||
|  |       defaultValue: DataTypes.UUIDV4, | ||||||
|  |       primaryKey: true, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     mac: { | ||||||
|  |       type: DataTypes.STRING, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     latitude: { | ||||||
|  |       type: DataTypes.NUMBER, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     longitude: { | ||||||
|  |       type: DataTypes.NUMBER, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     created_at_utc: { | ||||||
|  |       type: DataTypes.DATE, | ||||||
|  |       defaultValue: DataTypes.NOW, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |     updated_at_utc: { | ||||||
|  |       type: DataTypes.DATE, | ||||||
|  |       defaultValue: DataTypes.NOW, | ||||||
|  |       allowNull: false, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     sequelize, | ||||||
|  |     modelName: "WifiLocationHistory", | ||||||
|  |     tableName: "wifi_location_history", | ||||||
|  |     timestamps: false, | ||||||
|  |   } | ||||||
|  | ); | ||||||
| @ -6,8 +6,7 @@ export class WifiScan extends Model { | |||||||
|   public wifi_scan_id!: string; |   public wifi_scan_id!: string; | ||||||
|   public mac!: string; |   public mac!: string; | ||||||
|   public rssi!: number; |   public rssi!: number; | ||||||
|   public latitude!: number; |   public scanned_at_utc!: Date; | ||||||
|   public longitude!: number; |  | ||||||
|   public created_at_utc!: Date; |   public created_at_utc!: Date; | ||||||
|   public updated_at_utc!: Date; |   public updated_at_utc!: Date; | ||||||
| } | } | ||||||
| @ -30,15 +29,12 @@ WifiScan.init( | |||||||
|     }, |     }, | ||||||
|     rssi: { |     rssi: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.NUMBER, | ||||||
|       allowNull: true, |       allowNull: false, | ||||||
|     }, |     }, | ||||||
|     latitude: { |     scanned_at_utc: { | ||||||
|       type: DataTypes.NUMBER, |       type: DataTypes.DATE, | ||||||
|       allowNull: true, |       defaultValue: DataTypes.NOW, | ||||||
|     }, |       allowNull: false, | ||||||
|     longitude: { |  | ||||||
|       type: DataTypes.NUMBER, |  | ||||||
|       allowNull: true, |  | ||||||
|     }, |     }, | ||||||
|     created_at_utc: { |     created_at_utc: { | ||||||
|       type: DataTypes.DATE, |       type: DataTypes.DATE, | ||||||
|  | |||||||
| @ -1,4 +1,7 @@ | |||||||
| import memoizee from "memoizee"; | interface WigleApiResonse { | ||||||
|  |   response?: WifiLocationResponse, | ||||||
|  |   status_code: number, | ||||||
|  | } | ||||||
|  |  | ||||||
| interface WifiLocationResponse { | interface WifiLocationResponse { | ||||||
|   success: boolean; |   success: boolean; | ||||||
| @ -43,25 +46,26 @@ interface Result { | |||||||
|  |  | ||||||
| export const getLocationForWifi = async ( | export const getLocationForWifi = async ( | ||||||
|   mac: string |   mac: string | ||||||
| ): Promise<WifiLocationResponse | undefined> => { | ): Promise<WigleApiResonse | undefined> => { | ||||||
|   try { |   try { | ||||||
|     const url = `${process.env.WIGLE_BASE_URL!}${process.env |     const url = `${process.env.WIGLE_BASE_URL!}${process.env | ||||||
|       .WIGLE_NETWORK_SEARCH!}?netid=${encodeURIComponent(mac)}`; |       .WIGLE_NETWORK_SEARCH!}?netid=${encodeURIComponent(mac)}`; | ||||||
|  |  | ||||||
|     const response = await fetch(url, { |     const response = await fetch(url, { | ||||||
|       method: "GET", |       method: "GET", | ||||||
|       headers: { |       headers: { | ||||||
|         "Content-Type": "application/json", |         "Content-Type": "application/json", | ||||||
|         Cookie: `auth=${process.env.WIGLE_TOKEN}`, |         Authorization: `Basic ${process.env.WIGLE_TOKEN}`, | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     return await response.json(); |  | ||||||
|  |     if (response.ok) { | ||||||
|  |       return { status_code: response.status, response: await response.json() }; | ||||||
|  |     } | ||||||
|  |     console.log(response.status); | ||||||
|  |     return { status_code: response.status }; | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error("Fehler beim Aufruf des Services:", error); |     console.error("Error during call of API wigle.net:", error); | ||||||
|  |     return undefined; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const getLocationForWifiMemoized = memoizee(getLocationForWifi, { |  | ||||||
|   maxAge: Number(process.env.GET_LOCATION_WIFI_MAX_AGE), |  | ||||||
|   max: Number(process.env.GET_LOCATION_WIFI_MAX), |  | ||||||
|   primitive: process.env.GET_LOCATION_WIFI_PRIMITIVE === "true", |  | ||||||
| }); |  | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								server/src/repositories/wifiLocationHistoryRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/src/repositories/wifiLocationHistoryRepository.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | import { Attributes, FindOptions } from "sequelize"; | ||||||
|  | import { injectable } from "tsyringe"; | ||||||
|  | import { WifiLocationHistory } from "../models/wifiLocationHistory"; | ||||||
|  |  | ||||||
|  | @injectable() | ||||||
|  | export class WifiLocationHistoryRepository { | ||||||
|  |   public async findAll() { | ||||||
|  |     return await WifiLocationHistory.findAll(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async findOne(options?: FindOptions<Attributes<WifiLocationHistory>>) { | ||||||
|  |     return await WifiLocationHistory.findOne(options); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async findById(id: string) { | ||||||
|  |     return await WifiLocationHistory.findByPk(id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async create(data: Partial<WifiLocationHistory>) { | ||||||
|  |     return await WifiLocationHistory.create(data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async createMany(data: Partial<WifiLocationHistory>[]) { | ||||||
|  |     return await WifiLocationHistory.bulkCreate(data); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								server/src/repositories/wifiLocationRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								server/src/repositories/wifiLocationRepository.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | import { Attributes, FindOptions } from "sequelize"; | ||||||
|  | import { injectable } from "tsyringe"; | ||||||
|  | import { WifiLocation } from "../models/wifiLocation"; | ||||||
|  |  | ||||||
|  | @injectable() | ||||||
|  | export class WifiLocationRepository { | ||||||
|  |   public async findAll(options?: FindOptions<Attributes<WifiLocation>>) { | ||||||
|  |     return await WifiLocation.findAll(options); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async findById(id: string) { | ||||||
|  |     return await WifiLocation.findByPk(id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async create(data: Partial<WifiLocation>) { | ||||||
|  |     return await WifiLocation.create(data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async createMany(data: Partial<WifiLocation>[]) { | ||||||
|  |     return await WifiLocation.bulkCreate(data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async update(id: string, data: Partial<WifiLocation>) { | ||||||
|  |     const wifiScan = await this.findById(id); | ||||||
|  |     if (wifiScan) { | ||||||
|  |       return await wifiScan.update(data); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async delete(id: string) { | ||||||
|  |     const wifiScan = await this.findById(id); | ||||||
|  |     if (wifiScan) { | ||||||
|  |       await wifiScan.destroy(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,12 +1,53 @@ | |||||||
| import { inject, injectable } from "tsyringe"; | import { inject, injectable } from "tsyringe"; | ||||||
| import { Location } from "../models/location"; | import { Location } from "../models/location"; | ||||||
| import { LocationRepository } from "../repositories/locationRepository"; | import { LocationRepository } from "../repositories/locationRepository"; | ||||||
|  | import { WifiLocationService } from "./wifiLocationService"; | ||||||
|  |  | ||||||
|  | interface CreateLocationParams { | ||||||
|  |   lp_ttn_end_device_uplinks_id: string; | ||||||
|  |   wifi?: Coordinates; | ||||||
|  |   gnss?: Coordinates; | ||||||
|  |   ttn_gw?: Coordinates; | ||||||
|  |   gnss_timestamp?: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface CreateLocationTriangulationParams { | ||||||
|  |   lp_ttn_end_device_uplinks_id: string; | ||||||
|  |   wifi: { | ||||||
|  |     mac: string; | ||||||
|  |     rssi: number; | ||||||
|  |   }[]; | ||||||
|  |   ttn_gw: LocationSignal[]; | ||||||
|  |   gnss?: Coordinates; | ||||||
|  |   gnss_timestamp?: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface LocationSignal extends Coordinates { | ||||||
|  |   rssi: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface Coordinates { | ||||||
|  |   latitude: number; | ||||||
|  |   longitude: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface UpdateTtnGatewayReceptionParams { | ||||||
|  |   ttn_gateway_reception_id: string; | ||||||
|  |   gateway_id?: string; | ||||||
|  |   eui?: string; | ||||||
|  |   rssi?: number; | ||||||
|  |   latitude?: number; | ||||||
|  |   longitude?: number; | ||||||
|  |   altitude?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
| @injectable() | @injectable() | ||||||
| export class LocationService { | export class LocationService { | ||||||
|   constructor( |   constructor( | ||||||
|     @inject(LocationRepository) |     @inject(LocationRepository) | ||||||
|     private repository: LocationRepository |     private repository: LocationRepository, | ||||||
|  |     @inject(WifiLocationService) | ||||||
|  |     private wifiLocationService: WifiLocationService | ||||||
|   ) { } |   ) { } | ||||||
|  |  | ||||||
|   public async getAllLocations() { |   public async getAllLocations() { | ||||||
| @ -17,8 +58,34 @@ export class LocationService { | |||||||
|     return this.repository.findById(id); |     return this.repository.findById(id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async createLocation(data: Partial<Location>) { |   public async createLocation(data: CreateLocationParams) { | ||||||
|     return this.repository.create(data); |     return this.repository.create({ | ||||||
|  |       lp_ttn_end_device_uplinks_id: data.lp_ttn_end_device_uplinks_id, | ||||||
|  |       wifi_latitude: data.wifi?.latitude, | ||||||
|  |       wifi_longitude: data.wifi?.longitude, | ||||||
|  |       ttn_gw_latitude: data.ttn_gw?.latitude, | ||||||
|  |       ttn_gw_longitude: data.ttn_gw?.longitude, | ||||||
|  |       gnss_latitude: data.gnss?.latitude, | ||||||
|  |       gnss_longitude: data.gnss?.longitude, | ||||||
|  |       gnss_location_at_utc: data.gnss_timestamp, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async createLocationFromTriangulation( | ||||||
|  |     data: CreateLocationTriangulationParams | ||||||
|  |   ) { | ||||||
|  |     const wifi_location = this.calculateVirtualLocation( | ||||||
|  |       await this.enrichWifiObjectWithLocation(data.wifi) | ||||||
|  |     ); | ||||||
|  |     const gateway_location = this.calculateVirtualLocation(data.ttn_gw); | ||||||
|  |  | ||||||
|  |     return this.createLocation({ | ||||||
|  |       lp_ttn_end_device_uplinks_id: data.lp_ttn_end_device_uplinks_id, | ||||||
|  |       wifi: wifi_location, | ||||||
|  |       ttn_gw: gateway_location, | ||||||
|  |       gnss: data.gnss, | ||||||
|  |       gnss_timestamp: data.gnss_timestamp, | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async updateLocation(id: string, data: Partial<Location>) { |   public async updateLocation(id: string, data: Partial<Location>) { | ||||||
| @ -28,4 +95,48 @@ export class LocationService { | |||||||
|   public async deleteLocation(id: string) { |   public async deleteLocation(id: string) { | ||||||
|     return this.repository.delete(id); |     return this.repository.delete(id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async enrichWifiObjectWithLocation( | ||||||
|  |     wifis: CreateLocationTriangulationParams["wifi"] | ||||||
|  |   ) { | ||||||
|  |     const enrichedWifi = await Promise.all( | ||||||
|  |       wifis.map(async (wifi) => { | ||||||
|  |         const location = await this.wifiLocationService.getWifiLocationByMac( | ||||||
|  |           wifi.mac | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return location?.latitude !== undefined && | ||||||
|  |           location.longitude !== undefined | ||||||
|  |           ? { | ||||||
|  |             rssi: wifi.rssi, | ||||||
|  |             latitude: location.latitude, | ||||||
|  |             longitude: location.longitude, | ||||||
|  |           } | ||||||
|  |           : null; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return enrichedWifi.filter((wifi) => wifi !== null); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private calculateVirtualLocation(locations: LocationSignal[]) { | ||||||
|  |     if (locations.length === 0) return undefined; | ||||||
|  |  | ||||||
|  |     const { totalWeight, weightedLatitude, weightedLongitude } = | ||||||
|  |       locations.reduce( | ||||||
|  |         (acc, { latitude, longitude, rssi }) => { | ||||||
|  |           const weight = 1 / Math.abs(rssi); | ||||||
|  |           acc.totalWeight += weight; | ||||||
|  |           acc.weightedLatitude += latitude * weight; | ||||||
|  |           acc.weightedLongitude += longitude * weight; | ||||||
|  |           return acc; | ||||||
|  |         }, | ||||||
|  |         { totalWeight: 0, weightedLatitude: 0, weightedLongitude: 0 } | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       latitude: weightedLatitude / totalWeight, | ||||||
|  |       longitude: weightedLongitude / totalWeight, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,26 @@ | |||||||
| import { inject, injectable } from "tsyringe"; | import { inject, injectable } from "tsyringe"; | ||||||
| import { TtnGatewayReception } from "../models/ttnGatewayReception"; |  | ||||||
| import { TtnGatewayReceptionRepository } from "../repositories/ttnGatewayReceptionRepository"; | import { TtnGatewayReceptionRepository } from "../repositories/ttnGatewayReceptionRepository"; | ||||||
|  |  | ||||||
|  | interface CreateTtnGatewayReceptionParams { | ||||||
|  |   lp_ttn_end_device_uplinks_id: string; | ||||||
|  |   gateway_id: string; | ||||||
|  |   eui?: string; | ||||||
|  |   rssi?: number; | ||||||
|  |   latitude?: number; | ||||||
|  |   longitude?: number; | ||||||
|  |   altitude?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface UpdateTtnGatewayReceptionParams { | ||||||
|  |   ttn_gateway_reception_id: string; | ||||||
|  |   gateway_id?: string; | ||||||
|  |   eui?: string; | ||||||
|  |   rssi?: number; | ||||||
|  |   latitude?: number; | ||||||
|  |   longitude?: number; | ||||||
|  |   altitude?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
| @injectable() | @injectable() | ||||||
| export class TtnGatewayReceptionService { | export class TtnGatewayReceptionService { | ||||||
|   constructor( |   constructor( | ||||||
| @ -17,19 +36,24 @@ export class TtnGatewayReceptionService { | |||||||
|     return this.repository.findById(id); |     return this.repository.findById(id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async createGatewayReception(data: Partial<TtnGatewayReception>) { |   public async createTtnGatewayReception( | ||||||
|  |     data: CreateTtnGatewayReceptionParams | ||||||
|  |   ) { | ||||||
|  |     if (data.latitude !== undefined && data.longitude !== undefined) | ||||||
|       return this.repository.create(data); |       return this.repository.create(data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async createGatewayReceptions(data: Partial<TtnGatewayReception>[]) { |   public async filterAndInsertGatewayReception( | ||||||
|     return this.repository.createMany(data); |     data: CreateTtnGatewayReceptionParams[] | ||||||
|  |   ) { | ||||||
|  |     const result = await Promise.all( | ||||||
|  |       data.map(async (gateway) => await this.createTtnGatewayReception(gateway)) | ||||||
|  |     ); | ||||||
|  |     return result.filter((gateway) => gateway !== undefined); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async updateGatewayReception( |   public async updateGatewayReception(data: UpdateTtnGatewayReceptionParams) { | ||||||
|     id: string, |     return this.repository.update(data.ttn_gateway_reception_id, data); | ||||||
|     data: Partial<TtnGatewayReception> |  | ||||||
|   ) { |  | ||||||
|     return this.repository.update(id, data); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async deleteGatewayReception(id: string) { |   public async deleteGatewayReception(id: string) { | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								server/src/services/wifiLocationHistoryService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								server/src/services/wifiLocationHistoryService.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | import { inject, injectable } from "tsyringe"; | ||||||
|  | import { WifiLocationHistoryRepository } from "../repositories/wifiLocationHistoryRepository"; | ||||||
|  |  | ||||||
|  | interface CreateWifiLocationHistoryParams { | ||||||
|  |   mac: string; | ||||||
|  |   latitude: number; | ||||||
|  |   longitude: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface UpdateWifiLocationHistoryParams { | ||||||
|  |   mac: string; | ||||||
|  |   latitude: number; | ||||||
|  |   longitude: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @injectable() | ||||||
|  | export class WifiLocationHistoryService { | ||||||
|  |   constructor( | ||||||
|  |     @inject(WifiLocationHistoryRepository) | ||||||
|  |     private repository: WifiLocationHistoryRepository | ||||||
|  |   ) {} | ||||||
|  |  | ||||||
|  |   public async getAllWifiLocationHistories() { | ||||||
|  |     return this.repository.findAll(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async getWifiLocationHistoryById(id: string) { | ||||||
|  |     return this.repository.findById(id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async createWifiLocationHistory( | ||||||
|  |     data: CreateWifiLocationHistoryParams | ||||||
|  |   ) { | ||||||
|  |     const existingEntry = await this.repository.findOne({ | ||||||
|  |       where: { | ||||||
|  |         mac: data.mac, | ||||||
|  |       }, | ||||||
|  |       order: [["updated_at_utc", "DESC"]], | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       !existingEntry || | ||||||
|  |       existingEntry.latitude !== data.latitude || | ||||||
|  |       existingEntry.longitude !== data.longitude | ||||||
|  |     ) { | ||||||
|  |       return await this.repository.create(data); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return existingEntry; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										80
									
								
								server/src/services/wifiLocationService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/src/services/wifiLocationService.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | |||||||
|  | import { Op } from "sequelize"; | ||||||
|  | import { inject, injectable } from "tsyringe"; | ||||||
|  | import { getLocationForWifi } from "../proxy/wigle"; | ||||||
|  | import { WifiLocationRepository } from "../repositories/wifiLocationRepository"; | ||||||
|  | import { WifiLocationHistoryService } from "./wifiLocationHistoryService"; | ||||||
|  | import { StatusCodes } from "http-status-codes"; | ||||||
|  |  | ||||||
|  | interface UpdateWifiLocationParams { | ||||||
|  |   mac: string; | ||||||
|  |   latitude: number; | ||||||
|  |   longitude: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @injectable() | ||||||
|  | export class WifiLocationService { | ||||||
|  |   constructor( | ||||||
|  |     @inject(WifiLocationRepository) private repository: WifiLocationRepository, | ||||||
|  |     @inject(WifiLocationHistoryService) | ||||||
|  |     private wifiLocationHistory: WifiLocationHistoryService | ||||||
|  |   ) { } | ||||||
|  |  | ||||||
|  |   public async getAllWifiLocations() { | ||||||
|  |     return this.repository.findAll(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async getAllWifiLocationsByAddresses(macAddresses: string[]) { | ||||||
|  |     return this.repository.findAll({ | ||||||
|  |       where: { mac: { [Op.in]: macAddresses } }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async getWifiLocationByMac(mac: string) { | ||||||
|  |     let wifiLocation = await this.repository.findById(mac); | ||||||
|  |  | ||||||
|  |     if (wifiLocation) return wifiLocation; | ||||||
|  |  | ||||||
|  |     const apiResponse = await getLocationForWifi(mac); | ||||||
|  |  | ||||||
|  |     if (apiResponse == undefined) { | ||||||
|  |       await this.repository.create({ | ||||||
|  |         mac, | ||||||
|  |       }); | ||||||
|  |       return undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (apiResponse.response == undefined) { | ||||||
|  |       const request_limit_exceeded = apiResponse.status_code === StatusCodes.TOO_MANY_REQUESTS; | ||||||
|  |       await this.repository.create({ | ||||||
|  |         mac, | ||||||
|  |         request_limit_exceeded, | ||||||
|  |       }); | ||||||
|  |       return undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (apiResponse.response.totalResults == 0) { | ||||||
|  |       await this.repository.create({ mac, location_not_resolvable: true }); | ||||||
|  |       return undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     wifiLocation = await this.repository.create({ | ||||||
|  |       mac, | ||||||
|  |       latitude: apiResponse.response.results[0].trilat, | ||||||
|  |       longitude: apiResponse.response.results[0].trilong, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await this.wifiLocationHistory.createWifiLocationHistory( | ||||||
|  |       wifiLocation.dataValues | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return wifiLocation; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updateWifiLocation(data: UpdateWifiLocationParams) { | ||||||
|  |     return this.repository.update(data.mac, data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async deleteWifiLocation(id: string) { | ||||||
|  |     return this.repository.delete(id); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,7 +1,20 @@ | |||||||
| import { inject, injectable } from "tsyringe"; | import { inject, injectable } from "tsyringe"; | ||||||
| import { WifiScan } from "../models/wifiScan"; |  | ||||||
| import { WifiScanRepository } from "../repositories/wifiScanRepository"; | import { WifiScanRepository } from "../repositories/wifiScanRepository"; | ||||||
|  |  | ||||||
|  | interface CreateWifiScanParams { | ||||||
|  |   lp_ttn_end_device_uplinks_id: string; | ||||||
|  |   mac: string; | ||||||
|  |   rssi: number; | ||||||
|  |   scanned_at_timestamp?: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface UpdateWifiScanParams { | ||||||
|  |   wifi_scan_id: string; | ||||||
|  |   mac?: string; | ||||||
|  |   rssi?: number; | ||||||
|  |   scanned_at_timestamp?: Date; | ||||||
|  | } | ||||||
|  |  | ||||||
| @injectable() | @injectable() | ||||||
| export class WifiScanService { | export class WifiScanService { | ||||||
|   constructor( |   constructor( | ||||||
| @ -16,16 +29,16 @@ export class WifiScanService { | |||||||
|     return this.repository.findById(id); |     return this.repository.findById(id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async createWifiScan(data: Partial<WifiScan>) { |   public async createWifiScan(data: CreateWifiScanParams) { | ||||||
|       return this.repository.create(data); |       return this.repository.create(data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async createWifiScans(data: Partial<WifiScan>[]) { |   public async createWifiScans(data: CreateWifiScanParams[]) { | ||||||
|     return this.repository.createMany(data); |     return await this.repository.createMany(data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async updateWifiScan(id: string, data: Partial<WifiScan>) { |   public async updateWifiScan(data: UpdateWifiScanParams) { | ||||||
|     return this.repository.update(id, data); |     return this.repository.update(data.wifi_scan_id, data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async deleteWifiScan(id: string) { |   public async deleteWifiScan(id: string) { | ||||||
|  | |||||||
| @ -8,14 +8,14 @@ export const ttnMessageValidator = z.object({ | |||||||
|     }), |     }), | ||||||
|     dev_eui: z.string(), |     dev_eui: z.string(), | ||||||
|     join_eui: z.string(), |     join_eui: z.string(), | ||||||
|     dev_addr: z.string(), |     dev_addr: z.string().optional(), | ||||||
|   }), |   }), | ||||||
|   correlation_ids: z.array(z.string()), |   correlation_ids: z.array(z.string()), | ||||||
|   received_at: z.string(), |   received_at: z.string(), | ||||||
|   uplink_message: z.object({ |   uplink_message: z.object({ | ||||||
|     session_key_id: z.string(), |     session_key_id: z.string().optional(), | ||||||
|     f_port: z.number().optional(), |     f_port: z.number().optional(), | ||||||
|     f_cnt: z.number(), |     f_cnt: z.number().optional(), | ||||||
|     frm_payload: z.string().optional(), |     frm_payload: z.string().optional(), | ||||||
|     decoded_payload: z |     decoded_payload: z | ||||||
|       .object({ |       .object({ | ||||||
| @ -25,8 +25,8 @@ export const ttnMessageValidator = z.object({ | |||||||
|             z.object({ |             z.object({ | ||||||
|               measurementId: z.string(), |               measurementId: z.string(), | ||||||
|               measurementValue: z.union([z.array(z.any()), z.number()]), |               measurementValue: z.union([z.array(z.any()), z.number()]), | ||||||
|               motionId: z.number(), |               motionId: z.number().optional(), | ||||||
|               timestamp: z.number(), |               timestamp: z.number().optional(), | ||||||
|               type: z.string(), |               type: z.string(), | ||||||
|             }) |             }) | ||||||
|           ) |           ) | ||||||
| @ -41,20 +41,22 @@ export const ttnMessageValidator = z.object({ | |||||||
|           gateway_id: z.string(), |           gateway_id: z.string(), | ||||||
|           eui: z.string().optional(), |           eui: z.string().optional(), | ||||||
|         }), |         }), | ||||||
|         time: z.string(), |         time: z.string().optional(), | ||||||
|         timestamp: z.number().optional(), |         timestamp: z.number().optional(), | ||||||
|         rssi: z.number(), |         rssi: z.number(), | ||||||
|         channel_rssi: z.number(), |         channel_rssi: z.number(), | ||||||
|         snr: z.number(), |         snr: z.number().optional(), | ||||||
|         location: z.object({ |         location: z | ||||||
|  |           .object({ | ||||||
|             latitude: z.number(), |             latitude: z.number(), | ||||||
|             longitude: z.number(), |             longitude: z.number(), | ||||||
|           altitude: z.number(), |             altitude: z.number().optional(), | ||||||
|             source: z.string().optional(), |             source: z.string().optional(), | ||||||
|         }), |           }) | ||||||
|         uplink_token: z.string(), |           .optional(), | ||||||
|  |         uplink_token: z.string().optional(), | ||||||
|         channel_index: z.number().optional(), |         channel_index: z.number().optional(), | ||||||
|         received_at: z.string(), |         received_at: z.string().optional(), | ||||||
|       }) |       }) | ||||||
|     ), |     ), | ||||||
|     settings: z.object({ |     settings: z.object({ | ||||||
| @ -62,29 +64,33 @@ export const ttnMessageValidator = z.object({ | |||||||
|         lora: z.object({ |         lora: z.object({ | ||||||
|           bandwidth: z.number(), |           bandwidth: z.number(), | ||||||
|           spreading_factor: z.number(), |           spreading_factor: z.number(), | ||||||
|           coding_rate: z.string(), |           coding_rate: z.string().optional(), | ||||||
|         }), |         }), | ||||||
|       }), |       }), | ||||||
|       frequency: z.string(), |       frequency: z.string(), | ||||||
|       timestamp: z.number().optional(), |       timestamp: z.number().optional(), | ||||||
|       time: z.string().optional(), |       time: z.string().optional(), | ||||||
|     }), |     }), | ||||||
|     received_at: z.string(), |     received_at: z.string().optional(), | ||||||
|     confirmed: z.boolean().optional(), |     confirmed: z.boolean().optional(), | ||||||
|     consumed_airtime: z.string(), |     consumed_airtime: z.string().optional(), | ||||||
|     version_ids: z.object({ |     version_ids: z | ||||||
|  |       .object({ | ||||||
|         brand_id: z.string(), |         brand_id: z.string(), | ||||||
|         model_id: z.string(), |         model_id: z.string(), | ||||||
|         hardware_version: z.string(), |         hardware_version: z.string(), | ||||||
|         firmware_version: z.string(), |         firmware_version: z.string(), | ||||||
|         band_id: z.string(), |         band_id: z.string(), | ||||||
|     }), |       }) | ||||||
|     network_ids: z.object({ |       .optional(), | ||||||
|  |     network_ids: z | ||||||
|  |       .object({ | ||||||
|         net_id: z.string(), |         net_id: z.string(), | ||||||
|         ns_id: z.string(), |         ns_id: z.string(), | ||||||
|         tenant_id: z.string(), |         tenant_id: z.string(), | ||||||
|         cluster_id: z.string(), |         cluster_id: z.string(), | ||||||
|         cluster_address: z.string(), |         cluster_address: z.string(), | ||||||
|     }), |       }) | ||||||
|  |       .optional(), | ||||||
|   }), |   }), | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								webClient/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								webClient/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,26 +0,0 @@ | |||||||
| # Logs |  | ||||||
| logs |  | ||||||
| *.log |  | ||||||
| npm-debug.log* |  | ||||||
| yarn-debug.log* |  | ||||||
| yarn-error.log* |  | ||||||
| pnpm-debug.log* |  | ||||||
| lerna-debug.log* |  | ||||||
|  |  | ||||||
| node_modules |  | ||||||
| dist |  | ||||||
| dist-ssr |  | ||||||
| *.local |  | ||||||
|  |  | ||||||
| # Editor directories and files |  | ||||||
| .vscode/* |  | ||||||
| !.vscode/extensions.json |  | ||||||
| .idea |  | ||||||
| .DS_Store |  | ||||||
| *.suo |  | ||||||
| *.ntvs* |  | ||||||
| *.njsproj |  | ||||||
| *.sln |  | ||||||
| *.sw? |  | ||||||
|  |  | ||||||
| .env |  | ||||||
| @ -1,50 +0,0 @@ | |||||||
| # React + TypeScript + Vite |  | ||||||
|  |  | ||||||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. |  | ||||||
|  |  | ||||||
| Currently, two official plugins are available: |  | ||||||
|  |  | ||||||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh |  | ||||||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh |  | ||||||
|  |  | ||||||
| ## Expanding the ESLint configuration |  | ||||||
|  |  | ||||||
| If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: |  | ||||||
|  |  | ||||||
| - Configure the top-level `parserOptions` property like this: |  | ||||||
|  |  | ||||||
| ```js |  | ||||||
| export default tseslint.config({ |  | ||||||
|   languageOptions: { |  | ||||||
|     // other options... |  | ||||||
|     parserOptions: { |  | ||||||
|       project: ['./tsconfig.node.json', './tsconfig.app.json'], |  | ||||||
|       tsconfigRootDir: import.meta.dirname, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }) |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` |  | ||||||
| - Optionally add `...tseslint.configs.stylisticTypeChecked` |  | ||||||
| - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: |  | ||||||
|  |  | ||||||
| ```js |  | ||||||
| // eslint.config.js |  | ||||||
| import react from 'eslint-plugin-react' |  | ||||||
|  |  | ||||||
| export default tseslint.config({ |  | ||||||
|   // Set the react version |  | ||||||
|   settings: { react: { version: '18.3' } }, |  | ||||||
|   plugins: { |  | ||||||
|     // Add the react plugin |  | ||||||
|     react, |  | ||||||
|   }, |  | ||||||
|   rules: { |  | ||||||
|     // other rules... |  | ||||||
|     // Enable its recommended rules |  | ||||||
|     ...react.configs.recommended.rules, |  | ||||||
|     ...react.configs['jsx-runtime'].rules, |  | ||||||
|   }, |  | ||||||
| }) |  | ||||||
| ``` |  | ||||||
| @ -1,28 +0,0 @@ | |||||||
| import js from '@eslint/js' |  | ||||||
| import globals from 'globals' |  | ||||||
| import reactHooks from 'eslint-plugin-react-hooks' |  | ||||||
| import reactRefresh from 'eslint-plugin-react-refresh' |  | ||||||
| import tseslint from 'typescript-eslint' |  | ||||||
|  |  | ||||||
| export default tseslint.config( |  | ||||||
|   { ignores: ['dist'] }, |  | ||||||
|   { |  | ||||||
|     extends: [js.configs.recommended, ...tseslint.configs.recommended], |  | ||||||
|     files: ['**/*.{ts,tsx}'], |  | ||||||
|     languageOptions: { |  | ||||||
|       ecmaVersion: 2020, |  | ||||||
|       globals: globals.browser, |  | ||||||
|     }, |  | ||||||
|     plugins: { |  | ||||||
|       'react-hooks': reactHooks, |  | ||||||
|       'react-refresh': reactRefresh, |  | ||||||
|     }, |  | ||||||
|     rules: { |  | ||||||
|       ...reactHooks.configs.recommended.rules, |  | ||||||
|       'react-refresh/only-export-components': [ |  | ||||||
|         'warn', |  | ||||||
|         { allowConstantExport: true }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| ) |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| <!doctype html> |  | ||||||
| <html lang="en"> |  | ||||||
|   <head> |  | ||||||
|     <meta charset="UTF-8" /> |  | ||||||
|     <link rel="icon" type="image/svg+xml" href="/vite.svg" /> |  | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |  | ||||||
|     <title>Vite + React + TS</title> |  | ||||||
|   </head> |  | ||||||
|   <body> |  | ||||||
|     <div id="root"></div> |  | ||||||
|     <script type="module" src="/src/main.tsx"></script> |  | ||||||
|   </body> |  | ||||||
| </html> |  | ||||||
							
								
								
									
										4347
									
								
								webClient/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4347
									
								
								webClient/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,40 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "location_hub_ui", |  | ||||||
|   "private": true, |  | ||||||
|   "version": "0.0.0", |  | ||||||
|   "type": "module", |  | ||||||
|   "scripts": { |  | ||||||
|     "dev": "vite", |  | ||||||
|     "build": "tsc -b && vite build", |  | ||||||
|     "lint": "eslint .", |  | ||||||
|     "preview": "vite preview" |  | ||||||
|   }, |  | ||||||
|   "dependencies": { |  | ||||||
|     "@emotion/react": "^11.14.0", |  | ||||||
|     "@emotion/styled": "^11.14.0", |  | ||||||
|     "@mui/icons-material": "^6.4.3", |  | ||||||
|     "@mui/material": "^6.4.3", |  | ||||||
|     "@mui/x-charts": "^7.25.0", |  | ||||||
|     "@mui/x-data-grid": "^7.25.0", |  | ||||||
|     "@mui/x-date-pickers": "^7.25.0", |  | ||||||
|     "@mui/x-tree-view": "^7.25.0", |  | ||||||
|     "@reduxjs/toolkit": "^2.5.0", |  | ||||||
|     "dayjs": "^1.11.13", |  | ||||||
|     "react": "^18.3.1", |  | ||||||
|     "react-dom": "^18.3.1", |  | ||||||
|     "react-redux": "^9.2.0" |  | ||||||
|   }, |  | ||||||
|   "devDependencies": { |  | ||||||
|     "@eslint/js": "^9.17.0", |  | ||||||
|     "@types/react": "^18.3.18", |  | ||||||
|     "@types/react-dom": "^18.3.5", |  | ||||||
|     "@vitejs/plugin-react-swc": "^3.5.0", |  | ||||||
|     "eslint": "^9.17.0", |  | ||||||
|     "eslint-plugin-react-hooks": "^5.0.0", |  | ||||||
|     "eslint-plugin-react-refresh": "^0.4.16", |  | ||||||
|     "globals": "^15.14.0", |  | ||||||
|     "typescript": "~5.6.2", |  | ||||||
|     "typescript-eslint": "^8.18.2", |  | ||||||
|     "vite": "^6.0.5" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1 +0,0 @@ | |||||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> |  | ||||||
| Before Width: | Height: | Size: 1.5 KiB | 
| @ -1,56 +0,0 @@ | |||||||
| import Box from "@mui/material/Box"; |  | ||||||
| import CssBaseline from "@mui/material/CssBaseline"; |  | ||||||
| import Stack from "@mui/material/Stack"; |  | ||||||
| import { alpha } from "@mui/material/styles"; |  | ||||||
| import AppNavbar from "./components/AppNavbar"; |  | ||||||
| import Header from "./components/Header"; |  | ||||||
| import SideMenu from "./components/SideMenu"; |  | ||||||
| import AppTheme from "./shared-theme/AppTheme"; |  | ||||||
| import { |  | ||||||
|   chartsCustomizations, |  | ||||||
|   dataGridCustomizations, |  | ||||||
|   datePickersCustomizations, |  | ||||||
|   treeViewCustomizations, |  | ||||||
| } from "./theme/customizations"; |  | ||||||
|  |  | ||||||
| const xThemeComponents = { |  | ||||||
|   ...chartsCustomizations, |  | ||||||
|   ...dataGridCustomizations, |  | ||||||
|   ...datePickersCustomizations, |  | ||||||
|   ...treeViewCustomizations, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default function Dashboard(props: { disableCustomTheme?: boolean }) { |  | ||||||
|   return ( |  | ||||||
|     <AppTheme {...props} themeComponents={xThemeComponents}> |  | ||||||
|       <CssBaseline enableColorScheme /> |  | ||||||
|       <Box sx={{ display: "flex" }}> |  | ||||||
|         <SideMenu /> |  | ||||||
|         <AppNavbar /> |  | ||||||
|         {/* Main content */} |  | ||||||
|         <Box |  | ||||||
|           component="main" |  | ||||||
|           sx={(theme) => ({ |  | ||||||
|             flexGrow: 1, |  | ||||||
|             backgroundColor: theme.vars |  | ||||||
|               ? `rgba(${theme.vars.palette.background.defaultChannel} / 1)` |  | ||||||
|               : alpha(theme.palette.background.default, 1), |  | ||||||
|             overflow: "auto", |  | ||||||
|           })} |  | ||||||
|         > |  | ||||||
|           <Stack |  | ||||||
|             spacing={2} |  | ||||||
|             sx={{ |  | ||||||
|               alignItems: "center", |  | ||||||
|               mx: 3, |  | ||||||
|               pb: 5, |  | ||||||
|               mt: { xs: 8, md: 0 }, |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             <Header /> |  | ||||||
|           </Stack> |  | ||||||
|         </Box> |  | ||||||
|       </Box> |  | ||||||
|     </AppTheme> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| import { fetchBaseQuery } from "@reduxjs/toolkit/query/react"; |  | ||||||
|  |  | ||||||
| const BASE_URL = import.meta.env.VITE_BASE_URL; |  | ||||||
|  |  | ||||||
| export const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL }); |  | ||||||
| @ -1,14 +0,0 @@ | |||||||
| import { configureStore } from "@reduxjs/toolkit"; |  | ||||||
| import { locationApi } from "../features/locations/locationApi"; |  | ||||||
|  |  | ||||||
| export const store = configureStore({ |  | ||||||
|   reducer: { |  | ||||||
|     [locationApi.reducerPath]: locationApi.reducer, |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   middleware: (getDefaultMiddleware) => |  | ||||||
|     getDefaultMiddleware().concat(locationApi.middleware), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export type RootState = ReturnType<typeof store.getState>; |  | ||||||
| export type AppDispatch = typeof store.dispatch; |  | ||||||
| @ -1,107 +0,0 @@ | |||||||
| import DashboardRoundedIcon from "@mui/icons-material/DashboardRounded"; |  | ||||||
| import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; |  | ||||||
| import AppBar from "@mui/material/AppBar"; |  | ||||||
| import Box from "@mui/material/Box"; |  | ||||||
| import Stack from "@mui/material/Stack"; |  | ||||||
| import { styled } from "@mui/material/styles"; |  | ||||||
| import { tabsClasses } from "@mui/material/Tabs"; |  | ||||||
| import MuiToolbar from "@mui/material/Toolbar"; |  | ||||||
| import Typography from "@mui/material/Typography"; |  | ||||||
| import * as React from "react"; |  | ||||||
| import MenuButton from "./MenuButton"; |  | ||||||
| import SideMenuMobile from "./SideMenuMobile"; |  | ||||||
|  |  | ||||||
| const Toolbar = styled(MuiToolbar)({ |  | ||||||
|   width: "100%", |  | ||||||
|   padding: "12px", |  | ||||||
|   display: "flex", |  | ||||||
|   flexDirection: "column", |  | ||||||
|   alignItems: "start", |  | ||||||
|   justifyContent: "center", |  | ||||||
|   gap: "12px", |  | ||||||
|   flexShrink: 0, |  | ||||||
|   [`& ${tabsClasses.flexContainer}`]: { |  | ||||||
|     gap: "8px", |  | ||||||
|     p: "8px", |  | ||||||
|     pb: 0, |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export default function AppNavbar() { |  | ||||||
|   const [open, setOpen] = React.useState(false); |  | ||||||
|  |  | ||||||
|   const toggleDrawer = (newOpen: boolean) => () => { |  | ||||||
|     setOpen(newOpen); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <AppBar |  | ||||||
|       position="fixed" |  | ||||||
|       sx={{ |  | ||||||
|         display: { xs: "auto", md: "none" }, |  | ||||||
|         boxShadow: 0, |  | ||||||
|         bgcolor: "background.paper", |  | ||||||
|         backgroundImage: "none", |  | ||||||
|         borderBottom: "1px solid", |  | ||||||
|         borderColor: "divider", |  | ||||||
|         top: "var(--template-frame-height, 0px)", |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|       <Toolbar variant="regular"> |  | ||||||
|         <Stack |  | ||||||
|           direction="row" |  | ||||||
|           sx={{ |  | ||||||
|             alignItems: "center", |  | ||||||
|             flexGrow: 1, |  | ||||||
|             width: "100%", |  | ||||||
|             gap: 1, |  | ||||||
|           }} |  | ||||||
|         > |  | ||||||
|           <Stack |  | ||||||
|             direction="row" |  | ||||||
|             spacing={1} |  | ||||||
|             sx={{ justifyContent: "center", mr: "auto" }} |  | ||||||
|           > |  | ||||||
|             <CustomIcon /> |  | ||||||
|             <Typography |  | ||||||
|               variant="h4" |  | ||||||
|               component="h1" |  | ||||||
|               sx={{ color: "text.primary" }} |  | ||||||
|             > |  | ||||||
|               Dashboard |  | ||||||
|             </Typography> |  | ||||||
|           </Stack> |  | ||||||
|           <MenuButton aria-label="menu" onClick={toggleDrawer(true)}> |  | ||||||
|             <MenuRoundedIcon /> |  | ||||||
|           </MenuButton> |  | ||||||
|           <SideMenuMobile open={open} toggleDrawer={toggleDrawer} /> |  | ||||||
|         </Stack> |  | ||||||
|       </Toolbar> |  | ||||||
|     </AppBar> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function CustomIcon() { |  | ||||||
|   return ( |  | ||||||
|     <Box |  | ||||||
|       sx={{ |  | ||||||
|         width: "1.5rem", |  | ||||||
|         height: "1.5rem", |  | ||||||
|         bgcolor: "black", |  | ||||||
|         borderRadius: "999px", |  | ||||||
|         display: "flex", |  | ||||||
|         justifyContent: "center", |  | ||||||
|         alignItems: "center", |  | ||||||
|         alignSelf: "center", |  | ||||||
|         backgroundImage: |  | ||||||
|           "linear-gradient(135deg, hsl(210, 98%, 60%) 0%, hsl(210, 100%, 35%) 100%)", |  | ||||||
|         color: "hsla(210, 100%, 95%, 0.9)", |  | ||||||
|         border: "1px solid", |  | ||||||
|         borderColor: "hsl(210, 100%, 55%)", |  | ||||||
|         boxShadow: "inset 0 2px 5px rgba(255, 255, 255, 0.3)", |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|       <DashboardRoundedIcon color="inherit" sx={{ fontSize: "1rem" }} /> |  | ||||||
|     </Box> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,24 +0,0 @@ | |||||||
| import AutoAwesomeRoundedIcon from "@mui/icons-material/AutoAwesomeRounded"; |  | ||||||
| import Button from "@mui/material/Button"; |  | ||||||
| import Card from "@mui/material/Card"; |  | ||||||
| import CardContent from "@mui/material/CardContent"; |  | ||||||
| import Typography from "@mui/material/Typography"; |  | ||||||
|  |  | ||||||
| export default function CardAlert() { |  | ||||||
|   return ( |  | ||||||
|     <Card variant="outlined" sx={{ m: 1.5, p: 1.5 }}> |  | ||||||
|       <CardContent> |  | ||||||
|         <AutoAwesomeRoundedIcon fontSize="small" /> |  | ||||||
|         <Typography gutterBottom sx={{ fontWeight: 600 }}> |  | ||||||
|           Plan about to expire |  | ||||||
|         </Typography> |  | ||||||
|         <Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}> |  | ||||||
|           Enjoy 10% off when renewing your plan today. |  | ||||||
|         </Typography> |  | ||||||
|         <Button variant="contained" size="small" fullWidth> |  | ||||||
|           Get the discount |  | ||||||
|         </Button> |  | ||||||
|       </CardContent> |  | ||||||
|     </Card> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,78 +0,0 @@ | |||||||
| import * as React from "react"; |  | ||||||
| import dayjs, { Dayjs } from "dayjs"; |  | ||||||
| import Button from "@mui/material/Button"; |  | ||||||
| import CalendarTodayRoundedIcon from "@mui/icons-material/CalendarTodayRounded"; |  | ||||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; |  | ||||||
| import { UseDateFieldProps } from "@mui/x-date-pickers/DateField"; |  | ||||||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; |  | ||||||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; |  | ||||||
| import { |  | ||||||
|   BaseSingleInputFieldProps, |  | ||||||
|   DateValidationError, |  | ||||||
|   FieldSection, |  | ||||||
| } from "@mui/x-date-pickers/models"; |  | ||||||
|  |  | ||||||
| interface ButtonFieldProps |  | ||||||
|   extends UseDateFieldProps<Dayjs, false>, |  | ||||||
|     BaseSingleInputFieldProps< |  | ||||||
|       Dayjs | null, |  | ||||||
|       Dayjs, |  | ||||||
|       FieldSection, |  | ||||||
|       false, |  | ||||||
|       DateValidationError |  | ||||||
|     > { |  | ||||||
|   setOpen?: React.Dispatch<React.SetStateAction<boolean>>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function ButtonField(props: ButtonFieldProps) { |  | ||||||
|   const { |  | ||||||
|     setOpen, |  | ||||||
|     label, |  | ||||||
|     id, |  | ||||||
|     disabled, |  | ||||||
|     InputProps: { ref } = {}, |  | ||||||
|     inputProps: { "aria-label": ariaLabel } = {}, |  | ||||||
|   } = props; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Button |  | ||||||
|       variant="outlined" |  | ||||||
|       id={id} |  | ||||||
|       disabled={disabled} |  | ||||||
|       ref={ref} |  | ||||||
|       aria-label={ariaLabel} |  | ||||||
|       size="small" |  | ||||||
|       onClick={() => setOpen?.((prev) => !prev)} |  | ||||||
|       startIcon={<CalendarTodayRoundedIcon fontSize="small" />} |  | ||||||
|       sx={{ minWidth: "fit-content" }} |  | ||||||
|     > |  | ||||||
|       {label ? `${label}` : "Pick a date"} |  | ||||||
|     </Button> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default function CustomDatePicker() { |  | ||||||
|   const [value, setValue] = React.useState<Dayjs | null>(dayjs("2023-04-17")); |  | ||||||
|   const [open, setOpen] = React.useState(false); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <LocalizationProvider dateAdapter={AdapterDayjs}> |  | ||||||
|       <DatePicker |  | ||||||
|         value={value} |  | ||||||
|         label={value == null ? null : value.format("MMM DD, YYYY")} |  | ||||||
|         onChange={(newValue) => setValue(newValue)} |  | ||||||
|         slots={{ field: ButtonField }} |  | ||||||
|         slotProps={{ |  | ||||||
|           // eslint-disable-next-line @typescript-eslint/no-explicit-any |  | ||||||
|           field: { setOpen } as any, |  | ||||||
|           nextIconButton: { size: "small" }, |  | ||||||
|           previousIconButton: { size: "small" }, |  | ||||||
|         }} |  | ||||||
|         open={open} |  | ||||||
|         onClose={() => setOpen(false)} |  | ||||||
|         onOpen={() => setOpen(true)} |  | ||||||
|         views={["day", "month", "year"]} |  | ||||||
|       /> |  | ||||||
|     </LocalizationProvider> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,35 +0,0 @@ | |||||||
| import NotificationsRoundedIcon from "@mui/icons-material/NotificationsRounded"; |  | ||||||
| import Stack from "@mui/material/Stack"; |  | ||||||
| import CustomDatePicker from "./CustomDatePicker"; |  | ||||||
| import MenuButton from "./MenuButton"; |  | ||||||
| import NavbarBreadcrumbs from "./NavbarBreadcrumbs"; |  | ||||||
|  |  | ||||||
| import ColorModeIconDropdown from "../shared-theme/ColorModeIconDropdown"; |  | ||||||
| import Search from "./Search"; |  | ||||||
|  |  | ||||||
| export default function Header() { |  | ||||||
|   return ( |  | ||||||
|     <Stack |  | ||||||
|       direction="row" |  | ||||||
|       sx={{ |  | ||||||
|         display: { xs: "none", md: "flex" }, |  | ||||||
|         width: "100%", |  | ||||||
|         alignItems: { xs: "flex-start", md: "center" }, |  | ||||||
|         justifyContent: "space-between", |  | ||||||
|         maxWidth: { sm: "100%", md: "1700px" }, |  | ||||||
|         pt: 1.5, |  | ||||||
|       }} |  | ||||||
|       spacing={2} |  | ||||||
|     > |  | ||||||
|       <NavbarBreadcrumbs /> |  | ||||||
|       <Stack direction="row" sx={{ gap: 1 }}> |  | ||||||
|         <Search /> |  | ||||||
|         <CustomDatePicker /> |  | ||||||
|         <MenuButton showBadge aria-label="Open notifications"> |  | ||||||
|           <NotificationsRoundedIcon /> |  | ||||||
|         </MenuButton> |  | ||||||
|         <ColorModeIconDropdown /> |  | ||||||
|       </Stack> |  | ||||||
|     </Stack> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| import Badge, { badgeClasses } from "@mui/material/Badge"; |  | ||||||
| import IconButton, { IconButtonProps } from "@mui/material/IconButton"; |  | ||||||
|  |  | ||||||
| export interface MenuButtonProps extends IconButtonProps { |  | ||||||
|   showBadge?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default function MenuButton({ |  | ||||||
|   showBadge = false, |  | ||||||
|   ...props |  | ||||||
| }: MenuButtonProps) { |  | ||||||
|   return ( |  | ||||||
|     <Badge |  | ||||||
|       color="error" |  | ||||||
|       variant="dot" |  | ||||||
|       invisible={!showBadge} |  | ||||||
|       sx={{ [`& .${badgeClasses.badge}`]: { right: 2, top: 2 } }} |  | ||||||
|     > |  | ||||||
|       <IconButton size="small" {...props} /> |  | ||||||
|     </Badge> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,54 +0,0 @@ | |||||||
| import AnalyticsRoundedIcon from "@mui/icons-material/AnalyticsRounded"; |  | ||||||
| import AssignmentRoundedIcon from "@mui/icons-material/AssignmentRounded"; |  | ||||||
| import HelpRoundedIcon from "@mui/icons-material/HelpRounded"; |  | ||||||
| import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; |  | ||||||
| import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; |  | ||||||
| import PeopleRoundedIcon from "@mui/icons-material/PeopleRounded"; |  | ||||||
| import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; |  | ||||||
| import List from "@mui/material/List"; |  | ||||||
| import ListItem from "@mui/material/ListItem"; |  | ||||||
| import ListItemButton from "@mui/material/ListItemButton"; |  | ||||||
| import ListItemIcon from "@mui/material/ListItemIcon"; |  | ||||||
| import ListItemText from "@mui/material/ListItemText"; |  | ||||||
| import Stack from "@mui/material/Stack"; |  | ||||||
|  |  | ||||||
| const mainListItems = [ |  | ||||||
|   { text: "Home", icon: <HomeRoundedIcon /> }, |  | ||||||
|   { text: "Analytics", icon: <AnalyticsRoundedIcon /> }, |  | ||||||
|   { text: "Clients", icon: <PeopleRoundedIcon /> }, |  | ||||||
|   { text: "Tasks", icon: <AssignmentRoundedIcon /> }, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| const secondaryListItems = [ |  | ||||||
|   { text: "Settings", icon: <SettingsRoundedIcon /> }, |  | ||||||
|   { text: "About", icon: <InfoRoundedIcon /> }, |  | ||||||
|   { text: "Feedback", icon: <HelpRoundedIcon /> }, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export default function MenuContent() { |  | ||||||
|   return ( |  | ||||||
|     <Stack sx={{ flexGrow: 1, p: 1, justifyContent: "space-between" }}> |  | ||||||
|       <List dense> |  | ||||||
|         {mainListItems.map((item, index) => ( |  | ||||||
|           <ListItem key={index} disablePadding sx={{ display: "block" }}> |  | ||||||
|             <ListItemButton selected={index === 0}> |  | ||||||
|               <ListItemIcon>{item.icon}</ListItemIcon> |  | ||||||
|               <ListItemText primary={item.text} /> |  | ||||||
|             </ListItemButton> |  | ||||||
|           </ListItem> |  | ||||||
|         ))} |  | ||||||
|       </List> |  | ||||||
|  |  | ||||||
|       <List dense> |  | ||||||
|         {secondaryListItems.map((item, index) => ( |  | ||||||
|           <ListItem key={index} disablePadding sx={{ display: "block" }}> |  | ||||||
|             <ListItemButton> |  | ||||||
|               <ListItemIcon>{item.icon}</ListItemIcon> |  | ||||||
|               <ListItemText primary={item.text} /> |  | ||||||
|             </ListItemButton> |  | ||||||
|           </ListItem> |  | ||||||
|         ))} |  | ||||||
|       </List> |  | ||||||
|     </Stack> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,32 +0,0 @@ | |||||||
| import NavigateNextRoundedIcon from "@mui/icons-material/NavigateNextRounded"; |  | ||||||
| import Breadcrumbs, { breadcrumbsClasses } from "@mui/material/Breadcrumbs"; |  | ||||||
| import { styled } from "@mui/material/styles"; |  | ||||||
| import Typography from "@mui/material/Typography"; |  | ||||||
|  |  | ||||||
| const StyledBreadcrumbs = styled(Breadcrumbs)(({ theme }) => ({ |  | ||||||
|   margin: theme.spacing(1, 0), |  | ||||||
|   [`& .${breadcrumbsClasses.separator}`]: { |  | ||||||
|     color: (theme.vars || theme).palette.action.disabled, |  | ||||||
|     margin: 1, |  | ||||||
|   }, |  | ||||||
|   [`& .${breadcrumbsClasses.ol}`]: { |  | ||||||
|     alignItems: "center", |  | ||||||
|   }, |  | ||||||
| })); |  | ||||||
|  |  | ||||||
| export default function NavbarBreadcrumbs() { |  | ||||||
|   return ( |  | ||||||
|     <StyledBreadcrumbs |  | ||||||
|       aria-label="breadcrumb" |  | ||||||
|       separator={<NavigateNextRoundedIcon fontSize="small" />} |  | ||||||
|     > |  | ||||||
|       <Typography variant="body1">Dashboard</Typography> |  | ||||||
|       <Typography |  | ||||||
|         variant="body1" |  | ||||||
|         sx={{ color: "text.primary", fontWeight: 600 }} |  | ||||||
|       > |  | ||||||
|         Home |  | ||||||
|       </Typography> |  | ||||||
|     </StyledBreadcrumbs> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,79 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { styled } from '@mui/material/styles'; |  | ||||||
| import Divider, { dividerClasses } from '@mui/material/Divider'; |  | ||||||
| import Menu from '@mui/material/Menu'; |  | ||||||
| import MuiMenuItem from '@mui/material/MenuItem'; |  | ||||||
| import { paperClasses } from '@mui/material/Paper'; |  | ||||||
| import { listClasses } from '@mui/material/List'; |  | ||||||
| import ListItemText from '@mui/material/ListItemText'; |  | ||||||
| import ListItemIcon, { listItemIconClasses } from '@mui/material/ListItemIcon'; |  | ||||||
| import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded'; |  | ||||||
| import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'; |  | ||||||
| import MenuButton from './MenuButton'; |  | ||||||
|  |  | ||||||
| const MenuItem = styled(MuiMenuItem)({ |  | ||||||
|   margin: '2px 0', |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export default function OptionsMenu() { |  | ||||||
|   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); |  | ||||||
|   const open = Boolean(anchorEl); |  | ||||||
|   const handleClick = (event: React.MouseEvent<HTMLElement>) => { |  | ||||||
|     setAnchorEl(event.currentTarget); |  | ||||||
|   }; |  | ||||||
|   const handleClose = () => { |  | ||||||
|     setAnchorEl(null); |  | ||||||
|   }; |  | ||||||
|   return ( |  | ||||||
|     <React.Fragment> |  | ||||||
|       <MenuButton |  | ||||||
|         aria-label="Open menu" |  | ||||||
|         onClick={handleClick} |  | ||||||
|         sx={{ borderColor: 'transparent' }} |  | ||||||
|       > |  | ||||||
|         <MoreVertRoundedIcon /> |  | ||||||
|       </MenuButton> |  | ||||||
|       <Menu |  | ||||||
|         anchorEl={anchorEl} |  | ||||||
|         id="menu" |  | ||||||
|         open={open} |  | ||||||
|         onClose={handleClose} |  | ||||||
|         onClick={handleClose} |  | ||||||
|         transformOrigin={{ horizontal: 'right', vertical: 'top' }} |  | ||||||
|         anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} |  | ||||||
|         sx={{ |  | ||||||
|           [`& .${listClasses.root}`]: { |  | ||||||
|             padding: '4px', |  | ||||||
|           }, |  | ||||||
|           [`& .${paperClasses.root}`]: { |  | ||||||
|             padding: 0, |  | ||||||
|           }, |  | ||||||
|           [`& .${dividerClasses.root}`]: { |  | ||||||
|             margin: '4px -4px', |  | ||||||
|           }, |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         <MenuItem onClick={handleClose}>Profile</MenuItem> |  | ||||||
|         <MenuItem onClick={handleClose}>My account</MenuItem> |  | ||||||
|         <Divider /> |  | ||||||
|         <MenuItem onClick={handleClose}>Add another account</MenuItem> |  | ||||||
|         <MenuItem onClick={handleClose}>Settings</MenuItem> |  | ||||||
|         <Divider /> |  | ||||||
|         <MenuItem |  | ||||||
|           onClick={handleClose} |  | ||||||
|           sx={{ |  | ||||||
|             [`& .${listItemIconClasses.root}`]: { |  | ||||||
|               ml: 'auto', |  | ||||||
|               minWidth: 0, |  | ||||||
|             }, |  | ||||||
|           }} |  | ||||||
|         > |  | ||||||
|           <ListItemText>Logout</ListItemText> |  | ||||||
|           <ListItemIcon> |  | ||||||
|             <LogoutRoundedIcon fontSize="small" /> |  | ||||||
|           </ListItemIcon> |  | ||||||
|         </MenuItem> |  | ||||||
|       </Menu> |  | ||||||
|     </React.Fragment> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| import SearchRoundedIcon from "@mui/icons-material/SearchRounded"; |  | ||||||
| import FormControl from "@mui/material/FormControl"; |  | ||||||
| import InputAdornment from "@mui/material/InputAdornment"; |  | ||||||
| import OutlinedInput from "@mui/material/OutlinedInput"; |  | ||||||
|  |  | ||||||
| export default function Search() { |  | ||||||
|   return ( |  | ||||||
|     <FormControl sx={{ width: { xs: "100%", md: "25ch" } }} variant="outlined"> |  | ||||||
|       <OutlinedInput |  | ||||||
|         size="small" |  | ||||||
|         id="search" |  | ||||||
|         placeholder="Search…" |  | ||||||
|         sx={{ flexGrow: 1 }} |  | ||||||
|         startAdornment={ |  | ||||||
|           <InputAdornment position="start" sx={{ color: "text.primary" }}> |  | ||||||
|             <SearchRoundedIcon fontSize="small" /> |  | ||||||
|           </InputAdornment> |  | ||||||
|         } |  | ||||||
|         inputProps={{ |  | ||||||
|           "aria-label": "search", |  | ||||||
|         }} |  | ||||||
|       /> |  | ||||||
|     </FormControl> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,102 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import MuiAvatar from '@mui/material/Avatar'; |  | ||||||
| import MuiListItemAvatar from '@mui/material/ListItemAvatar'; |  | ||||||
| import MenuItem from '@mui/material/MenuItem'; |  | ||||||
| import ListItemText from '@mui/material/ListItemText'; |  | ||||||
| import ListItemIcon from '@mui/material/ListItemIcon'; |  | ||||||
| import ListSubheader from '@mui/material/ListSubheader'; |  | ||||||
| import Select, { SelectChangeEvent, selectClasses } from '@mui/material/Select'; |  | ||||||
| import Divider from '@mui/material/Divider'; |  | ||||||
| import { styled } from '@mui/material/styles'; |  | ||||||
| import AddRoundedIcon from '@mui/icons-material/AddRounded'; |  | ||||||
| import DevicesRoundedIcon from '@mui/icons-material/DevicesRounded'; |  | ||||||
| import SmartphoneRoundedIcon from '@mui/icons-material/SmartphoneRounded'; |  | ||||||
| import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded'; |  | ||||||
|  |  | ||||||
| const Avatar = styled(MuiAvatar)(({ theme }) => ({ |  | ||||||
|   width: 28, |  | ||||||
|   height: 28, |  | ||||||
|   backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|   color: (theme.vars || theme).palette.text.secondary, |  | ||||||
|   border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
| })); |  | ||||||
|  |  | ||||||
| const ListItemAvatar = styled(MuiListItemAvatar)({ |  | ||||||
|   minWidth: 0, |  | ||||||
|   marginRight: 12, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export default function SelectContent() { |  | ||||||
|   const [company, setCompany] = React.useState(''); |  | ||||||
|  |  | ||||||
|   const handleChange = (event: SelectChangeEvent) => { |  | ||||||
|     setCompany(event.target.value as string); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Select |  | ||||||
|       labelId="company-select" |  | ||||||
|       id="company-simple-select" |  | ||||||
|       value={company} |  | ||||||
|       onChange={handleChange} |  | ||||||
|       displayEmpty |  | ||||||
|       inputProps={{ 'aria-label': 'Select company' }} |  | ||||||
|       fullWidth |  | ||||||
|       sx={{ |  | ||||||
|         maxHeight: 56, |  | ||||||
|         width: 215, |  | ||||||
|         '&.MuiList-root': { |  | ||||||
|           p: '8px', |  | ||||||
|         }, |  | ||||||
|         [`& .${selectClasses.select}`]: { |  | ||||||
|           display: 'flex', |  | ||||||
|           alignItems: 'center', |  | ||||||
|           gap: '2px', |  | ||||||
|           pl: 1, |  | ||||||
|         }, |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|       <ListSubheader sx={{ pt: 0 }}>Production</ListSubheader> |  | ||||||
|       <MenuItem value=""> |  | ||||||
|         <ListItemAvatar> |  | ||||||
|           <Avatar alt="Sitemark web"> |  | ||||||
|             <DevicesRoundedIcon sx={{ fontSize: '1rem' }} /> |  | ||||||
|           </Avatar> |  | ||||||
|         </ListItemAvatar> |  | ||||||
|         <ListItemText primary="Sitemark-web" secondary="Web app" /> |  | ||||||
|       </MenuItem> |  | ||||||
|       <MenuItem value={10}> |  | ||||||
|         <ListItemAvatar> |  | ||||||
|           <Avatar alt="Sitemark App"> |  | ||||||
|             <SmartphoneRoundedIcon sx={{ fontSize: '1rem' }} /> |  | ||||||
|           </Avatar> |  | ||||||
|         </ListItemAvatar> |  | ||||||
|         <ListItemText primary="Sitemark-app" secondary="Mobile application" /> |  | ||||||
|       </MenuItem> |  | ||||||
|       <MenuItem value={20}> |  | ||||||
|         <ListItemAvatar> |  | ||||||
|           <Avatar alt="Sitemark Store"> |  | ||||||
|             <DevicesRoundedIcon sx={{ fontSize: '1rem' }} /> |  | ||||||
|           </Avatar> |  | ||||||
|         </ListItemAvatar> |  | ||||||
|         <ListItemText primary="Sitemark-Store" secondary="Web app" /> |  | ||||||
|       </MenuItem> |  | ||||||
|       <ListSubheader>Development</ListSubheader> |  | ||||||
|       <MenuItem value={30}> |  | ||||||
|         <ListItemAvatar> |  | ||||||
|           <Avatar alt="Sitemark Store"> |  | ||||||
|             <ConstructionRoundedIcon sx={{ fontSize: '1rem' }} /> |  | ||||||
|           </Avatar> |  | ||||||
|         </ListItemAvatar> |  | ||||||
|         <ListItemText primary="Sitemark-Admin" secondary="Web app" /> |  | ||||||
|       </MenuItem> |  | ||||||
|       <Divider sx={{ mx: -1 }} /> |  | ||||||
|       <MenuItem value={40}> |  | ||||||
|         <ListItemIcon> |  | ||||||
|           <AddRoundedIcon /> |  | ||||||
|         </ListItemIcon> |  | ||||||
|         <ListItemText primary="Add product" secondary="Web app" /> |  | ||||||
|       </MenuItem> |  | ||||||
|     </Select> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,80 +0,0 @@ | |||||||
| import Avatar from "@mui/material/Avatar"; |  | ||||||
| import Box from "@mui/material/Box"; |  | ||||||
| import Divider from "@mui/material/Divider"; |  | ||||||
| import MuiDrawer, { drawerClasses } from "@mui/material/Drawer"; |  | ||||||
| import Stack from "@mui/material/Stack"; |  | ||||||
| import { styled } from "@mui/material/styles"; |  | ||||||
| import Typography from "@mui/material/Typography"; |  | ||||||
| import CardAlert from "./CardAlert"; |  | ||||||
| import MenuContent from "./MenuContent"; |  | ||||||
| import OptionsMenu from "./OptionsMenu"; |  | ||||||
| import SelectContent from "./SelectContent"; |  | ||||||
|  |  | ||||||
| const drawerWidth = 240; |  | ||||||
|  |  | ||||||
| const Drawer = styled(MuiDrawer)({ |  | ||||||
|   width: drawerWidth, |  | ||||||
|   flexShrink: 0, |  | ||||||
|   boxSizing: "border-box", |  | ||||||
|   mt: 10, |  | ||||||
|   [`& .${drawerClasses.paper}`]: { |  | ||||||
|     width: drawerWidth, |  | ||||||
|     boxSizing: "border-box", |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export default function SideMenu() { |  | ||||||
|   return ( |  | ||||||
|     <Drawer |  | ||||||
|       variant="permanent" |  | ||||||
|       sx={{ |  | ||||||
|         display: { xs: "none", md: "block" }, |  | ||||||
|         [`& .${drawerClasses.paper}`]: { |  | ||||||
|           backgroundColor: "background.paper", |  | ||||||
|         }, |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|       <Box |  | ||||||
|         sx={{ |  | ||||||
|           display: "flex", |  | ||||||
|           mt: "calc(var(--template-frame-height, 0px) + 4px)", |  | ||||||
|           p: 1.5, |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         <SelectContent /> |  | ||||||
|       </Box> |  | ||||||
|       <Divider /> |  | ||||||
|       <MenuContent /> |  | ||||||
|       <CardAlert /> |  | ||||||
|       <Stack |  | ||||||
|         direction="row" |  | ||||||
|         sx={{ |  | ||||||
|           p: 2, |  | ||||||
|           gap: 1, |  | ||||||
|           alignItems: "center", |  | ||||||
|           borderTop: "1px solid", |  | ||||||
|           borderColor: "divider", |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         <Avatar |  | ||||||
|           sizes="small" |  | ||||||
|           alt="Riley Carter" |  | ||||||
|           src="/static/images/avatar/7.jpg" |  | ||||||
|           sx={{ width: 36, height: 36 }} |  | ||||||
|         /> |  | ||||||
|         <Box sx={{ mr: "auto" }}> |  | ||||||
|           <Typography |  | ||||||
|             variant="body2" |  | ||||||
|             sx={{ fontWeight: 500, lineHeight: "16px" }} |  | ||||||
|           > |  | ||||||
|             Riley Carter |  | ||||||
|           </Typography> |  | ||||||
|           <Typography variant="caption" sx={{ color: "text.secondary" }}> |  | ||||||
|             riley@email.com |  | ||||||
|           </Typography> |  | ||||||
|         </Box> |  | ||||||
|         <OptionsMenu /> |  | ||||||
|       </Stack> |  | ||||||
|     </Drawer> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,79 +0,0 @@ | |||||||
| import LogoutRoundedIcon from "@mui/icons-material/LogoutRounded"; |  | ||||||
| import NotificationsRoundedIcon from "@mui/icons-material/NotificationsRounded"; |  | ||||||
| import Avatar from "@mui/material/Avatar"; |  | ||||||
| import Button from "@mui/material/Button"; |  | ||||||
| import Divider from "@mui/material/Divider"; |  | ||||||
| import Drawer, { drawerClasses } from "@mui/material/Drawer"; |  | ||||||
| import Stack from "@mui/material/Stack"; |  | ||||||
| import Typography from "@mui/material/Typography"; |  | ||||||
|  |  | ||||||
| import CardAlert from "./CardAlert"; |  | ||||||
| import MenuButton from "./MenuButton"; |  | ||||||
| import MenuContent from "./MenuContent"; |  | ||||||
|  |  | ||||||
| interface SideMenuMobileProps { |  | ||||||
|   open: boolean | undefined; |  | ||||||
|   toggleDrawer: (newOpen: boolean) => () => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default function SideMenuMobile({ |  | ||||||
|   open, |  | ||||||
|   toggleDrawer, |  | ||||||
| }: SideMenuMobileProps) { |  | ||||||
|   return ( |  | ||||||
|     <Drawer |  | ||||||
|       anchor="right" |  | ||||||
|       open={open} |  | ||||||
|       onClose={toggleDrawer(false)} |  | ||||||
|       sx={{ |  | ||||||
|         zIndex: (theme) => theme.zIndex.drawer + 1, |  | ||||||
|         [`& .${drawerClasses.paper}`]: { |  | ||||||
|           backgroundImage: "none", |  | ||||||
|           backgroundColor: "background.paper", |  | ||||||
|         }, |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|       <Stack |  | ||||||
|         sx={{ |  | ||||||
|           maxWidth: "70dvw", |  | ||||||
|           height: "100%", |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         <Stack direction="row" sx={{ p: 2, pb: 0, gap: 1 }}> |  | ||||||
|           <Stack |  | ||||||
|             direction="row" |  | ||||||
|             sx={{ gap: 1, alignItems: "center", flexGrow: 1, p: 1 }} |  | ||||||
|           > |  | ||||||
|             <Avatar |  | ||||||
|               sizes="small" |  | ||||||
|               alt="Riley Carter" |  | ||||||
|               src="/static/images/avatar/7.jpg" |  | ||||||
|               sx={{ width: 24, height: 24 }} |  | ||||||
|             /> |  | ||||||
|             <Typography component="p" variant="h6"> |  | ||||||
|               Riley Carter |  | ||||||
|             </Typography> |  | ||||||
|           </Stack> |  | ||||||
|           <MenuButton showBadge> |  | ||||||
|             <NotificationsRoundedIcon /> |  | ||||||
|           </MenuButton> |  | ||||||
|         </Stack> |  | ||||||
|         <Divider /> |  | ||||||
|         <Stack sx={{ flexGrow: 1 }}> |  | ||||||
|           <MenuContent /> |  | ||||||
|           <Divider /> |  | ||||||
|         </Stack> |  | ||||||
|         <CardAlert /> |  | ||||||
|         <Stack sx={{ p: 2 }}> |  | ||||||
|           <Button |  | ||||||
|             variant="outlined" |  | ||||||
|             fullWidth |  | ||||||
|             startIcon={<LogoutRoundedIcon />} |  | ||||||
|           > |  | ||||||
|             Logout |  | ||||||
|           </Button> |  | ||||||
|         </Stack> |  | ||||||
|       </Stack> |  | ||||||
|     </Drawer> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,47 +0,0 @@ | |||||||
| import { createApi } from "@reduxjs/toolkit/query/react"; |  | ||||||
| import { baseQuery } from "../../app/apiBaseQuery"; |  | ||||||
| import { Location } from "../../models/location"; |  | ||||||
|  |  | ||||||
| export const locationApi = createApi({ |  | ||||||
|   reducerPath: "locationApi", |  | ||||||
|   baseQuery, |  | ||||||
|   endpoints: (builder) => ({ |  | ||||||
|     getLocations: builder.query<Location[], void>({ |  | ||||||
|       query: () => "/locations", |  | ||||||
|     }), |  | ||||||
|     getLocationById: builder.query<Location, string>({ |  | ||||||
|       query: (id) => `/locations/${id}`, |  | ||||||
|     }), |  | ||||||
|     createLocation: builder.mutation<Location, Partial<Location>>({ |  | ||||||
|       query: (newLocation) => ({ |  | ||||||
|         url: "/locations", |  | ||||||
|         method: "POST", |  | ||||||
|         body: newLocation, |  | ||||||
|       }), |  | ||||||
|     }), |  | ||||||
|     updateLocation: builder.mutation< |  | ||||||
|       Location, |  | ||||||
|       { id: string; location: Partial<Location> } |  | ||||||
|     >({ |  | ||||||
|       query: ({ id, location }) => ({ |  | ||||||
|         url: `/locations/${id}`, |  | ||||||
|         method: "PUT", |  | ||||||
|         body: location, |  | ||||||
|       }), |  | ||||||
|     }), |  | ||||||
|     deleteLocation: builder.mutation<void, string>({ |  | ||||||
|       query: (id) => ({ |  | ||||||
|         url: `/locations/${id}`, |  | ||||||
|         method: "DELETE", |  | ||||||
|       }), |  | ||||||
|     }), |  | ||||||
|   }), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const { |  | ||||||
|   useGetLocationsQuery, |  | ||||||
|   useGetLocationByIdQuery, |  | ||||||
|   useCreateLocationMutation, |  | ||||||
|   useUpdateLocationMutation, |  | ||||||
|   useDeleteLocationMutation, |  | ||||||
| } = locationApi; |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| import React from "react"; |  | ||||||
| import ReactDOM from "react-dom/client"; |  | ||||||
| import App from "./App.tsx"; |  | ||||||
|  |  | ||||||
| ReactDOM.createRoot(document.getElementById("root")!).render( |  | ||||||
|   <React.StrictMode> |  | ||||||
|     <App /> |  | ||||||
|   </React.StrictMode> |  | ||||||
| ); |  | ||||||
| @ -1,12 +0,0 @@ | |||||||
| export interface Location { |  | ||||||
|   location_id: string; |  | ||||||
|   lp_ttn_end_device_uplinks_id: string; |  | ||||||
|   wifi_latitude: number; |  | ||||||
|   wifi_longitude: number; |  | ||||||
|   gnss_latitude: number; |  | ||||||
|   gnss_longitude: number; |  | ||||||
|   ttn_gw_latitude: number; |  | ||||||
|   ttn_gw_longitude: number; |  | ||||||
|   created_at_utc: string; |  | ||||||
|   updated_at_utc: string; |  | ||||||
| } |  | ||||||
| @ -1,53 +0,0 @@ | |||||||
| import type { ThemeOptions } from "@mui/material/styles"; |  | ||||||
| import { ThemeProvider, createTheme } from "@mui/material/styles"; |  | ||||||
| import * as React from "react"; |  | ||||||
| import { dataDisplayCustomizations } from "./customizations/dataDisplay"; |  | ||||||
| import { feedbackCustomizations } from "./customizations/feedback"; |  | ||||||
| import { inputsCustomizations } from "./customizations/inputs"; |  | ||||||
| import { navigationCustomizations } from "./customizations/navigation"; |  | ||||||
| import { surfacesCustomizations } from "./customizations/surfaces"; |  | ||||||
| import { colorSchemes, shadows, shape, typography } from "./themePrimitives"; |  | ||||||
|  |  | ||||||
| interface AppThemeProps { |  | ||||||
|   children: React.ReactNode; |  | ||||||
|   /** |  | ||||||
|    * This is for the docs site. You can ignore it or remove it. |  | ||||||
|    */ |  | ||||||
|   disableCustomTheme?: boolean; |  | ||||||
|   themeComponents?: ThemeOptions["components"]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default function AppTheme(props: AppThemeProps) { |  | ||||||
|   const { children, disableCustomTheme, themeComponents } = props; |  | ||||||
|   const theme = React.useMemo(() => { |  | ||||||
|     return disableCustomTheme |  | ||||||
|       ? {} |  | ||||||
|       : createTheme({ |  | ||||||
|           // For more details about CSS variables configuration, see https://mui.com/material-ui/customization/css-theme-variables/configuration/ |  | ||||||
|           cssVariables: { |  | ||||||
|             colorSchemeSelector: "data-mui-color-scheme", |  | ||||||
|             cssVarPrefix: "template", |  | ||||||
|           }, |  | ||||||
|           colorSchemes, // Recently added in v6 for building light & dark mode app, see https://mui.com/material-ui/customization/palette/#color-schemes |  | ||||||
|           typography, |  | ||||||
|           shadows, |  | ||||||
|           shape, |  | ||||||
|           components: { |  | ||||||
|             ...inputsCustomizations, |  | ||||||
|             ...dataDisplayCustomizations, |  | ||||||
|             ...feedbackCustomizations, |  | ||||||
|             ...navigationCustomizations, |  | ||||||
|             ...surfacesCustomizations, |  | ||||||
|             ...themeComponents, |  | ||||||
|           }, |  | ||||||
|         }); |  | ||||||
|   }, [disableCustomTheme, themeComponents]); |  | ||||||
|   if (disableCustomTheme) { |  | ||||||
|     return <React.Fragment>{children}</React.Fragment>; |  | ||||||
|   } |  | ||||||
|   return ( |  | ||||||
|     <ThemeProvider theme={theme} disableTransitionOnChange> |  | ||||||
|       {children} |  | ||||||
|     </ThemeProvider> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,89 +0,0 @@ | |||||||
| import DarkModeIcon from "@mui/icons-material/DarkModeRounded"; |  | ||||||
| import LightModeIcon from "@mui/icons-material/LightModeRounded"; |  | ||||||
| import Box from "@mui/material/Box"; |  | ||||||
| import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton"; |  | ||||||
| import Menu from "@mui/material/Menu"; |  | ||||||
| import MenuItem from "@mui/material/MenuItem"; |  | ||||||
| import { useColorScheme } from "@mui/material/styles"; |  | ||||||
| import * as React from "react"; |  | ||||||
|  |  | ||||||
| export default function ColorModeIconDropdown(props: IconButtonOwnProps) { |  | ||||||
|   const { mode, systemMode, setMode } = useColorScheme(); |  | ||||||
|   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); |  | ||||||
|   const open = Boolean(anchorEl); |  | ||||||
|   const handleClick = (event: React.MouseEvent<HTMLElement>) => { |  | ||||||
|     setAnchorEl(event.currentTarget); |  | ||||||
|   }; |  | ||||||
|   const handleClose = () => { |  | ||||||
|     setAnchorEl(null); |  | ||||||
|   }; |  | ||||||
|   const handleMode = (targetMode: "system" | "light" | "dark") => () => { |  | ||||||
|     setMode(targetMode); |  | ||||||
|     handleClose(); |  | ||||||
|   }; |  | ||||||
|   if (!mode) { |  | ||||||
|     return ( |  | ||||||
|       <Box |  | ||||||
|         data-screenshot="toggle-mode" |  | ||||||
|         sx={(theme) => ({ |  | ||||||
|           verticalAlign: "bottom", |  | ||||||
|           display: "inline-flex", |  | ||||||
|           width: "2.25rem", |  | ||||||
|           height: "2.25rem", |  | ||||||
|           borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|           border: "1px solid", |  | ||||||
|           borderColor: (theme.vars || theme).palette.divider, |  | ||||||
|         })} |  | ||||||
|       /> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|   const resolvedMode = (systemMode || mode) as "light" | "dark"; |  | ||||||
|   const icon = { |  | ||||||
|     light: <LightModeIcon />, |  | ||||||
|     dark: <DarkModeIcon />, |  | ||||||
|   }[resolvedMode]; |  | ||||||
|   return ( |  | ||||||
|     <React.Fragment> |  | ||||||
|       <IconButton |  | ||||||
|         data-screenshot="toggle-mode" |  | ||||||
|         onClick={handleClick} |  | ||||||
|         disableRipple |  | ||||||
|         size="small" |  | ||||||
|         aria-controls={open ? "color-scheme-menu" : undefined} |  | ||||||
|         aria-haspopup="true" |  | ||||||
|         aria-expanded={open ? "true" : undefined} |  | ||||||
|         {...props} |  | ||||||
|       > |  | ||||||
|         {icon} |  | ||||||
|       </IconButton> |  | ||||||
|       <Menu |  | ||||||
|         anchorEl={anchorEl} |  | ||||||
|         id="account-menu" |  | ||||||
|         open={open} |  | ||||||
|         onClose={handleClose} |  | ||||||
|         onClick={handleClose} |  | ||||||
|         slotProps={{ |  | ||||||
|           paper: { |  | ||||||
|             variant: "outlined", |  | ||||||
|             elevation: 0, |  | ||||||
|             sx: { |  | ||||||
|               my: "4px", |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }} |  | ||||||
|         transformOrigin={{ horizontal: "right", vertical: "top" }} |  | ||||||
|         anchorOrigin={{ horizontal: "right", vertical: "bottom" }} |  | ||||||
|       > |  | ||||||
|         <MenuItem selected={mode === "system"} onClick={handleMode("system")}> |  | ||||||
|           System |  | ||||||
|         </MenuItem> |  | ||||||
|         <MenuItem selected={mode === "light"} onClick={handleMode("light")}> |  | ||||||
|           Light |  | ||||||
|         </MenuItem> |  | ||||||
|         <MenuItem selected={mode === "dark"} onClick={handleMode("dark")}> |  | ||||||
|           Dark |  | ||||||
|         </MenuItem> |  | ||||||
|       </Menu> |  | ||||||
|     </React.Fragment> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,233 +0,0 @@ | |||||||
| import { Theme, alpha, Components } from '@mui/material/styles'; |  | ||||||
| import { svgIconClasses } from '@mui/material/SvgIcon'; |  | ||||||
| import { typographyClasses } from '@mui/material/Typography'; |  | ||||||
| import { buttonBaseClasses } from '@mui/material/ButtonBase'; |  | ||||||
| import { chipClasses } from '@mui/material/Chip'; |  | ||||||
| import { iconButtonClasses } from '@mui/material/IconButton'; |  | ||||||
| import { gray, red, green } from '../themePrimitives'; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const dataDisplayCustomizations: Components<Theme> = { |  | ||||||
|   MuiList: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         padding: '8px', |  | ||||||
|         display: 'flex', |  | ||||||
|         flexDirection: 'column', |  | ||||||
|         gap: 0, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiListItem: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         [`& .${svgIconClasses.root}`]: { |  | ||||||
|           width: '1rem', |  | ||||||
|           height: '1rem', |  | ||||||
|           color: (theme.vars || theme).palette.text.secondary, |  | ||||||
|         }, |  | ||||||
|         [`& .${typographyClasses.root}`]: { |  | ||||||
|           fontWeight: 500, |  | ||||||
|         }, |  | ||||||
|         [`& .${buttonBaseClasses.root}`]: { |  | ||||||
|           display: 'flex', |  | ||||||
|           gap: 8, |  | ||||||
|           padding: '2px 8px', |  | ||||||
|           borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|           opacity: 0.7, |  | ||||||
|           '&.Mui-selected': { |  | ||||||
|             opacity: 1, |  | ||||||
|             backgroundColor: alpha(theme.palette.action.selected, 0.3), |  | ||||||
|             [`& .${svgIconClasses.root}`]: { |  | ||||||
|               color: (theme.vars || theme).palette.text.primary, |  | ||||||
|             }, |  | ||||||
|             '&:focus-visible': { |  | ||||||
|               backgroundColor: alpha(theme.palette.action.selected, 0.3), |  | ||||||
|             }, |  | ||||||
|             '&:hover': { |  | ||||||
|               backgroundColor: alpha(theme.palette.action.selected, 0.5), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           '&:focus-visible': { |  | ||||||
|             backgroundColor: 'transparent', |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiListItemText: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       primary: ({ theme }) => ({ |  | ||||||
|         fontSize: theme.typography.body2.fontSize, |  | ||||||
|         fontWeight: 500, |  | ||||||
|         lineHeight: theme.typography.body2.lineHeight, |  | ||||||
|       }), |  | ||||||
|       secondary: ({ theme }) => ({ |  | ||||||
|         fontSize: theme.typography.caption.fontSize, |  | ||||||
|         lineHeight: theme.typography.caption.lineHeight, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiListSubheader: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         backgroundColor: 'transparent', |  | ||||||
|         padding: '4px 8px', |  | ||||||
|         fontSize: theme.typography.caption.fontSize, |  | ||||||
|         fontWeight: 500, |  | ||||||
|         lineHeight: theme.typography.caption.lineHeight, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiListItemIcon: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         minWidth: 0, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiChip: { |  | ||||||
|     defaultProps: { |  | ||||||
|       size: 'small', |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         border: '1px solid', |  | ||||||
|         borderRadius: '999px', |  | ||||||
|         [`& .${chipClasses.label}`]: { |  | ||||||
|           fontWeight: 600, |  | ||||||
|         }, |  | ||||||
|         variants: [ |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'default', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               borderColor: gray[200], |  | ||||||
|               backgroundColor: gray[100], |  | ||||||
|               [`& .${chipClasses.label}`]: { |  | ||||||
|                 color: gray[500], |  | ||||||
|               }, |  | ||||||
|               [`& .${chipClasses.icon}`]: { |  | ||||||
|                 color: gray[500], |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 borderColor: gray[700], |  | ||||||
|                 backgroundColor: gray[800], |  | ||||||
|                 [`& .${chipClasses.label}`]: { |  | ||||||
|                   color: gray[300], |  | ||||||
|                 }, |  | ||||||
|                 [`& .${chipClasses.icon}`]: { |  | ||||||
|                   color: gray[300], |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'success', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               borderColor: green[200], |  | ||||||
|               backgroundColor: green[50], |  | ||||||
|               [`& .${chipClasses.label}`]: { |  | ||||||
|                 color: green[500], |  | ||||||
|               }, |  | ||||||
|               [`& .${chipClasses.icon}`]: { |  | ||||||
|                 color: green[500], |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 borderColor: green[800], |  | ||||||
|                 backgroundColor: green[900], |  | ||||||
|                 [`& .${chipClasses.label}`]: { |  | ||||||
|                   color: green[300], |  | ||||||
|                 }, |  | ||||||
|                 [`& .${chipClasses.icon}`]: { |  | ||||||
|                   color: green[300], |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'error', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               borderColor: red[100], |  | ||||||
|               backgroundColor: red[50], |  | ||||||
|               [`& .${chipClasses.label}`]: { |  | ||||||
|                 color: red[500], |  | ||||||
|               }, |  | ||||||
|               [`& .${chipClasses.icon}`]: { |  | ||||||
|                 color: red[500], |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 borderColor: red[800], |  | ||||||
|                 backgroundColor: red[900], |  | ||||||
|                 [`& .${chipClasses.label}`]: { |  | ||||||
|                   color: red[200], |  | ||||||
|                 }, |  | ||||||
|                 [`& .${chipClasses.icon}`]: { |  | ||||||
|                   color: red[300], |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { size: 'small' }, |  | ||||||
|             style: { |  | ||||||
|               maxHeight: 20, |  | ||||||
|               [`& .${chipClasses.label}`]: { |  | ||||||
|                 fontSize: theme.typography.caption.fontSize, |  | ||||||
|               }, |  | ||||||
|               [`& .${svgIconClasses.root}`]: { |  | ||||||
|                 fontSize: theme.typography.caption.fontSize, |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { size: 'medium' }, |  | ||||||
|             style: { |  | ||||||
|               [`& .${chipClasses.label}`]: { |  | ||||||
|                 fontSize: theme.typography.caption.fontSize, |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiTablePagination: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       actions: { |  | ||||||
|         display: 'flex', |  | ||||||
|         gap: 8, |  | ||||||
|         marginRight: 6, |  | ||||||
|         [`& .${iconButtonClasses.root}`]: { |  | ||||||
|           minWidth: 0, |  | ||||||
|           width: 36, |  | ||||||
|           height: 36, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiIcon: { |  | ||||||
|     defaultProps: { |  | ||||||
|       fontSize: 'small', |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         variants: [ |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               fontSize: 'small', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               fontSize: '1rem', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,46 +0,0 @@ | |||||||
| import { Theme, alpha, Components } from '@mui/material/styles'; |  | ||||||
| import { gray, orange } from '../themePrimitives'; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const feedbackCustomizations: Components<Theme> = { |  | ||||||
|   MuiAlert: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         borderRadius: 10, |  | ||||||
|         backgroundColor: orange[100], |  | ||||||
|         color: (theme.vars || theme).palette.text.primary, |  | ||||||
|         border: `1px solid ${alpha(orange[300], 0.5)}`, |  | ||||||
|         '& .MuiAlert-icon': { |  | ||||||
|           color: orange[500], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           backgroundColor: `${alpha(orange[900], 0.5)}`, |  | ||||||
|           border: `1px solid ${alpha(orange[800], 0.5)}`, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiDialog: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         '& .MuiDialog-paper': { |  | ||||||
|           borderRadius: '10px', |  | ||||||
|           border: '1px solid', |  | ||||||
|           borderColor: (theme.vars || theme).palette.divider, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiLinearProgress: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         height: 8, |  | ||||||
|         borderRadius: 8, |  | ||||||
|         backgroundColor: gray[200], |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           backgroundColor: gray[800], |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,445 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { alpha, Theme, Components } from '@mui/material/styles'; |  | ||||||
| import { outlinedInputClasses } from '@mui/material/OutlinedInput'; |  | ||||||
| import { svgIconClasses } from '@mui/material/SvgIcon'; |  | ||||||
| import { toggleButtonGroupClasses } from '@mui/material/ToggleButtonGroup'; |  | ||||||
| import { toggleButtonClasses } from '@mui/material/ToggleButton'; |  | ||||||
| import CheckBoxOutlineBlankRoundedIcon from '@mui/icons-material/CheckBoxOutlineBlankRounded'; |  | ||||||
| import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; |  | ||||||
| import RemoveRoundedIcon from '@mui/icons-material/RemoveRounded'; |  | ||||||
| import { gray, brand } from '../themePrimitives'; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const inputsCustomizations: Components<Theme> = { |  | ||||||
|   MuiButtonBase: { |  | ||||||
|     defaultProps: { |  | ||||||
|       disableTouchRipple: true, |  | ||||||
|       disableRipple: true, |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         boxSizing: 'border-box', |  | ||||||
|         transition: 'all 100ms ease-in', |  | ||||||
|         '&:focus-visible': { |  | ||||||
|           outline: `3px solid ${alpha(theme.palette.primary.main, 0.5)}`, |  | ||||||
|           outlineOffset: '2px', |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiButton: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         boxShadow: 'none', |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         textTransform: 'none', |  | ||||||
|         variants: [ |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               size: 'small', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               height: '2.25rem', |  | ||||||
|               padding: '8px 12px', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               size: 'medium', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               height: '2.5rem', // 40px |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'primary', |  | ||||||
|               variant: 'contained', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               color: 'white', |  | ||||||
|               backgroundColor: gray[900], |  | ||||||
|               backgroundImage: `linear-gradient(to bottom, ${gray[700]}, ${gray[800]})`, |  | ||||||
|               boxShadow: `inset 0 1px 0 ${gray[600]}, inset 0 -1px 0 1px hsl(220, 0%, 0%)`, |  | ||||||
|               border: `1px solid ${gray[700]}`, |  | ||||||
|               '&:hover': { |  | ||||||
|                 backgroundImage: 'none', |  | ||||||
|                 backgroundColor: gray[700], |  | ||||||
|                 boxShadow: 'none', |  | ||||||
|               }, |  | ||||||
|               '&:active': { |  | ||||||
|                 backgroundColor: gray[800], |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 color: 'black', |  | ||||||
|                 backgroundColor: gray[50], |  | ||||||
|                 backgroundImage: `linear-gradient(to bottom, ${gray[100]}, ${gray[50]})`, |  | ||||||
|                 boxShadow: 'inset 0 -1px 0  hsl(220, 30%, 80%)', |  | ||||||
|                 border: `1px solid ${gray[50]}`, |  | ||||||
|                 '&:hover': { |  | ||||||
|                   backgroundImage: 'none', |  | ||||||
|                   backgroundColor: gray[300], |  | ||||||
|                   boxShadow: 'none', |  | ||||||
|                 }, |  | ||||||
|                 '&:active': { |  | ||||||
|                   backgroundColor: gray[400], |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'secondary', |  | ||||||
|               variant: 'contained', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               color: 'white', |  | ||||||
|               backgroundColor: brand[300], |  | ||||||
|               backgroundImage: `linear-gradient(to bottom, ${alpha(brand[400], 0.8)}, ${brand[500]})`, |  | ||||||
|               boxShadow: `inset 0 2px 0 ${alpha(brand[200], 0.2)}, inset 0 -2px 0 ${alpha(brand[700], 0.4)}`, |  | ||||||
|               border: `1px solid ${brand[500]}`, |  | ||||||
|               '&:hover': { |  | ||||||
|                 backgroundColor: brand[700], |  | ||||||
|                 boxShadow: 'none', |  | ||||||
|               }, |  | ||||||
|               '&:active': { |  | ||||||
|                 backgroundColor: brand[700], |  | ||||||
|                 backgroundImage: 'none', |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               variant: 'outlined', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               color: (theme.vars || theme).palette.text.primary, |  | ||||||
|               border: '1px solid', |  | ||||||
|               borderColor: gray[200], |  | ||||||
|               backgroundColor: alpha(gray[50], 0.3), |  | ||||||
|               '&:hover': { |  | ||||||
|                 backgroundColor: gray[100], |  | ||||||
|                 borderColor: gray[300], |  | ||||||
|               }, |  | ||||||
|               '&:active': { |  | ||||||
|                 backgroundColor: gray[200], |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 backgroundColor: gray[800], |  | ||||||
|                 borderColor: gray[700], |  | ||||||
|  |  | ||||||
|                 '&:hover': { |  | ||||||
|                   backgroundColor: gray[900], |  | ||||||
|                   borderColor: gray[600], |  | ||||||
|                 }, |  | ||||||
|                 '&:active': { |  | ||||||
|                   backgroundColor: gray[900], |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'secondary', |  | ||||||
|               variant: 'outlined', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               color: brand[700], |  | ||||||
|               border: '1px solid', |  | ||||||
|               borderColor: brand[200], |  | ||||||
|               backgroundColor: brand[50], |  | ||||||
|               '&:hover': { |  | ||||||
|                 backgroundColor: brand[100], |  | ||||||
|                 borderColor: brand[400], |  | ||||||
|               }, |  | ||||||
|               '&:active': { |  | ||||||
|                 backgroundColor: alpha(brand[200], 0.7), |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 color: brand[50], |  | ||||||
|                 border: '1px solid', |  | ||||||
|                 borderColor: brand[900], |  | ||||||
|                 backgroundColor: alpha(brand[900], 0.3), |  | ||||||
|                 '&:hover': { |  | ||||||
|                   borderColor: brand[700], |  | ||||||
|                   backgroundColor: alpha(brand[900], 0.6), |  | ||||||
|                 }, |  | ||||||
|                 '&:active': { |  | ||||||
|                   backgroundColor: alpha(brand[900], 0.5), |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               variant: 'text', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               color: gray[600], |  | ||||||
|               '&:hover': { |  | ||||||
|                 backgroundColor: gray[100], |  | ||||||
|               }, |  | ||||||
|               '&:active': { |  | ||||||
|                 backgroundColor: gray[200], |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 color: gray[50], |  | ||||||
|                 '&:hover': { |  | ||||||
|                   backgroundColor: gray[700], |  | ||||||
|                 }, |  | ||||||
|                 '&:active': { |  | ||||||
|                   backgroundColor: alpha(gray[700], 0.7), |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               color: 'secondary', |  | ||||||
|               variant: 'text', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               color: brand[700], |  | ||||||
|               '&:hover': { |  | ||||||
|                 backgroundColor: alpha(brand[100], 0.5), |  | ||||||
|               }, |  | ||||||
|               '&:active': { |  | ||||||
|                 backgroundColor: alpha(brand[200], 0.7), |  | ||||||
|               }, |  | ||||||
|               ...theme.applyStyles('dark', { |  | ||||||
|                 color: brand[100], |  | ||||||
|                 '&:hover': { |  | ||||||
|                   backgroundColor: alpha(brand[900], 0.5), |  | ||||||
|                 }, |  | ||||||
|                 '&:active': { |  | ||||||
|                   backgroundColor: alpha(brand[900], 0.3), |  | ||||||
|                 }, |  | ||||||
|               }), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiIconButton: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         boxShadow: 'none', |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         textTransform: 'none', |  | ||||||
|         fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|         letterSpacing: 0, |  | ||||||
|         color: (theme.vars || theme).palette.text.primary, |  | ||||||
|         border: '1px solid ', |  | ||||||
|         borderColor: gray[200], |  | ||||||
|         backgroundColor: alpha(gray[50], 0.3), |  | ||||||
|         '&:hover': { |  | ||||||
|           backgroundColor: gray[100], |  | ||||||
|           borderColor: gray[300], |  | ||||||
|         }, |  | ||||||
|         '&:active': { |  | ||||||
|           backgroundColor: gray[200], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           backgroundColor: gray[800], |  | ||||||
|           borderColor: gray[700], |  | ||||||
|           '&:hover': { |  | ||||||
|             backgroundColor: gray[900], |  | ||||||
|             borderColor: gray[600], |  | ||||||
|           }, |  | ||||||
|           '&:active': { |  | ||||||
|             backgroundColor: gray[900], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|         variants: [ |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               size: 'small', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               width: '2.25rem', |  | ||||||
|               height: '2.25rem', |  | ||||||
|               padding: '0.25rem', |  | ||||||
|               [`& .${svgIconClasses.root}`]: { fontSize: '1rem' }, |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               size: 'medium', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               width: '2.5rem', |  | ||||||
|               height: '2.5rem', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiToggleButtonGroup: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         borderRadius: '10px', |  | ||||||
|         boxShadow: `0 4px 16px ${alpha(gray[400], 0.2)}`, |  | ||||||
|         [`& .${toggleButtonGroupClasses.selected}`]: { |  | ||||||
|           color: brand[500], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           [`& .${toggleButtonGroupClasses.selected}`]: { |  | ||||||
|             color: '#fff', |  | ||||||
|           }, |  | ||||||
|           boxShadow: `0 4px 16px ${alpha(brand[700], 0.5)}`, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiToggleButton: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         padding: '12px 16px', |  | ||||||
|         textTransform: 'none', |  | ||||||
|         borderRadius: '10px', |  | ||||||
|         fontWeight: 500, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           color: gray[400], |  | ||||||
|           boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)', |  | ||||||
|           [`&.${toggleButtonClasses.selected}`]: { |  | ||||||
|             color: brand[300], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiCheckbox: { |  | ||||||
|     defaultProps: { |  | ||||||
|       disableRipple: true, |  | ||||||
|       icon: ( |  | ||||||
|         <CheckBoxOutlineBlankRoundedIcon sx={{ color: 'hsla(210, 0%, 0%, 0.0)' }} /> |  | ||||||
|       ), |  | ||||||
|       checkedIcon: <CheckRoundedIcon sx={{ height: 14, width: 14 }} />, |  | ||||||
|       indeterminateIcon: <RemoveRoundedIcon sx={{ height: 14, width: 14 }} />, |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         margin: 10, |  | ||||||
|         height: 16, |  | ||||||
|         width: 16, |  | ||||||
|         borderRadius: 5, |  | ||||||
|         border: '1px solid ', |  | ||||||
|         borderColor: alpha(gray[300], 0.8), |  | ||||||
|         boxShadow: '0 0 0 1.5px hsla(210, 0%, 0%, 0.04) inset', |  | ||||||
|         backgroundColor: alpha(gray[100], 0.4), |  | ||||||
|         transition: 'border-color, background-color, 120ms ease-in', |  | ||||||
|         '&:hover': { |  | ||||||
|           borderColor: brand[300], |  | ||||||
|         }, |  | ||||||
|         '&.Mui-focusVisible': { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           outlineOffset: '2px', |  | ||||||
|           borderColor: brand[400], |  | ||||||
|         }, |  | ||||||
|         '&.Mui-checked': { |  | ||||||
|           color: 'white', |  | ||||||
|           backgroundColor: brand[500], |  | ||||||
|           borderColor: brand[500], |  | ||||||
|           boxShadow: `none`, |  | ||||||
|           '&:hover': { |  | ||||||
|             backgroundColor: brand[600], |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           borderColor: alpha(gray[700], 0.8), |  | ||||||
|           boxShadow: '0 0 0 1.5px hsl(210, 0%, 0%) inset', |  | ||||||
|           backgroundColor: alpha(gray[900], 0.8), |  | ||||||
|           '&:hover': { |  | ||||||
|             borderColor: brand[300], |  | ||||||
|           }, |  | ||||||
|           '&.Mui-focusVisible': { |  | ||||||
|             borderColor: brand[400], |  | ||||||
|             outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|             outlineOffset: '2px', |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiInputBase: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         border: 'none', |  | ||||||
|       }, |  | ||||||
|       input: { |  | ||||||
|         '&::placeholder': { |  | ||||||
|           opacity: 0.7, |  | ||||||
|           color: gray[500], |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiOutlinedInput: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       input: { |  | ||||||
|         padding: 0, |  | ||||||
|       }, |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         padding: '8px 12px', |  | ||||||
|         color: (theme.vars || theme).palette.text.primary, |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|         backgroundColor: (theme.vars || theme).palette.background.default, |  | ||||||
|         transition: 'border 120ms ease-in', |  | ||||||
|         '&:hover': { |  | ||||||
|           borderColor: gray[400], |  | ||||||
|         }, |  | ||||||
|         [`&.${outlinedInputClasses.focused}`]: { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           borderColor: brand[400], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           '&:hover': { |  | ||||||
|             borderColor: gray[500], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|         variants: [ |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               size: 'small', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               height: '2.25rem', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             props: { |  | ||||||
|               size: 'medium', |  | ||||||
|             }, |  | ||||||
|             style: { |  | ||||||
|               height: '2.5rem', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }), |  | ||||||
|       notchedOutline: { |  | ||||||
|         border: 'none', |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiInputAdornment: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         color: (theme.vars || theme).palette.grey[500], |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           color: (theme.vars || theme).palette.grey[400], |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiFormLabel: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         typography: theme.typography.caption, |  | ||||||
|         marginBottom: 8, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,279 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { Theme, alpha, Components } from '@mui/material/styles'; |  | ||||||
| import { SvgIconProps } from '@mui/material/SvgIcon'; |  | ||||||
| import { buttonBaseClasses } from '@mui/material/ButtonBase'; |  | ||||||
| import { dividerClasses } from '@mui/material/Divider'; |  | ||||||
| import { menuItemClasses } from '@mui/material/MenuItem'; |  | ||||||
| import { selectClasses } from '@mui/material/Select'; |  | ||||||
| import { tabClasses } from '@mui/material/Tab'; |  | ||||||
| import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; |  | ||||||
| import { gray, brand } from '../themePrimitives'; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const navigationCustomizations: Components<Theme> = { |  | ||||||
|   MuiMenuItem: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         padding: '6px 8px', |  | ||||||
|         [`&.${menuItemClasses.focusVisible}`]: { |  | ||||||
|           backgroundColor: 'transparent', |  | ||||||
|         }, |  | ||||||
|         [`&.${menuItemClasses.selected}`]: { |  | ||||||
|           [`&.${menuItemClasses.focusVisible}`]: { |  | ||||||
|             backgroundColor: alpha(theme.palette.action.selected, 0.3), |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiMenu: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       list: { |  | ||||||
|         gap: '0px', |  | ||||||
|         [`&.${dividerClasses.root}`]: { |  | ||||||
|           margin: '0 -8px', |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       paper: ({ theme }) => ({ |  | ||||||
|         marginTop: '4px', |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|         backgroundImage: 'none', |  | ||||||
|         background: 'hsl(0, 0%, 100%)', |  | ||||||
|         boxShadow: |  | ||||||
|           'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px', |  | ||||||
|         [`& .${buttonBaseClasses.root}`]: { |  | ||||||
|           '&.Mui-selected': { |  | ||||||
|             backgroundColor: alpha(theme.palette.action.selected, 0.3), |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           background: gray[900], |  | ||||||
|           boxShadow: |  | ||||||
|             'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px', |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiSelect: { |  | ||||||
|     defaultProps: { |  | ||||||
|       IconComponent: React.forwardRef<SVGSVGElement, SvgIconProps>((props, ref) => ( |  | ||||||
|         <UnfoldMoreRoundedIcon fontSize="small" {...props} ref={ref} /> |  | ||||||
|       )), |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         border: '1px solid', |  | ||||||
|         borderColor: gray[200], |  | ||||||
|         backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|         boxShadow: `inset 0 1px 0 1px hsla(220, 0%, 100%, 0.6), inset 0 -1px 0 1px hsla(220, 35%, 90%, 0.5)`, |  | ||||||
|         '&:hover': { |  | ||||||
|           borderColor: gray[300], |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|           boxShadow: 'none', |  | ||||||
|         }, |  | ||||||
|         [`&.${selectClasses.focused}`]: { |  | ||||||
|           outlineOffset: 0, |  | ||||||
|           borderColor: gray[400], |  | ||||||
|         }, |  | ||||||
|         '&:before, &:after': { |  | ||||||
|           display: 'none', |  | ||||||
|         }, |  | ||||||
|  |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|           borderColor: gray[700], |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|           boxShadow: `inset 0 1px 0 1px ${alpha(gray[700], 0.15)}, inset 0 -1px 0 1px hsla(220, 0%, 0%, 0.7)`, |  | ||||||
|           '&:hover': { |  | ||||||
|             borderColor: alpha(gray[700], 0.7), |  | ||||||
|             backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|             boxShadow: 'none', |  | ||||||
|           }, |  | ||||||
|           [`&.${selectClasses.focused}`]: { |  | ||||||
|             outlineOffset: 0, |  | ||||||
|             borderColor: gray[900], |  | ||||||
|           }, |  | ||||||
|           '&:before, &:after': { |  | ||||||
|             display: 'none', |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|       select: ({ theme }) => ({ |  | ||||||
|         display: 'flex', |  | ||||||
|         alignItems: 'center', |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           display: 'flex', |  | ||||||
|           alignItems: 'center', |  | ||||||
|           '&:focus-visible': { |  | ||||||
|             backgroundColor: gray[900], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiLink: { |  | ||||||
|     defaultProps: { |  | ||||||
|       underline: 'none', |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         color: (theme.vars || theme).palette.text.primary, |  | ||||||
|         fontWeight: 500, |  | ||||||
|         position: 'relative', |  | ||||||
|         textDecoration: 'none', |  | ||||||
|         width: 'fit-content', |  | ||||||
|         '&::before': { |  | ||||||
|           content: '""', |  | ||||||
|           position: 'absolute', |  | ||||||
|           width: '100%', |  | ||||||
|           height: '1px', |  | ||||||
|           bottom: 0, |  | ||||||
|           left: 0, |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.text.secondary, |  | ||||||
|           opacity: 0.3, |  | ||||||
|           transition: 'width 0.3s ease, opacity 0.3s ease', |  | ||||||
|         }, |  | ||||||
|         '&:hover::before': { |  | ||||||
|           width: 0, |  | ||||||
|         }, |  | ||||||
|         '&:focus-visible': { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           outlineOffset: '4px', |  | ||||||
|           borderRadius: '2px', |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiDrawer: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       paper: ({ theme }) => ({ |  | ||||||
|         backgroundColor: (theme.vars || theme).palette.background.default, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPaginationItem: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         '&.Mui-selected': { |  | ||||||
|           color: 'white', |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.grey[900], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           '&.Mui-selected': { |  | ||||||
|             color: 'black', |  | ||||||
|             backgroundColor: (theme.vars || theme).palette.grey[50], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiTabs: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { minHeight: 'fit-content' }, |  | ||||||
|       indicator: ({ theme }) => ({ |  | ||||||
|         backgroundColor: (theme.vars || theme).palette.grey[800], |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.grey[200], |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiTab: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         padding: '6px 8px', |  | ||||||
|         marginBottom: '8px', |  | ||||||
|         textTransform: 'none', |  | ||||||
|         minWidth: 'fit-content', |  | ||||||
|         minHeight: 'fit-content', |  | ||||||
|         color: (theme.vars || theme).palette.text.secondary, |  | ||||||
|         borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         border: '1px solid', |  | ||||||
|         borderColor: 'transparent', |  | ||||||
|         ':hover': { |  | ||||||
|           color: (theme.vars || theme).palette.text.primary, |  | ||||||
|           backgroundColor: gray[100], |  | ||||||
|           borderColor: gray[200], |  | ||||||
|         }, |  | ||||||
|         [`&.${tabClasses.selected}`]: { |  | ||||||
|           color: gray[900], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           ':hover': { |  | ||||||
|             color: (theme.vars || theme).palette.text.primary, |  | ||||||
|             backgroundColor: gray[800], |  | ||||||
|             borderColor: gray[700], |  | ||||||
|           }, |  | ||||||
|           [`&.${tabClasses.selected}`]: { |  | ||||||
|             color: '#fff', |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiStepConnector: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       line: ({ theme }) => ({ |  | ||||||
|         borderTop: '1px solid', |  | ||||||
|         borderColor: (theme.vars || theme).palette.divider, |  | ||||||
|         flex: 1, |  | ||||||
|         borderRadius: '99px', |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiStepIcon: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         color: 'transparent', |  | ||||||
|         border: `1px solid ${gray[400]}`, |  | ||||||
|         width: 12, |  | ||||||
|         height: 12, |  | ||||||
|         borderRadius: '50%', |  | ||||||
|         '& text': { |  | ||||||
|           display: 'none', |  | ||||||
|         }, |  | ||||||
|         '&.Mui-active': { |  | ||||||
|           border: 'none', |  | ||||||
|           color: (theme.vars || theme).palette.primary.main, |  | ||||||
|         }, |  | ||||||
|         '&.Mui-completed': { |  | ||||||
|           border: 'none', |  | ||||||
|           color: (theme.vars || theme).palette.success.main, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           border: `1px solid ${gray[700]}`, |  | ||||||
|           '&.Mui-active': { |  | ||||||
|             border: 'none', |  | ||||||
|             color: (theme.vars || theme).palette.primary.light, |  | ||||||
|           }, |  | ||||||
|           '&.Mui-completed': { |  | ||||||
|             border: 'none', |  | ||||||
|             color: (theme.vars || theme).palette.success.light, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|         variants: [ |  | ||||||
|           { |  | ||||||
|             props: { completed: true }, |  | ||||||
|             style: { |  | ||||||
|               width: 12, |  | ||||||
|               height: 12, |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiStepLabel: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       label: ({ theme }) => ({ |  | ||||||
|         '&.Mui-completed': { |  | ||||||
|           opacity: 0.6, |  | ||||||
|           ...theme.applyStyles('dark', { opacity: 0.5 }), |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,113 +0,0 @@ | |||||||
| import { alpha, Theme, Components } from '@mui/material/styles'; |  | ||||||
| import { gray } from '../themePrimitives'; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const surfacesCustomizations: Components<Theme> = { |  | ||||||
|   MuiAccordion: { |  | ||||||
|     defaultProps: { |  | ||||||
|       elevation: 0, |  | ||||||
|       disableGutters: true, |  | ||||||
|     }, |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         padding: 4, |  | ||||||
|         overflow: 'clip', |  | ||||||
|         backgroundColor: (theme.vars || theme).palette.background.default, |  | ||||||
|         border: '1px solid', |  | ||||||
|         borderColor: (theme.vars || theme).palette.divider, |  | ||||||
|         ':before': { |  | ||||||
|           backgroundColor: 'transparent', |  | ||||||
|         }, |  | ||||||
|         '&:not(:last-of-type)': { |  | ||||||
|           borderBottom: 'none', |  | ||||||
|         }, |  | ||||||
|         '&:first-of-type': { |  | ||||||
|           borderTopLeftRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|           borderTopRightRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         }, |  | ||||||
|         '&:last-of-type': { |  | ||||||
|           borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|           borderBottomRightRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiAccordionSummary: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         border: 'none', |  | ||||||
|         borderRadius: 8, |  | ||||||
|         '&:hover': { backgroundColor: gray[50] }, |  | ||||||
|         '&:focus-visible': { backgroundColor: 'transparent' }, |  | ||||||
|         ...theme.applyStyles('dark', { |  | ||||||
|           '&:hover': { backgroundColor: gray[800] }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiAccordionDetails: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { mb: 20, border: 'none' }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPaper: { |  | ||||||
|     defaultProps: { |  | ||||||
|       elevation: 0, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiCard: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => { |  | ||||||
|         return { |  | ||||||
|           padding: 16, |  | ||||||
|           gap: 16, |  | ||||||
|           transition: 'all 100ms ease', |  | ||||||
|           backgroundColor: gray[50], |  | ||||||
|           borderRadius: (theme.vars || theme).shape.borderRadius, |  | ||||||
|           border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|           boxShadow: 'none', |  | ||||||
|           ...theme.applyStyles('dark', { |  | ||||||
|             backgroundColor: gray[800], |  | ||||||
|           }), |  | ||||||
|           variants: [ |  | ||||||
|             { |  | ||||||
|               props: { |  | ||||||
|                 variant: 'outlined', |  | ||||||
|               }, |  | ||||||
|               style: { |  | ||||||
|                 border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|                 boxShadow: 'none', |  | ||||||
|                 background: 'hsl(0, 0%, 100%)', |  | ||||||
|                 ...theme.applyStyles('dark', { |  | ||||||
|                   background: alpha(gray[900], 0.4), |  | ||||||
|                 }), |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|           ], |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiCardContent: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         padding: 0, |  | ||||||
|         '&:last-child': { paddingBottom: 0 }, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiCardHeader: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         padding: 0, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiCardActions: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         padding: 0, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,410 +0,0 @@ | |||||||
| /* eslint-disable @typescript-eslint/no-empty-object-type */ |  | ||||||
| import { alpha, createTheme, PaletteMode, Shadows } from "@mui/material/styles"; |  | ||||||
|  |  | ||||||
| declare module "@mui/material/Paper" { |  | ||||||
|   interface PaperPropsVariantOverrides { |  | ||||||
|     highlighted: true; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| declare module "@mui/material/styles/createPalette" { |  | ||||||
|   interface ColorRange { |  | ||||||
|     50: string; |  | ||||||
|     100: string; |  | ||||||
|     200: string; |  | ||||||
|     300: string; |  | ||||||
|     400: string; |  | ||||||
|     500: string; |  | ||||||
|     600: string; |  | ||||||
|     700: string; |  | ||||||
|     800: string; |  | ||||||
|     900: string; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   interface PaletteColor extends ColorRange {} |  | ||||||
|  |  | ||||||
|   interface Palette { |  | ||||||
|     baseShadow: string; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const defaultTheme = createTheme(); |  | ||||||
|  |  | ||||||
| const customShadows: Shadows = [...defaultTheme.shadows]; |  | ||||||
|  |  | ||||||
| export const brand = { |  | ||||||
|   50: "hsl(210, 100%, 95%)", |  | ||||||
|   100: "hsl(210, 100%, 92%)", |  | ||||||
|   200: "hsl(210, 100%, 80%)", |  | ||||||
|   300: "hsl(210, 100%, 65%)", |  | ||||||
|   400: "hsl(210, 98%, 48%)", |  | ||||||
|   500: "hsl(210, 98%, 42%)", |  | ||||||
|   600: "hsl(210, 98%, 55%)", |  | ||||||
|   700: "hsl(210, 100%, 35%)", |  | ||||||
|   800: "hsl(210, 100%, 16%)", |  | ||||||
|   900: "hsl(210, 100%, 21%)", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const gray = { |  | ||||||
|   50: "hsl(220, 35%, 97%)", |  | ||||||
|   100: "hsl(220, 30%, 94%)", |  | ||||||
|   200: "hsl(220, 20%, 88%)", |  | ||||||
|   300: "hsl(220, 20%, 80%)", |  | ||||||
|   400: "hsl(220, 20%, 65%)", |  | ||||||
|   500: "hsl(220, 20%, 42%)", |  | ||||||
|   600: "hsl(220, 20%, 35%)", |  | ||||||
|   700: "hsl(220, 20%, 25%)", |  | ||||||
|   800: "hsl(220, 30%, 6%)", |  | ||||||
|   900: "hsl(220, 35%, 3%)", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const green = { |  | ||||||
|   50: "hsl(120, 80%, 98%)", |  | ||||||
|   100: "hsl(120, 75%, 94%)", |  | ||||||
|   200: "hsl(120, 75%, 87%)", |  | ||||||
|   300: "hsl(120, 61%, 77%)", |  | ||||||
|   400: "hsl(120, 44%, 53%)", |  | ||||||
|   500: "hsl(120, 59%, 30%)", |  | ||||||
|   600: "hsl(120, 70%, 25%)", |  | ||||||
|   700: "hsl(120, 75%, 16%)", |  | ||||||
|   800: "hsl(120, 84%, 10%)", |  | ||||||
|   900: "hsl(120, 87%, 6%)", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const orange = { |  | ||||||
|   50: "hsl(45, 100%, 97%)", |  | ||||||
|   100: "hsl(45, 92%, 90%)", |  | ||||||
|   200: "hsl(45, 94%, 80%)", |  | ||||||
|   300: "hsl(45, 90%, 65%)", |  | ||||||
|   400: "hsl(45, 90%, 40%)", |  | ||||||
|   500: "hsl(45, 90%, 35%)", |  | ||||||
|   600: "hsl(45, 91%, 25%)", |  | ||||||
|   700: "hsl(45, 94%, 20%)", |  | ||||||
|   800: "hsl(45, 95%, 16%)", |  | ||||||
|   900: "hsl(45, 93%, 12%)", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const red = { |  | ||||||
|   50: "hsl(0, 100%, 97%)", |  | ||||||
|   100: "hsl(0, 92%, 90%)", |  | ||||||
|   200: "hsl(0, 94%, 80%)", |  | ||||||
|   300: "hsl(0, 90%, 65%)", |  | ||||||
|   400: "hsl(0, 90%, 40%)", |  | ||||||
|   500: "hsl(0, 90%, 30%)", |  | ||||||
|   600: "hsl(0, 91%, 25%)", |  | ||||||
|   700: "hsl(0, 94%, 18%)", |  | ||||||
|   800: "hsl(0, 95%, 12%)", |  | ||||||
|   900: "hsl(0, 93%, 6%)", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const getDesignTokens = (mode: PaletteMode) => { |  | ||||||
|   customShadows[1] = |  | ||||||
|     mode === "dark" |  | ||||||
|       ? "hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px" |  | ||||||
|       : "hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px"; |  | ||||||
|  |  | ||||||
|   return { |  | ||||||
|     palette: { |  | ||||||
|       mode, |  | ||||||
|       primary: { |  | ||||||
|         light: brand[200], |  | ||||||
|         main: brand[400], |  | ||||||
|         dark: brand[700], |  | ||||||
|         contrastText: brand[50], |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           contrastText: brand[50], |  | ||||||
|           light: brand[300], |  | ||||||
|           main: brand[400], |  | ||||||
|           dark: brand[700], |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       info: { |  | ||||||
|         light: brand[100], |  | ||||||
|         main: brand[300], |  | ||||||
|         dark: brand[600], |  | ||||||
|         contrastText: gray[50], |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           contrastText: brand[300], |  | ||||||
|           light: brand[500], |  | ||||||
|           main: brand[700], |  | ||||||
|           dark: brand[900], |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       warning: { |  | ||||||
|         light: orange[300], |  | ||||||
|         main: orange[400], |  | ||||||
|         dark: orange[800], |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           light: orange[400], |  | ||||||
|           main: orange[500], |  | ||||||
|           dark: orange[700], |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       error: { |  | ||||||
|         light: red[300], |  | ||||||
|         main: red[400], |  | ||||||
|         dark: red[800], |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           light: red[400], |  | ||||||
|           main: red[500], |  | ||||||
|           dark: red[700], |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       success: { |  | ||||||
|         light: green[300], |  | ||||||
|         main: green[400], |  | ||||||
|         dark: green[800], |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           light: green[400], |  | ||||||
|           main: green[500], |  | ||||||
|           dark: green[700], |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       grey: { |  | ||||||
|         ...gray, |  | ||||||
|       }, |  | ||||||
|       divider: mode === "dark" ? alpha(gray[700], 0.6) : alpha(gray[300], 0.4), |  | ||||||
|       background: { |  | ||||||
|         default: "hsl(0, 0%, 99%)", |  | ||||||
|         paper: "hsl(220, 35%, 97%)", |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           default: gray[900], |  | ||||||
|           paper: "hsl(220, 30%, 7%)", |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       text: { |  | ||||||
|         primary: gray[800], |  | ||||||
|         secondary: gray[600], |  | ||||||
|         warning: orange[400], |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           primary: "hsl(0, 0%, 100%)", |  | ||||||
|           secondary: gray[400], |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         hover: alpha(gray[200], 0.2), |  | ||||||
|         selected: `${alpha(gray[200], 0.3)}`, |  | ||||||
|         ...(mode === "dark" && { |  | ||||||
|           hover: alpha(gray[600], 0.2), |  | ||||||
|           selected: alpha(gray[600], 0.3), |  | ||||||
|         }), |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     typography: { |  | ||||||
|       fontFamily: "Inter, sans-serif", |  | ||||||
|       h1: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(48), |  | ||||||
|         fontWeight: 600, |  | ||||||
|         lineHeight: 1.2, |  | ||||||
|         letterSpacing: -0.5, |  | ||||||
|       }, |  | ||||||
|       h2: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(36), |  | ||||||
|         fontWeight: 600, |  | ||||||
|         lineHeight: 1.2, |  | ||||||
|       }, |  | ||||||
|       h3: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(30), |  | ||||||
|         lineHeight: 1.2, |  | ||||||
|       }, |  | ||||||
|       h4: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(24), |  | ||||||
|         fontWeight: 600, |  | ||||||
|         lineHeight: 1.5, |  | ||||||
|       }, |  | ||||||
|       h5: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(20), |  | ||||||
|         fontWeight: 600, |  | ||||||
|       }, |  | ||||||
|       h6: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(18), |  | ||||||
|         fontWeight: 600, |  | ||||||
|       }, |  | ||||||
|       subtitle1: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(18), |  | ||||||
|       }, |  | ||||||
|       subtitle2: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(14), |  | ||||||
|         fontWeight: 500, |  | ||||||
|       }, |  | ||||||
|       body1: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(14), |  | ||||||
|       }, |  | ||||||
|       body2: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(14), |  | ||||||
|         fontWeight: 400, |  | ||||||
|       }, |  | ||||||
|       caption: { |  | ||||||
|         fontSize: defaultTheme.typography.pxToRem(12), |  | ||||||
|         fontWeight: 400, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     shape: { |  | ||||||
|       borderRadius: 8, |  | ||||||
|     }, |  | ||||||
|     shadows: customShadows, |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const colorSchemes = { |  | ||||||
|   light: { |  | ||||||
|     palette: { |  | ||||||
|       primary: { |  | ||||||
|         light: brand[200], |  | ||||||
|         main: brand[400], |  | ||||||
|         dark: brand[700], |  | ||||||
|         contrastText: brand[50], |  | ||||||
|       }, |  | ||||||
|       info: { |  | ||||||
|         light: brand[100], |  | ||||||
|         main: brand[300], |  | ||||||
|         dark: brand[600], |  | ||||||
|         contrastText: gray[50], |  | ||||||
|       }, |  | ||||||
|       warning: { |  | ||||||
|         light: orange[300], |  | ||||||
|         main: orange[400], |  | ||||||
|         dark: orange[800], |  | ||||||
|       }, |  | ||||||
|       error: { |  | ||||||
|         light: red[300], |  | ||||||
|         main: red[400], |  | ||||||
|         dark: red[800], |  | ||||||
|       }, |  | ||||||
|       success: { |  | ||||||
|         light: green[300], |  | ||||||
|         main: green[400], |  | ||||||
|         dark: green[800], |  | ||||||
|       }, |  | ||||||
|       grey: { |  | ||||||
|         ...gray, |  | ||||||
|       }, |  | ||||||
|       divider: alpha(gray[300], 0.4), |  | ||||||
|       background: { |  | ||||||
|         default: "hsl(0, 0%, 99%)", |  | ||||||
|         paper: "hsl(220, 35%, 97%)", |  | ||||||
|       }, |  | ||||||
|       text: { |  | ||||||
|         primary: gray[800], |  | ||||||
|         secondary: gray[600], |  | ||||||
|         warning: orange[400], |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         hover: alpha(gray[200], 0.2), |  | ||||||
|         selected: `${alpha(gray[200], 0.3)}`, |  | ||||||
|       }, |  | ||||||
|       baseShadow: |  | ||||||
|         "hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px", |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   dark: { |  | ||||||
|     palette: { |  | ||||||
|       primary: { |  | ||||||
|         contrastText: brand[50], |  | ||||||
|         light: brand[300], |  | ||||||
|         main: brand[400], |  | ||||||
|         dark: brand[700], |  | ||||||
|       }, |  | ||||||
|       info: { |  | ||||||
|         contrastText: brand[300], |  | ||||||
|         light: brand[500], |  | ||||||
|         main: brand[700], |  | ||||||
|         dark: brand[900], |  | ||||||
|       }, |  | ||||||
|       warning: { |  | ||||||
|         light: orange[400], |  | ||||||
|         main: orange[500], |  | ||||||
|         dark: orange[700], |  | ||||||
|       }, |  | ||||||
|       error: { |  | ||||||
|         light: red[400], |  | ||||||
|         main: red[500], |  | ||||||
|         dark: red[700], |  | ||||||
|       }, |  | ||||||
|       success: { |  | ||||||
|         light: green[400], |  | ||||||
|         main: green[500], |  | ||||||
|         dark: green[700], |  | ||||||
|       }, |  | ||||||
|       grey: { |  | ||||||
|         ...gray, |  | ||||||
|       }, |  | ||||||
|       divider: alpha(gray[700], 0.6), |  | ||||||
|       background: { |  | ||||||
|         default: gray[900], |  | ||||||
|         paper: "hsl(220, 30%, 7%)", |  | ||||||
|       }, |  | ||||||
|       text: { |  | ||||||
|         primary: "hsl(0, 0%, 100%)", |  | ||||||
|         secondary: gray[400], |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         hover: alpha(gray[600], 0.2), |  | ||||||
|         selected: alpha(gray[600], 0.3), |  | ||||||
|       }, |  | ||||||
|       baseShadow: |  | ||||||
|         "hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px", |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const typography = { |  | ||||||
|   fontFamily: "Inter, sans-serif", |  | ||||||
|   h1: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(48), |  | ||||||
|     fontWeight: 600, |  | ||||||
|     lineHeight: 1.2, |  | ||||||
|     letterSpacing: -0.5, |  | ||||||
|   }, |  | ||||||
|   h2: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(36), |  | ||||||
|     fontWeight: 600, |  | ||||||
|     lineHeight: 1.2, |  | ||||||
|   }, |  | ||||||
|   h3: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(30), |  | ||||||
|     lineHeight: 1.2, |  | ||||||
|   }, |  | ||||||
|   h4: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(24), |  | ||||||
|     fontWeight: 600, |  | ||||||
|     lineHeight: 1.5, |  | ||||||
|   }, |  | ||||||
|   h5: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(20), |  | ||||||
|     fontWeight: 600, |  | ||||||
|   }, |  | ||||||
|   h6: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(18), |  | ||||||
|     fontWeight: 600, |  | ||||||
|   }, |  | ||||||
|   subtitle1: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(18), |  | ||||||
|   }, |  | ||||||
|   subtitle2: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(14), |  | ||||||
|     fontWeight: 500, |  | ||||||
|   }, |  | ||||||
|   body1: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(14), |  | ||||||
|   }, |  | ||||||
|   body2: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(14), |  | ||||||
|     fontWeight: 400, |  | ||||||
|   }, |  | ||||||
|   caption: { |  | ||||||
|     fontSize: defaultTheme.typography.pxToRem(12), |  | ||||||
|     fontWeight: 400, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const shape = { |  | ||||||
|   borderRadius: 8, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // @ts-ignore |  | ||||||
| const defaultShadows: Shadows = [ |  | ||||||
|   "none", |  | ||||||
|   "var(--template-palette-baseShadow)", |  | ||||||
|   ...defaultTheme.shadows.slice(2), |  | ||||||
| ]; |  | ||||||
| export const shadows = defaultShadows; |  | ||||||
| @ -1,76 +0,0 @@ | |||||||
| import { Theme } from "@mui/material/styles"; |  | ||||||
| import { axisClasses, chartsGridClasses, legendClasses } from "@mui/x-charts"; |  | ||||||
| import type { ChartsComponents } from "@mui/x-charts/themeAugmentation"; |  | ||||||
| import { gray } from "../../shared-theme/themePrimitives"; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const chartsCustomizations: ChartsComponents<Theme> = { |  | ||||||
|   MuiChartsAxis: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         [`& .${axisClasses.line}`]: { |  | ||||||
|           stroke: gray[300], |  | ||||||
|         }, |  | ||||||
|         [`& .${axisClasses.tick}`]: { stroke: gray[300] }, |  | ||||||
|         [`& .${axisClasses.tickLabel}`]: { |  | ||||||
|           fill: gray[500], |  | ||||||
|           fontWeight: 500, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           [`& .${axisClasses.line}`]: { |  | ||||||
|             stroke: gray[700], |  | ||||||
|           }, |  | ||||||
|           [`& .${axisClasses.tick}`]: { stroke: gray[700] }, |  | ||||||
|           [`& .${axisClasses.tickLabel}`]: { |  | ||||||
|             fill: gray[300], |  | ||||||
|             fontWeight: 500, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiChartsTooltip: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       mark: ({ theme }) => ({ |  | ||||||
|         ry: 6, |  | ||||||
|         boxShadow: "none", |  | ||||||
|         border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|       }), |  | ||||||
|       table: ({ theme }) => ({ |  | ||||||
|         border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|         borderRadius: theme.shape.borderRadius, |  | ||||||
|         background: "hsl(0, 0%, 100%)", |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           background: gray[900], |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiChartsLegend: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: { |  | ||||||
|         [`& .${legendClasses.mark}`]: { |  | ||||||
|           ry: 6, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiChartsGrid: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         [`& .${chartsGridClasses.line}`]: { |  | ||||||
|           stroke: gray[200], |  | ||||||
|           strokeDasharray: "4 2", |  | ||||||
|           strokeWidth: 0.8, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           [`& .${chartsGridClasses.line}`]: { |  | ||||||
|             stroke: gray[700], |  | ||||||
|             strokeDasharray: "4 2", |  | ||||||
|             strokeWidth: 0.8, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,138 +0,0 @@ | |||||||
| import { checkboxClasses } from "@mui/material/Checkbox"; |  | ||||||
| import { iconButtonClasses } from "@mui/material/IconButton"; |  | ||||||
| import { listClasses } from "@mui/material/List"; |  | ||||||
| import { listItemIconClasses } from "@mui/material/ListItemIcon"; |  | ||||||
| import { menuItemClasses } from "@mui/material/MenuItem"; |  | ||||||
| import { paperClasses } from "@mui/material/Paper"; |  | ||||||
| import { alpha, Theme } from "@mui/material/styles"; |  | ||||||
| import { tablePaginationClasses } from "@mui/material/TablePagination"; |  | ||||||
| import { gridClasses } from "@mui/x-data-grid"; |  | ||||||
| import type { DataGridProComponents } from "@mui/x-data-grid-pro/themeAugmentation"; |  | ||||||
| import type { DataGridComponents } from "@mui/x-data-grid/themeAugmentation"; |  | ||||||
| import { gray } from "../../shared-theme/themePrimitives"; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const dataGridCustomizations: DataGridProComponents<Theme> & |  | ||||||
|   DataGridComponents<Theme> = { |  | ||||||
|   MuiDataGrid: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         "--DataGrid-overlayHeight": "300px", |  | ||||||
|         overflow: "clip", |  | ||||||
|         borderColor: (theme.vars || theme).palette.divider, |  | ||||||
|         backgroundColor: (theme.vars || theme).palette.background.default, |  | ||||||
|         [`& .${gridClasses.columnHeader}`]: { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|         }, |  | ||||||
|         [`& .${gridClasses.footerContainer}`]: { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.background.paper, |  | ||||||
|         }, |  | ||||||
|         [`& .${checkboxClasses.root}`]: { |  | ||||||
|           padding: theme.spacing(0.5), |  | ||||||
|           "& > svg": { |  | ||||||
|             fontSize: "1rem", |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         [`& .${tablePaginationClasses.root}`]: { |  | ||||||
|           marginRight: theme.spacing(1), |  | ||||||
|           "& .MuiIconButton-root": { |  | ||||||
|             maxHeight: 32, |  | ||||||
|             maxWidth: 32, |  | ||||||
|             "& > svg": { |  | ||||||
|               fontSize: "1rem", |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|       cell: ({ theme }) => ({ |  | ||||||
|         borderTopColor: (theme.vars || theme).palette.divider, |  | ||||||
|       }), |  | ||||||
|       menu: ({ theme }) => ({ |  | ||||||
|         borderRadius: theme.shape.borderRadius, |  | ||||||
|         backgroundImage: "none", |  | ||||||
|         [`& .${paperClasses.root}`]: { |  | ||||||
|           border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|         }, |  | ||||||
|  |  | ||||||
|         [`& .${menuItemClasses.root}`]: { |  | ||||||
|           margin: "0 4px", |  | ||||||
|         }, |  | ||||||
|         [`& .${listItemIconClasses.root}`]: { |  | ||||||
|           marginRight: 0, |  | ||||||
|         }, |  | ||||||
|         [`& .${listClasses.root}`]: { |  | ||||||
|           paddingLeft: 0, |  | ||||||
|           paddingRight: 0, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|  |  | ||||||
|       row: ({ theme }) => ({ |  | ||||||
|         "&:last-of-type": { |  | ||||||
|           borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|         }, |  | ||||||
|         "&:hover": { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|         }, |  | ||||||
|         "&.Mui-selected": { |  | ||||||
|           background: (theme.vars || theme).palette.action.selected, |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|       iconButtonContainer: ({ theme }) => ({ |  | ||||||
|         [`& .${iconButtonClasses.root}`]: { |  | ||||||
|           border: "none", |  | ||||||
|           backgroundColor: "transparent", |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: alpha(theme.palette.action.selected, 0.3), |  | ||||||
|           }, |  | ||||||
|           "&:active": { |  | ||||||
|             backgroundColor: gray[200], |  | ||||||
|           }, |  | ||||||
|           ...theme.applyStyles("dark", { |  | ||||||
|             color: gray[50], |  | ||||||
|             "&:hover": { |  | ||||||
|               backgroundColor: gray[800], |  | ||||||
|             }, |  | ||||||
|             "&:active": { |  | ||||||
|               backgroundColor: gray[900], |  | ||||||
|             }, |  | ||||||
|           }), |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|       menuIconButton: ({ theme }) => ({ |  | ||||||
|         border: "none", |  | ||||||
|         backgroundColor: "transparent", |  | ||||||
|         "&:hover": { |  | ||||||
|           backgroundColor: gray[100], |  | ||||||
|         }, |  | ||||||
|         "&:active": { |  | ||||||
|           backgroundColor: gray[200], |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           color: gray[50], |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: gray[800], |  | ||||||
|           }, |  | ||||||
|           "&:active": { |  | ||||||
|             backgroundColor: gray[900], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|       filterForm: ({ theme }) => ({ |  | ||||||
|         gap: theme.spacing(1), |  | ||||||
|         alignItems: "flex-end", |  | ||||||
|       }), |  | ||||||
|       columnsManagementHeader: ({ theme }) => ({ |  | ||||||
|         paddingRight: theme.spacing(3), |  | ||||||
|         paddingLeft: theme.spacing(3), |  | ||||||
|       }), |  | ||||||
|       columnHeaderTitleContainer: { |  | ||||||
|         flexGrow: 1, |  | ||||||
|         justifyContent: "space-between", |  | ||||||
|       }, |  | ||||||
|       columnHeaderDraggableContainer: { paddingRight: 2 }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,182 +0,0 @@ | |||||||
| import { menuItemClasses } from "@mui/material/MenuItem"; |  | ||||||
| import { alpha, Theme } from "@mui/material/styles"; |  | ||||||
| import { |  | ||||||
|   pickersDayClasses, |  | ||||||
|   pickersMonthClasses, |  | ||||||
|   pickersYearClasses, |  | ||||||
| } from "@mui/x-date-pickers"; |  | ||||||
| import type { PickersProComponents } from "@mui/x-date-pickers-pro/themeAugmentation"; |  | ||||||
| import type { PickerComponents } from "@mui/x-date-pickers/themeAugmentation"; |  | ||||||
| import { brand, gray } from "../../shared-theme/themePrimitives"; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const datePickersCustomizations: PickersProComponents<Theme> & |  | ||||||
|   PickerComponents<Theme> = { |  | ||||||
|   MuiPickersPopper: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       paper: ({ theme }) => ({ |  | ||||||
|         marginTop: 4, |  | ||||||
|         borderRadius: theme.shape.borderRadius, |  | ||||||
|         border: `1px solid ${(theme.vars || theme).palette.divider}`, |  | ||||||
|         backgroundImage: "none", |  | ||||||
|         background: "hsl(0, 0%, 100%)", |  | ||||||
|         boxShadow: |  | ||||||
|           "hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px", |  | ||||||
|         [`& .${menuItemClasses.root}`]: { |  | ||||||
|           borderRadius: 6, |  | ||||||
|           margin: "0 6px", |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           background: gray[900], |  | ||||||
|           boxShadow: |  | ||||||
|             "hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px", |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPickersArrowSwitcher: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       spacer: { width: 16 }, |  | ||||||
|       button: ({ theme }) => ({ |  | ||||||
|         backgroundColor: "transparent", |  | ||||||
|         color: (theme.vars || theme).palette.grey[500], |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           color: (theme.vars || theme).palette.grey[400], |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPickersCalendarHeader: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       switchViewButton: { |  | ||||||
|         padding: 0, |  | ||||||
|         border: "none", |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPickersMonth: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       monthButton: ({ theme }) => ({ |  | ||||||
|         fontSize: theme.typography.body1.fontSize, |  | ||||||
|         color: (theme.vars || theme).palette.grey[600], |  | ||||||
|         padding: theme.spacing(0.5), |  | ||||||
|         borderRadius: theme.shape.borderRadius, |  | ||||||
|         "&:hover": { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|         }, |  | ||||||
|         [`&.${pickersMonthClasses.selected}`]: { |  | ||||||
|           backgroundColor: gray[700], |  | ||||||
|           fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|         }, |  | ||||||
|         "&:focus": { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           outlineOffset: "2px", |  | ||||||
|           backgroundColor: "transparent", |  | ||||||
|           [`&.${pickersMonthClasses.selected}`]: { backgroundColor: gray[700] }, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           color: (theme.vars || theme).palette.grey[300], |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|           }, |  | ||||||
|           [`&.${pickersMonthClasses.selected}`]: { |  | ||||||
|             color: (theme.vars || theme).palette.common.black, |  | ||||||
|             fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|             backgroundColor: gray[300], |  | ||||||
|           }, |  | ||||||
|           "&:focus": { |  | ||||||
|             outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|             outlineOffset: "2px", |  | ||||||
|             backgroundColor: "transparent", |  | ||||||
|             [`&.${pickersMonthClasses.selected}`]: { |  | ||||||
|               backgroundColor: gray[300], |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPickersYear: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       yearButton: ({ theme }) => ({ |  | ||||||
|         fontSize: theme.typography.body1.fontSize, |  | ||||||
|         color: (theme.vars || theme).palette.grey[600], |  | ||||||
|         padding: theme.spacing(0.5), |  | ||||||
|         borderRadius: theme.shape.borderRadius, |  | ||||||
|         height: "fit-content", |  | ||||||
|         "&:hover": { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|         }, |  | ||||||
|         [`&.${pickersYearClasses.selected}`]: { |  | ||||||
|           backgroundColor: gray[700], |  | ||||||
|           fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|         }, |  | ||||||
|         "&:focus": { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           outlineOffset: "2px", |  | ||||||
|           backgroundColor: "transparent", |  | ||||||
|           [`&.${pickersYearClasses.selected}`]: { backgroundColor: gray[700] }, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           color: (theme.vars || theme).palette.grey[300], |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|           }, |  | ||||||
|           [`&.${pickersYearClasses.selected}`]: { |  | ||||||
|             color: (theme.vars || theme).palette.common.black, |  | ||||||
|             fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|             backgroundColor: gray[300], |  | ||||||
|           }, |  | ||||||
|           "&:focus": { |  | ||||||
|             outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|             outlineOffset: "2px", |  | ||||||
|             backgroundColor: "transparent", |  | ||||||
|             [`&.${pickersYearClasses.selected}`]: { |  | ||||||
|               backgroundColor: gray[300], |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   MuiPickersDay: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         fontSize: theme.typography.body1.fontSize, |  | ||||||
|         color: (theme.vars || theme).palette.grey[600], |  | ||||||
|         padding: theme.spacing(0.5), |  | ||||||
|         borderRadius: theme.shape.borderRadius, |  | ||||||
|         "&:hover": { |  | ||||||
|           backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|         }, |  | ||||||
|         [`&.${pickersDayClasses.selected}`]: { |  | ||||||
|           backgroundColor: gray[700], |  | ||||||
|           fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|         }, |  | ||||||
|         "&:focus": { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           outlineOffset: "2px", |  | ||||||
|           backgroundColor: "transparent", |  | ||||||
|           [`&.${pickersDayClasses.selected}`]: { backgroundColor: gray[700] }, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           color: (theme.vars || theme).palette.grey[300], |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: (theme.vars || theme).palette.action.hover, |  | ||||||
|           }, |  | ||||||
|           [`&.${pickersDayClasses.selected}`]: { |  | ||||||
|             color: (theme.vars || theme).palette.common.black, |  | ||||||
|             fontWeight: theme.typography.fontWeightMedium, |  | ||||||
|             backgroundColor: gray[300], |  | ||||||
|           }, |  | ||||||
|           "&:focus": { |  | ||||||
|             outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|             outlineOffset: "2px", |  | ||||||
|             backgroundColor: "transparent", |  | ||||||
|             [`&.${pickersDayClasses.selected}`]: { backgroundColor: gray[300] }, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,4 +0,0 @@ | |||||||
| export { chartsCustomizations } from './charts'; |  | ||||||
| export { dataGridCustomizations } from './dataGrid'; |  | ||||||
| export { datePickersCustomizations } from './datePickers'; |  | ||||||
| export { treeViewCustomizations } from './treeView'; |  | ||||||
| @ -1,62 +0,0 @@ | |||||||
| import { alpha, Theme } from "@mui/material/styles"; |  | ||||||
| import type { TreeViewComponents } from "@mui/x-tree-view/themeAugmentation"; |  | ||||||
| import { brand, gray } from "../../shared-theme/themePrimitives"; |  | ||||||
|  |  | ||||||
| /* eslint-disable import/prefer-default-export */ |  | ||||||
| export const treeViewCustomizations: TreeViewComponents<Theme> = { |  | ||||||
|   MuiTreeItem2: { |  | ||||||
|     styleOverrides: { |  | ||||||
|       root: ({ theme }) => ({ |  | ||||||
|         position: "relative", |  | ||||||
|         boxSizing: "border-box", |  | ||||||
|         padding: theme.spacing(0, 1), |  | ||||||
|         "& .groupTransition": { |  | ||||||
|           marginLeft: theme.spacing(2), |  | ||||||
|           padding: theme.spacing(0), |  | ||||||
|           borderLeft: "1px solid", |  | ||||||
|           borderColor: (theme.vars || theme).palette.divider, |  | ||||||
|         }, |  | ||||||
|         "&:focus-visible .focused": { |  | ||||||
|           outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|           outlineOffset: "2px", |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: alpha(gray[300], 0.2), |  | ||||||
|             outline: `3px solid ${alpha(brand[500], 0.5)}`, |  | ||||||
|             outlineOffset: "2px", |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }), |  | ||||||
|       content: ({ theme }) => ({ |  | ||||||
|         marginTop: theme.spacing(1), |  | ||||||
|         padding: theme.spacing(0.5, 1), |  | ||||||
|         overflow: "clip", |  | ||||||
|         "&:hover": { |  | ||||||
|           backgroundColor: alpha(gray[300], 0.2), |  | ||||||
|         }, |  | ||||||
|  |  | ||||||
|         "&.selected": { |  | ||||||
|           backgroundColor: alpha(gray[300], 0.4), |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: alpha(gray[300], 0.6), |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         ...theme.applyStyles("dark", { |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: alpha(gray[500], 0.2), |  | ||||||
|           }, |  | ||||||
|           "&:focus-visible": { |  | ||||||
|             "&:hover": { |  | ||||||
|               backgroundColor: alpha(gray[500], 0.2), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           "&.selected": { |  | ||||||
|             backgroundColor: alpha(gray[500], 0.4), |  | ||||||
|             "&:hover": { |  | ||||||
|               backgroundColor: alpha(gray[500], 0.6), |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
							
								
								
									
										1
									
								
								webClient/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								webClient/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1 +0,0 @@ | |||||||
| /// <reference types="vite/types/importMeta.d.ts" /> |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| { |  | ||||||
|   "compilerOptions": { |  | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", |  | ||||||
|     "target": "ES2020", |  | ||||||
|     "useDefineForClassFields": true, |  | ||||||
|     "lib": ["ES2020", "DOM", "DOM.Iterable"], |  | ||||||
|     "module": "ESNext", |  | ||||||
|     "skipLibCheck": true, |  | ||||||
|  |  | ||||||
|     /* Bundler mode */ |  | ||||||
|     "moduleResolution": "bundler", |  | ||||||
|     "allowImportingTsExtensions": true, |  | ||||||
|     "isolatedModules": true, |  | ||||||
|     "moduleDetection": "force", |  | ||||||
|     "noEmit": true, |  | ||||||
|     "jsx": "react-jsx", |  | ||||||
|  |  | ||||||
|     /* Linting */ |  | ||||||
|     "strict": true, |  | ||||||
|     "noUnusedLocals": true, |  | ||||||
|     "noUnusedParameters": true, |  | ||||||
|     "noFallthroughCasesInSwitch": true, |  | ||||||
|     "noUncheckedSideEffectImports": true |  | ||||||
|   }, |  | ||||||
|   "include": ["src"] |  | ||||||
| } |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "files": [], |  | ||||||
|   "references": [ |  | ||||||
|     { "path": "./tsconfig.app.json" }, |  | ||||||
|     { "path": "./tsconfig.node.json" } |  | ||||||
|   ] |  | ||||||
| } |  | ||||||
| @ -1,24 +0,0 @@ | |||||||
| { |  | ||||||
|   "compilerOptions": { |  | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", |  | ||||||
|     "target": "ES2022", |  | ||||||
|     "lib": ["ES2023"], |  | ||||||
|     "module": "ESNext", |  | ||||||
|     "skipLibCheck": true, |  | ||||||
|  |  | ||||||
|     /* Bundler mode */ |  | ||||||
|     "moduleResolution": "bundler", |  | ||||||
|     "allowImportingTsExtensions": true, |  | ||||||
|     "isolatedModules": true, |  | ||||||
|     "moduleDetection": "force", |  | ||||||
|     "noEmit": true, |  | ||||||
|  |  | ||||||
|     /* Linting */ |  | ||||||
|     "strict": true, |  | ||||||
|     "noUnusedLocals": true, |  | ||||||
|     "noUnusedParameters": true, |  | ||||||
|     "noFallthroughCasesInSwitch": true, |  | ||||||
|     "noUncheckedSideEffectImports": true |  | ||||||
|   }, |  | ||||||
|   "include": ["vite.config.ts"] |  | ||||||
| } |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| import { defineConfig } from 'vite' |  | ||||||
| import react from '@vitejs/plugin-react-swc' |  | ||||||
|  |  | ||||||
| // https://vite.dev/config/ |  | ||||||
| export default defineConfig({ |  | ||||||
|   plugins: [react()], |  | ||||||
| }) |  | ||||||
		Reference in New Issue
	
	Block a user