From 619629898765a8c292e5aecb04276bdc7a9f046c Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Sat, 13 Dec 2025 10:27:44 -0800 Subject: [PATCH 01/27] Initial commit: vFDIO ERAM terminal with FP and AM message support --- .gitignore | 8 + .postcssrc | 5 + ".postcssrc\357\200\272Zone.Identifier" | 3 + CUSTOM_FLIGHTPLAN_COMMANDS.md | 108 + package-lock.json | 5244 +++++++++++++++++ package.json | 46 + src/App.tsx | 991 ++++ src/api/vNasDataApi.ts | 34 + src/components/Header.tsx | 26 + src/components/InputArea.tsx | 72 + src/components/Recat.tsx | 9 + src/contexts/HubContext.tsx | 505 ++ src/contexts/SocketContext.tsx | 13 + src/hooks/useHubConnector.ts | 12 + src/hooks/useSocketConnector.ts | 10 + src/index.html | 9 + src/index.tsx | 9 + src/login/Login.tsx | 142 + src/redux/hooks.ts | 5 + src/redux/slices/appSlice.ts | 31 + src/redux/slices/authSlice.ts | 214 + src/redux/slices/customFlightplanSlice.ts | 209 + src/redux/slices/mcaSlice.ts | 32 + src/redux/slices/sectorSlice.ts | 27 + src/redux/store.ts | 19 + src/redux/thunks/index.ts | 47 + src/redux/thunks/windowThunks.ts | 10 + src/services/customFlightplanCommandParser.ts | 818 +++ src/services/customFlightplanService.ts | 230 + src/styles/fonts/FDIOv1.ttf | Bin 0 -> 46876 bytes src/styles/terminal.css | 11 + src/types/aircraftId.ts | 1 + src/types/apiTypes/apiFlightplan.ts | 59 + src/types/apiTypes/apiSessionInfoDto.ts | 24 + src/types/apiTypes/apiTopic.ts | 8 + src/types/apiTypes/eramTypes.ts | 23 + src/types/apiTypes/index.ts | 19 + src/types/hold/holdAnnotations.ts | 4 + src/types/outageEntry.ts | 3 + src/types/utility-types.ts | 1 + src/utils/constants.ts | 4 + src/utils/hubUtils.ts | 35 + 42 files changed, 9080 insertions(+) create mode 100644 .gitignore create mode 100644 .postcssrc create mode 100644 ".postcssrc\357\200\272Zone.Identifier" create mode 100644 CUSTOM_FLIGHTPLAN_COMMANDS.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/api/vNasDataApi.ts create mode 100644 src/components/Header.tsx create mode 100644 src/components/InputArea.tsx create mode 100644 src/components/Recat.tsx create mode 100644 src/contexts/HubContext.tsx create mode 100644 src/contexts/SocketContext.tsx create mode 100644 src/hooks/useHubConnector.ts create mode 100644 src/hooks/useSocketConnector.ts create mode 100644 src/index.html create mode 100644 src/index.tsx create mode 100644 src/login/Login.tsx create mode 100644 src/redux/hooks.ts create mode 100644 src/redux/slices/appSlice.ts create mode 100644 src/redux/slices/authSlice.ts create mode 100644 src/redux/slices/customFlightplanSlice.ts create mode 100644 src/redux/slices/mcaSlice.ts create mode 100644 src/redux/slices/sectorSlice.ts create mode 100644 src/redux/store.ts create mode 100644 src/redux/thunks/index.ts create mode 100644 src/redux/thunks/windowThunks.ts create mode 100644 src/services/customFlightplanCommandParser.ts create mode 100644 src/services/customFlightplanService.ts create mode 100644 src/styles/fonts/FDIOv1.ttf create mode 100644 src/styles/terminal.css create mode 100644 src/types/aircraftId.ts create mode 100644 src/types/apiTypes/apiFlightplan.ts create mode 100644 src/types/apiTypes/apiSessionInfoDto.ts create mode 100644 src/types/apiTypes/apiTopic.ts create mode 100644 src/types/apiTypes/eramTypes.ts create mode 100644 src/types/apiTypes/index.ts create mode 100644 src/types/hold/holdAnnotations.ts create mode 100644 src/types/outageEntry.ts create mode 100644 src/types/utility-types.ts create mode 100644 src/utils/constants.ts create mode 100644 src/utils/hubUtils.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35630d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.parcel-cache/ +.env +.DS_Store +*.log +.vscode/ +coverage/ diff --git a/.postcssrc b/.postcssrc new file mode 100644 index 0000000..72f908d --- /dev/null +++ b/.postcssrc @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} \ No newline at end of file diff --git "a/.postcssrc\357\200\272Zone.Identifier" "b/.postcssrc\357\200\272Zone.Identifier" new file mode 100644 index 0000000..ccde74c --- /dev/null +++ "b/.postcssrc\357\200\272Zone.Identifier" @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=https://drive.usercontent.google.com/download?id=1iGkHH39rEFOHEDjznbzXhyafayt5nyhG&export=download&authuser=0&confirm=t&uuid=fb85a7a6-76ac-48f7-8912-7c768145bc9a&at=AN8xHoq109b4hfEhXsocz57Dilfh%3A1758315864130 diff --git a/CUSTOM_FLIGHTPLAN_COMMANDS.md b/CUSTOM_FLIGHTPLAN_COMMANDS.md new file mode 100644 index 0000000..5c9f042 --- /dev/null +++ b/CUSTOM_FLIGHTPLAN_COMMANDS.md @@ -0,0 +1,108 @@ +# Custom Flightplan Commands + +This system provides direct access to flightplan DTO objects with custom commands, allowing you to view and render flightplan data without relying on predefined hub commands. + +## Available Commands + +### Primary Command + +- **`FR `** - ERAM-style flight readout for specific aircraft + - Example: `FR UAL123` + - Shows formatted flight data like traditional ERAM systems + - Returns "REJECT - FLID NOT STORED" if aircraft not found + - Returns "REJECT - FLID DUPLICATION" with CID list if multiple matches + +### Basic Commands + +- **`FP `** - Display detailed flightplan for a specific aircraft + - Example: `FP UAL123` + +- **`FPHELP`** - Display help message with all available commands + +### List Commands + +- **`FPL [status]`** - List flightplans by status + - `FPL` or `FPL ALL` - Show all flightplans + - `FPL ACTIVE` - Show only active flightplans + - `FPL PROPOSED` - Show only proposed flightplans + - `FPL TENTATIVE` - Show only tentative flightplans + +### Search Commands + +- **`FPS [filters]`** - Search flightplans with multiple filters + - Filters: `status=`, `dep=`, `dest=`, `alt=`, `route=`, `acid=` + - Example: `FPS status=Active dep=KJFK` + - Example: `FPS dep=KJFK dest=KLAX` + +- **`FPF `** - Find flightplans by specific criteria + - Criteria: `DEP`, `DEST`, `WAYPOINT`, `ALT`, `TYPE` + - Example: `FPF DEP KJFK` - Find flights departing from JFK + - Example: `FPF WAYPOINT HOFFA` - Find flights routing via HOFFA waypoint + - Example: `FPF TYPE B738` - Find flights using Boeing 737-800 + +### Statistics Command + +- **`FPSTATS`** - Display comprehensive flightplan statistics + - Shows total counts by status + - Top aircraft types + - Average speed + - Altitude distribution + +## Usage Examples + +``` +FR UAL123 # ERAM-style flight readout for UAL123 +FP UAL123 # Show detailed info for UAL123 +FPS dep=KJFK dest=KLAX # Find all flights from JFK to LAX +FPL ACTIVE # List all active flightplans +FPF WAYPOINT HOFFA # Find flights via HOFFA waypoint +FPF DEP KORD # Find all departures from Chicago O'Hare +FPSTATS # Show statistics summary +``` + +## Features + +### Direct DTO Access +- Access to complete `ApiFlightplan` objects +- No dependency on hub-specific commands +- Full control over data filtering and display + +### Rich Filtering +- Filter by aircraft ID, departure, destination, altitude, status, route +- Combine multiple filters in search commands +- Case-insensitive searching + +### Multiple Display Modes +- Table view for multiple flightplans +- Detailed view for individual flightplans +- Statistics view for data analysis + +### Real-time Updates +- Automatically syncs with hub flightplan data +- Redux state management for consistency +- Command history tracking + +## Implementation Details + +### Files Created +- `src/services/customFlightplanService.ts` - Core flightplan operations +- `src/services/customFlightplanCommandParser.ts` - Command parsing logic +- `src/hooks/useCustomFlightplanCommands.ts` - React hook for integration +- `src/components/FlightplanVisualization.tsx` - UI components +- `src/redux/slices/customFlightplanSlice.ts` - State management + +### Integration +The system integrates seamlessly with your existing terminal interface in `App.tsx`. Commands are automatically detected and routed to the custom parser while preserving normal hub command functionality. + +### Data Sources +- Pulls flightplan data from your existing `HubContext` +- Works with the existing `Map` structure +- Maintains compatibility with current hub operations + +## Status Display +The terminal now shows: +- Current flightplan count +- Hub connection status +- Reminder to use `FPHELP` for custom commands + +This gives you complete control over flightplan data visualization and manipulation while maintaining integration with your existing vFDIO system. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8bffbce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5244 @@ +{ + "name": "vfdio", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vfdio", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.0.0", + "@fortawesome/free-solid-svg-icons": "^6.0.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "@microsoft/signalr": "^8.0.0", + "@reduxjs/toolkit": "^2.0.0", + "@tailwindcss/postcss": "^4.1.11", + "jose": "^5.0.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-redux": "^9.0.0", + "react-router-dom": "^6.0.0", + "react-toastify": "^10.0.0", + "tailwindcss": "^4.1.11" + }, + "devDependencies": { + "@types/node": "^24.1.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.1", + "events": "^3.3.0", + "os-browserify": "^0.3.0", + "parcel": "^2.15.4", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "stream-browserify": "^3.0.0", + "string_decoder": "^1.3.0", + "vm-browserify": "^1.1.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.3.tgz", + "integrity": "sha512-HlJco8RDY8NrzFVjy23b/7mNS4g9NegcrBG3n7jinwpc2x/AmSVk53IhWniLYM4szYLxRAFTAGwGn0EIlclDeQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", + "react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz", + "integrity": "sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.8.5.tgz", + "integrity": "sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.8.5.tgz", + "integrity": "sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.8.5.tgz", + "integrity": "sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.8.5.tgz", + "integrity": "sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.8.5.tgz", + "integrity": "sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@microsoft/signalr": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz", + "integrity": "sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@mischnic/json-sourcemap": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", + "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/lr": "^1.0.0", + "json5": "^2.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@parcel/bundler-default": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.15.4.tgz", + "integrity": "sha512-4vkaZuwGqL8L7NqEgjRznz9/QoeVKk0Z6z2nzfpdnSWA4xX3moUj+JeoqGUbyFGuPzfCma4SA4+txnQbKu0edQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/graph": "3.5.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/cache": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.15.4.tgz", + "integrity": "sha512-x/QgMuVvXQV6uNhIF+6kz6SzhVVkwf6WPSVG/xQvGMEiBabForDVYIhIEuN3RzUXCU352CGM6d8TtLLg61W1fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/fs": "2.15.4", + "@parcel/logger": "2.15.4", + "@parcel/utils": "2.15.4", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/codeframe": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.15.4.tgz", + "integrity": "sha512-ErAPEQaJIpB+ocNZ3rl8AEK6piA7JBInwZLNU0eHMthm01Ssb10JkpAadyn1w9IVfCey+kqQcEeWv47Yh6mL1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/compressor-raw": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.15.4.tgz", + "integrity": "sha512-gECePZxVXBwyo0DYbAq4V4SimVzHaJ3p8QOgFIfOqNmlEBbhLf3QSjArFPJNKiHZaJuclh4a+IShFBN+u6tXXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/config-default": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.15.4.tgz", + "integrity": "sha512-chUE4NpcSXpMfTcSmgl4Q78zH+ZFe0qdgZLBtF4EH2QQakW7wAXAYRxS2/P3xFkUj0/51sExhbCFWgulrlGDPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/bundler-default": "2.15.4", + "@parcel/compressor-raw": "2.15.4", + "@parcel/namer-default": "2.15.4", + "@parcel/optimizer-css": "2.15.4", + "@parcel/optimizer-html": "2.15.4", + "@parcel/optimizer-image": "2.15.4", + "@parcel/optimizer-svg": "2.15.4", + "@parcel/optimizer-swc": "2.15.4", + "@parcel/packager-css": "2.15.4", + "@parcel/packager-html": "2.15.4", + "@parcel/packager-js": "2.15.4", + "@parcel/packager-raw": "2.15.4", + "@parcel/packager-svg": "2.15.4", + "@parcel/packager-wasm": "2.15.4", + "@parcel/reporter-dev-server": "2.15.4", + "@parcel/resolver-default": "2.15.4", + "@parcel/runtime-browser-hmr": "2.15.4", + "@parcel/runtime-js": "2.15.4", + "@parcel/runtime-rsc": "2.15.4", + "@parcel/runtime-service-worker": "2.15.4", + "@parcel/transformer-babel": "2.15.4", + "@parcel/transformer-css": "2.15.4", + "@parcel/transformer-html": "2.15.4", + "@parcel/transformer-image": "2.15.4", + "@parcel/transformer-js": "2.15.4", + "@parcel/transformer-json": "2.15.4", + "@parcel/transformer-node": "2.15.4", + "@parcel/transformer-postcss": "2.15.4", + "@parcel/transformer-posthtml": "2.15.4", + "@parcel/transformer-raw": "2.15.4", + "@parcel/transformer-react-refresh-wrap": "2.15.4", + "@parcel/transformer-svg": "2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/core": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.15.4.tgz", + "integrity": "sha512-+TXxTm58lFwXXObFAEclwKX1p1AdixcD+M7T4NeFIQzQ4F20Vr+6oybCSqW1exNA3uHqVDDFLx7TT78seVjvkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "@parcel/cache": "2.15.4", + "@parcel/diagnostic": "2.15.4", + "@parcel/events": "2.15.4", + "@parcel/feature-flags": "2.15.4", + "@parcel/fs": "2.15.4", + "@parcel/graph": "3.5.4", + "@parcel/logger": "2.15.4", + "@parcel/package-manager": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/profiler": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.15.4", + "@parcel/utils": "2.15.4", + "@parcel/workers": "2.15.4", + "base-x": "^3.0.11", + "browserslist": "^4.24.5", + "clone": "^2.1.2", + "dotenv": "^16.5.0", + "dotenv-expand": "^11.0.7", + "json5": "^2.2.3", + "msgpackr": "^1.11.2", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@parcel/diagnostic": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.15.4.tgz", + "integrity": "sha512-8MAqefwzBKceNN3364OLm+p4HRD7AfimfFW3MntLxPB6bnelc9UBg5c9zEm34zYEctbmky8gqYgAUSDjqYC5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/error-overlay": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/error-overlay/-/error-overlay-2.15.4.tgz", + "integrity": "sha512-xxeaWm8fV8Z4uGy/c09mOvmFSHBOgF1gCMQwLCwZvfMLqIWkdZaUQ2cRhWZIS6pOXaRVC7YpcXzk2DOiSUNSbQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/events": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.15.4.tgz", + "integrity": "sha512-SBq4zstaFr7XQaXNaQmUuVh1swCUHrhtPCOSofvkJoQGhjsuhQlh4t0NmUikyKNdj7C1j40xCS1kGHuUO29b0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/feature-flags": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.15.4.tgz", + "integrity": "sha512-DJqZVtbfjWJseM0gk7yyDkAuOhP7/FVwZ/YVqjozIqXBhmQm07xctiqNQyZX2vBbQsxmVbjpqyq+DOj45WPEzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/fs": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.15.4.tgz", + "integrity": "sha512-5cahD2ByQaSi+YN0aDvrMWXZvs3mP7C5ey8zcDTDn7JxJa51sMqOQcdU3VUTzQFtAPeRM2KxUkxLhBBXgQqHZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/types-internal": "2.15.4", + "@parcel/utils": "2.15.4", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/graph": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.5.4.tgz", + "integrity": "sha512-uF7kyQXWK2fQZvG5eE0N3avYGLQE5Q0vyJsyypNcFW3kXNnrkZCUtbG7urmdae9mmZ2jXIVN4q4Bhd9pefGj9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/logger": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.15.4.tgz", + "integrity": "sha512-rQ7F5+FMQ7t+w5NGFRT8CWHhym0aunduufCjlafvRzUSKEN/5/nwTfCe9I5QsthGlXJWs+ZTy4zQ+wLtZQRBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/events": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.15.4.tgz", + "integrity": "sha512-u5Lwcr4ZVBSLFbKYht+mJqJ3ZMXvJdmDMU5eDtrIEKPpu9LrIDdPpDEXBoyO6pDsoV/2AqyXUUMzBRyCatkkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/namer-default": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.15.4.tgz", + "integrity": "sha512-EXsoQ1S+5ZIfy8431E7F0vVS7bfH5JpZ+vFVcUpArJDkhmMG7T/eP6Kp9CXHLJmn7ki1x7iIVytrja0XXRQWBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.6.4.tgz", + "integrity": "sha512-g3+usMnr7pfRqbMAksOpNA7GJk7HUNW1Wxx7Shhp4w0K9JUdVrd2LRKwZxbqL7H9NqWtVvUOT9cZbMlDR6bO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "@parcel/diagnostic": "2.15.4", + "@parcel/fs": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-css": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.15.4.tgz", + "integrity": "sha512-KQLuqwcvVFTNFtM+bzfvQivwunmhVAngmR4NiI8zQaykidYH28V8YkVAQmpbLbgoGad/UgG7grb0UshvnrQHpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4", + "browserslist": "^4.24.5", + "lightningcss": "^1.30.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-html": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-html/-/optimizer-html-2.15.4.tgz", + "integrity": "sha512-gBvt6RdDVMyO1Flvdtc8DxpxLgIXhaKuVXEjHdAP7sEW0SMdSd6r/tl6Plmcszig7sDwhDf6IsQOIvbzGHYZZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-image": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.15.4.tgz", + "integrity": "sha512-M8fo7eEL6JRcmLhSX9pUUGU4MPrPrE9cMNcwIt3DQLnSvQ+sshhUDa6t9hKWeHHhs16BHvxrvksN2TIbkgHODQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4", + "@parcel/workers": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/optimizer-svg": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-svg/-/optimizer-svg-2.15.4.tgz", + "integrity": "sha512-pPdjRaLPqjAEROXIHLc6JWLLki56alhuUNbalhLqBCgktZrrq2dGCjBEVgxqRczc9D+ePCX/e/xci4tC0Tkcbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-swc": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.15.4.tgz", + "integrity": "sha512-2m5cYESVCq6AGx252eSTArZ1Oc1Ve4GBGL7NhvgbNqOthyXlc2qAed6rCkARrBd8pfEl5+2XHeK1ijDAZdIZ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4", + "@swc/core": "^1.11.24", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/package-manager": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.15.4.tgz", + "integrity": "sha512-KZONBcEJ24moQdrpU0zJh9CYk3KKbpB5RUM70utAORem1yQKms+0Y4YED3njq6nZzbgwUN/Csc+powUHLZStvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/fs": "2.15.4", + "@parcel/logger": "2.15.4", + "@parcel/node-resolver-core": "3.6.4", + "@parcel/types": "2.15.4", + "@parcel/utils": "2.15.4", + "@parcel/workers": "2.15.4", + "@swc/core": "^1.11.24", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/packager-css": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.15.4.tgz", + "integrity": "sha512-bzSaNf+I5lmJFu95wSG2k7pGwjCDesZsV6Y9sozIL2LoSxqvkGhm/ABXAa3Ed7dLe3tSAEBzJcyqShQgLzSzuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4", + "lightningcss": "^1.30.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-html": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.15.4.tgz", + "integrity": "sha512-Uayux6A2Anm66Kmq22QhD0TuVp9LiRCMuPUzBd6n4ekNlG0Lzm6K3/okMkPG65nKbNjq5qcPscFWlDxggvjt2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/types": "2.15.4", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.15.4.tgz", + "integrity": "sha512-96bqhs1jyd28CfWQD+Yn8rSsd1ar7voHWyBtMLimsK+bDJIzL26Z7jWyRDwXRuLErYC01EoXRIRctxtmeRVJ2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.15.4", + "@parcel/utils": "2.15.4", + "globals": "^13.24.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-raw": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.15.4.tgz", + "integrity": "sha512-CaSpDt5jjcO0SYCtsDhw6yfTDQuDFQ875H42W/ftvSQL7RfLRljPthnbdcy9chvKBbvRBQF+0z8Sxwehrd5hsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-svg": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.15.4.tgz", + "integrity": "sha512-qHsyOgnzoA2XGMLIYUnX79XAaV327VTWQvIzju/OmOjcff4o3uiEcNL8w9k3p2w2oPXOLoQ0THMiivoUQSM8GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/types": "2.15.4", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-wasm": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.15.4.tgz", + "integrity": "sha512-YPVij7zrBchtXr/y29P4uh3C/+19PMhhLibYF/8oMJKkFkeU3Uv00/XLm915vdBPrIPjgw0YuIfLzUKip1uGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4" + }, + "engines": { + "node": ">=16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/plugin": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.15.4.tgz", + "integrity": "sha512-XVehjmzk8ZDOFf/BXo26L76ZqCGNKIQcN2ngxAnq0KRY/WFanL8yLaL0qQq+c9whlu09hkGz1CuhFBLAIjJMYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/profiler": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.15.4.tgz", + "integrity": "sha512-ezVZlttUmQ1MQD5e8yVb07vSGYEFOB59Y/jaxL9mGSLZkVhMIIHe/7SuA+4qVAH8dlg6bslXRqlsunLMPEgPsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/events": "2.15.4", + "@parcel/types-internal": "2.15.4", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.15.4.tgz", + "integrity": "sha512-us0HIwuJqpSguf+yi4n8foabVs26JGvRB/eSOf0KkRldxFciYLn4NJ8rt3Xm1zvxlDiSkD4v2n77u+ouIZ+AEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/types": "2.15.4", + "@parcel/utils": "2.15.4", + "chalk": "^4.1.2", + "term-size": "^2.2.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-dev-server": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.15.4.tgz", + "integrity": "sha512-uCNeDyArNNXI9YThlxyTx7+5ZSxlewyUdyrLdDZCqvn8s1xNB9W8sUNVps7mJZQSc+2ZRk3wyDemURD67uJk/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/codeframe": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-tracer": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.15.4.tgz", + "integrity": "sha512-9W1xsb/FtobCQ4z847nI6hFDaTZHLeThv/z05EF77R30RX2k+unG9ac5NQB1v4KLx09Bhfre32+sjYNReWxWlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4", + "chrome-trace-event": "^1.0.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/resolver-default": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.15.4.tgz", + "integrity": "sha512-4uKo3FFnubtIc4rM9jZiQQXpa1slawyRy5btJEfTFvbcnz0dm3WThLrsPDMfmPwNr9F/n5x8yzDLI6/fZ/elgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/node-resolver-core": "3.6.4", + "@parcel/plugin": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-browser-hmr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.15.4.tgz", + "integrity": "sha512-KRGzbxDUOQUkrJKxxY0WyU7oVaa9TvWTRlpuGJXzQJs/hw8vkAAoAm8+ptpypvBC8LnxFHzGbSyHPfL8C8MQOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-js": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.15.4.tgz", + "integrity": "sha512-zNRK+693CMkYiA0ckjPOmz+JVHD9bVzp27itcMyuDH6l/Or8m09RgCC4DIdIxBqiplsDSe39DwEc5X7b0vvcjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-rsc": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-rsc/-/runtime-rsc-2.15.4.tgz", + "integrity": "sha512-yHc4HEwzCQYLqa6Q1WtZ8xJeaDAk0p2i0b3ABq2I+izmRjer4jertlsEwh9mf9Z1eUGtJobdGYzl8Ai1VfhC3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-service-worker": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.15.4.tgz", + "integrity": "sha512-NGq/wS34GIVzo2ZURBjCqgHV+PU7eTcngCzmmk/wrCEeWnr13ld+CAIxVZoqyNJwYsF6VQanrjSM2/LhCXEdyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.15.4.tgz", + "integrity": "sha512-OxOux8z8YEYg23+15uMmYaloFp3x1RwcliBay6HqxUW7RTmtI1/z+xd8AtienCckACD60gvDGy04LjgbEGdJVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/rust-darwin-arm64": "2.15.4", + "@parcel/rust-darwin-x64": "2.15.4", + "@parcel/rust-linux-arm-gnueabihf": "2.15.4", + "@parcel/rust-linux-arm64-gnu": "2.15.4", + "@parcel/rust-linux-arm64-musl": "2.15.4", + "@parcel/rust-linux-x64-gnu": "2.15.4", + "@parcel/rust-linux-x64-musl": "2.15.4", + "@parcel/rust-win32-x64-msvc": "2.15.4" + }, + "peerDependencies": { + "napi-wasm": "^1.1.2" + }, + "peerDependenciesMeta": { + "napi-wasm": { + "optional": true + } + } + }, + "node_modules/@parcel/rust-darwin-arm64": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-arm64/-/rust-darwin-arm64-2.15.4.tgz", + "integrity": "sha512-cEpNDeEtvM5Nhj0QLN95QbcZ9yY6Z5W3+2OeHvnojEAP8Rp1XGzqVTTZdlyKyN1KTiyfzIOiQJCiEcr+kMc5Nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-darwin-x64": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-x64/-/rust-darwin-x64-2.15.4.tgz", + "integrity": "sha512-jL9i13sXKeBXXz8Z3BNYoScPOi+ljBA0ubAE3PN5DCoAA6wS4/FsAiRSIUw+3uxqASBD7+JvaT5sDUga1Xft5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-arm-gnueabihf": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm-gnueabihf/-/rust-linux-arm-gnueabihf-2.15.4.tgz", + "integrity": "sha512-c8HpVdDugCutlMILoOlkTioih9HGJpQrzS2G3cg/O1a5ZTacooGf3eGJGoh6dUBEv9WEaEb6zsTRwFv2BgtZcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-arm64-gnu": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-gnu/-/rust-linux-arm64-gnu-2.15.4.tgz", + "integrity": "sha512-Wcfs/JY4FnuLxQaU+VX2rI4j376Qo2LkZmq4zp9frnsajaAqmloVQfnbUkdnQPEL4I38eHXerzBX3LoXSxnZKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-arm64-musl": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-musl/-/rust-linux-arm64-musl-2.15.4.tgz", + "integrity": "sha512-xf9HxosEn3dU5M0zDSXqBaG8rEjLThRdTYqpkxHW/qQGzy0Se+/ntg8PeDHsSG5E9OK8xrcKH46Lhaw0QBF/Zw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-x64-gnu": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-gnu/-/rust-linux-x64-gnu-2.15.4.tgz", + "integrity": "sha512-RigXVCFj6h0AXmkuxU61rfgYuW+PXBR6qSkR2I20yKnAXoMfxLaZy9YJ3sAPMEjT9zXgzGAX+3syItMF+bRjaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-x64-musl": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-musl/-/rust-linux-x64-musl-2.15.4.tgz", + "integrity": "sha512-tHlRgonSr5ca8OvhbGzZUggCgCOirRz5dHhPSCm4ajMxeDMamwprq6lKy0sCNTXht4TXIEyugBcfEuRKEeVIBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-win32-x64-msvc": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-win32-x64-msvc/-/rust-win32-x64-msvc-2.15.4.tgz", + "integrity": "sha512-YsX6vMl/bfyxqZSN7yiaZQKLoJKELSZYcvg8gIv4CF1xkaTdmfr6gvq2iCyoV+bwrodNohN4Xfl8r7Wniu1/UA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/transformer-babel": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.15.4.tgz", + "integrity": "sha512-rb4nqZcTLkLD3nvuYJ9wwNb8x6cajBK2l6csdYMLEI4516SkIzkO/gs2cZ9M5q+CMhxAqpdEnrwektbOtQQasg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4", + "browserslist": "^4.24.5", + "json5": "^2.2.3", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-css": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.15.4.tgz", + "integrity": "sha512-6tVwSJsOssXgcB5XMAQGsexAffoBEi8GVql3YQqzI1EwVYs9zr+B5mfbesb4aWcegR02w99NHJYFP9CrOr3SWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4", + "browserslist": "^4.24.5", + "lightningcss": "^1.30.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.15.4.tgz", + "integrity": "sha512-gzYPbbyEuV8nzPojw86eD5Kf93AYUWcY8lu33gu0XHROJH7mq5MAwPwtb/U+EfpeCd0/oKbLzA2mkQksM1NncQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-image": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.15.4.tgz", + "integrity": "sha512-KOVwj2gKjUybuzHwarC/YVqRf3r2BD4/2ysckozj6DIji/bq3fd2rE9yqxWXO+zt918PsOSTzMKwRnaseaXLKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4", + "@parcel/workers": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/transformer-js": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.15.4.tgz", + "integrity": "sha512-HX76PalPjqCLmXJnuSeMr2km8WlnUsW8oaRZ6FuZtSo9QD8BqIcwKGxSbIy9JHkObBgmrMOVpGtYrJM4/BlYbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.15.4", + "@parcel/workers": "2.15.4", + "@swc/helpers": "^0.5.0", + "browserslist": "^4.24.5", + "nullthrows": "^1.1.1", + "regenerator-runtime": "^0.14.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@parcel/transformer-json": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.15.4.tgz", + "integrity": "sha512-1ASeOSH3gPeaXyy/TZ7ce2TOfJ3ZeK5SBnDs+MM8LFcQsTwdRJKjX/4Qq9RgtMRryYAGHgMa09Gvp9FuFRyd+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-node": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-node/-/transformer-node-2.15.4.tgz", + "integrity": "sha512-zV5jvZA971eQMcFtaWZkW1UfAH/G6XVM/87oJ2B4ip9o9aKUWIl296rrfg2xWxUQyPhy11B17CJ6b8NgieqqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.15.4.tgz", + "integrity": "sha512-cNueSpOj3ulmMX85xr9clh/t0+mzVE+Q3H7Cf/OammqUkG/xjmilq4q7ZTgQFyUtUdWpE9LWWHojbJuz6k2Ulw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/utils": "2.15.4", + "clone": "^2.1.2", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-posthtml": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.15.4.tgz", + "integrity": "sha512-dETI+CeKMwu5Dpvu8BrQtex6nwzbNWKQkXseiM5x6+Wf3j9RD2NVpAMBRMjLkw1XlC9Whz1egxLSgKlMKbjg0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-raw": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.15.4.tgz", + "integrity": "sha512-pY2j09UCW2v1fwQtVLlCztSdPOxhq0YcWmTHCk/mRp8zuUR+eyHgsz48FrUxRF7cr/EBjc0zlFcregRMRcaTMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.15.4.tgz", + "integrity": "sha512-MgoQrV8+BVjrczAns5ZZbTERGB3/U4MaCBmbg3CuiTiIyS8IJQnGi+OhYRdKAB4NlsgpMZ5T2JrRbQUIm9MM8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/error-overlay": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/utils": "2.15.4", + "react-refresh": "^0.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-svg": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.15.4.tgz", + "integrity": "sha512-Q22e0VRbx62VXFlvJWIlc8ihlLaPQgtnAZz5E1/+ojiNb+k0PmIRjNJclVWPF6IdCsLO5tnGfUOaXe2OnZz28Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/plugin": "2.15.4", + "@parcel/rust": "2.15.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/types": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.15.4.tgz", + "integrity": "sha512-fS3UMMinLtzn/NTSx/qx38saBgRniylldh0XZEUcGeME4D2Llu/QlLv+YZ/LJqrFci3fPRM+YAn2K+JT/u+/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types-internal": "2.15.4", + "@parcel/workers": "2.15.4" + } + }, + "node_modules/@parcel/types-internal": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.15.4.tgz", + "integrity": "sha512-kl5QEZ8PTWRvMkwmk7IG3VpP/5/MSGwt9Nrj9ctXLdZkDdXZpK7IbXAthLQ4zrByMaqZULL2IyDuBqBgfuAqlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/feature-flags": "2.15.4", + "@parcel/source-map": "^2.1.1", + "utility-types": "^3.11.0" + } + }, + "node_modules/@parcel/utils": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.15.4.tgz", + "integrity": "sha512-29m09sfPx0GHnmy1kkZ5XezprepdFGKKKUEJkyiYA4ERf55jjdnU2/GP4sWlZXxjh2Y+JFoCAFlCamEClq/8eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/codeframe": "2.15.4", + "@parcel/diagnostic": "2.15.4", + "@parcel/logger": "2.15.4", + "@parcel/markdown-ansi": "2.15.4", + "@parcel/rust": "2.15.4", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.2", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/workers": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.15.4.tgz", + "integrity": "sha512-wZ/5/mfjs5aeqhXY0c6fwuaBFeNpOXoOq2CKPSMDXt+GX2u/9/1bpVxN9XeGTAJO+ZD++CLq0hyzTnIHy58nyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.15.4", + "@parcel/logger": "2.15.4", + "@parcel/profiler": "2.15.4", + "@parcel/types-internal": "2.15.4", + "@parcel/utils": "2.15.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.15.4" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz", + "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.23" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.3", + "@swc/core-darwin-x64": "1.13.3", + "@swc/core-linux-arm-gnueabihf": "1.13.3", + "@swc/core-linux-arm64-gnu": "1.13.3", + "@swc/core-linux-arm64-musl": "1.13.3", + "@swc/core-linux-x64-gnu": "1.13.3", + "@swc/core-linux-x64-musl": "1.13.3", + "@swc/core-win32-arm64-msvc": "1.13.3", + "@swc/core-win32-ia32-msvc": "1.13.3", + "@swc/core-win32-x64-msvc": "1.13.3" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz", + "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz", + "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz", + "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz", + "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz", + "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", + "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz", + "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz", + "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz", + "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz", + "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "postcss": "^8.4.41", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dev": true, + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "dev": true, + "license": "ISC" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/lmdb": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-2.8.5.tgz", + "integrity": "sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "msgpackr": "^1.9.5", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.1.1", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "2.8.5", + "@lmdb/lmdb-darwin-x64": "2.8.5", + "@lmdb/lmdb-linux-arm": "2.8.5", + "@lmdb/lmdb-linux-arm64": "2.8.5", + "@lmdb/lmdb-linux-x64": "2.8.5", + "@lmdb/lmdb-win32-x64": "2.8.5" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/msgpackr-extract/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", + "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parcel": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.15.4.tgz", + "integrity": "sha512-eZHQ/omuQ7yBYB9XezyzSqhc826oy/uhloCNiej1CTZ+twAqJVtp4MRvTGMcivKhE+WE8QkYD5XkJHLLQsJQcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/config-default": "2.15.4", + "@parcel/core": "2.15.4", + "@parcel/diagnostic": "2.15.4", + "@parcel/events": "2.15.4", + "@parcel/feature-flags": "2.15.4", + "@parcel/fs": "2.15.4", + "@parcel/logger": "2.15.4", + "@parcel/package-manager": "2.15.4", + "@parcel/reporter-cli": "2.15.4", + "@parcel/reporter-dev-server": "2.15.4", + "@parcel/reporter-tracer": "2.15.4", + "@parcel/utils": "2.15.4", + "chalk": "^4.1.2", + "commander": "^12.1.0", + "get-port": "^4.2.0" + }, + "bin": { + "parcel": "lib/bin.js" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pbkdf2": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/pbkdf2/node_modules/create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/pbkdf2/node_modules/hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz", + "integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-toastify": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c3b6e7 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "vfdio", + "version": "0.0.1", + "description": "Virtual FDIO Emulation for VATSIM/vNAS", + "source": "src/index.html", + "scripts": { + "dev": "parcel src/index.html -p 3000", + "start": "parcel src/index.html -p 3000", + "build": "parcel build", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "@types/node": "^24.1.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.1", + "events": "^3.3.0", + "os-browserify": "^0.3.0", + "parcel": "^2.15.4", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "stream-browserify": "^3.0.0", + "string_decoder": "^1.3.0", + "vm-browserify": "^1.1.2" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.0.0", + "@fortawesome/free-solid-svg-icons": "^6.0.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "@microsoft/signalr": "^8.0.0", + "@reduxjs/toolkit": "^2.0.0", + "@tailwindcss/postcss": "^4.1.11", + "jose": "^5.0.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-redux": "^9.0.0", + "react-router-dom": "^6.0.0", + "react-toastify": "^10.0.0", + "tailwindcss": "^4.1.11" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b42870d --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,991 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Provider } from 'react-redux'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { store } from './redux/store'; +import { HubContextProvider } from './contexts/HubContext'; +import LoginProvider from './login/Login'; +import { useRootDispatch, useRootSelector } from './redux/hooks'; +import { getVnasConfig, vatsimTokenSelector, sessionSelector, logoutThunk, hubConnectedSelector } from './redux/slices/authSlice'; +import { useHubConnector } from './hooks/useHubConnector'; +import Header from './components/Header'; +import InputArea from './components/InputArea'; +import Recat from './components/Recat'; +import './styles/terminal.css'; +import { isTypedArray } from 'util/types'; +import type { ApiFlightplan } from './types/apiTypes/apiFlightplan'; + +const AppContent = () => { + const dispatch = useRootDispatch(); + const vatsimToken = useRootSelector(vatsimTokenSelector); + const session = useRootSelector(sessionSelector); + + useEffect(() => { + dispatch(getVnasConfig()); + }, [dispatch]); + + const MainApp = () => { + const [command, setCommand] = useState(''); + const [lastFeedback, setLastFeedback] = useState(''); + const [lastFeedbackErrorMessage, setLastFeedbackErrorMessage] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const { sendCommand, disconnectHub, deleteFlightplan, amendFlightplan, requestFlightStrip, flightplans, flightStrips, hubConnection } = useHubConnector(); + const hubConnected = useRootSelector(hubConnectedSelector); + + + // Blink cursor + maintain focus + const [cursorVisible, setCursorVisible] = useState(true); + useEffect(() => { + const blinkInterval = setInterval(() => { + setCursorVisible((prev) => !prev); + }, 300); + + const ensureFocus = () => { + if (document.activeElement !== terminalInputRef.current) { + terminalInputRef.current?.focus(); + } + }; + + const focusInterval = setInterval(ensureFocus, 500); + ensureFocus(); + + return () => { + clearInterval(blinkInterval); + clearInterval(focusInterval); + }; + }, []); + // end blinking cursor/focus section + + // handle ESC clear, need to define response areas handling, REMOVE placeholder text and replace with '' when ready to deploy. + const [responseTop, setResponseTop] = useState(''); + const [responseBottom, setResponseBottom] = useState(''); + + const handleEscapeClear = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + setTypedCommand(''); + setResponseTop(''); + setResponseBottom(''); + setLastFeedback(''); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleEscapeClear); + return () => window.removeEventListener('keydown', handleEscapeClear); + }, []); + + // Listen for ReceiveStripItems events and display formatted strips + useEffect(() => { + if (!hubConnection) return; + + const handleStripPrint = (topic: any, stripItems: any[]) => { + stripItems.forEach(strip => { + if (strip?.fieldValues) { + const fieldValues = strip.fieldValues; + + console.log('ReceiveStripItems - Full fieldValues:', fieldValues); + + // Fixed column positions based on ERAM reference (80 char width) + // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) + const line1_aircraftId = (fieldValues[0] || '').substring(0, 17).padEnd(17); + const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); + const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 11).padEnd(13); + + // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces + let route = (fieldValues[11] || ''); + route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); + const line1_route = route.substring(0, 40); + + console.log('Route info:', { fullRoute: route, length: route.length, needsContinuation: route.length > 40 }); + + // Line 2: Revision Number (starts at position 3) + const line2 = ' ' + (fieldValues[1] || ''); + + // Line 3: Aircraft Type/Equipment (starts at column 1) + const line3_typeEquip = (fieldValues[3] || '').substring(0, 16).padEnd(18); + const line3_time = (fieldValues[6] || '').substring(0, 7).padEnd(22); + + // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) + const line4_cid = (fieldValues[4] || '').substring(0, 17).padEnd(18); + const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(22); + const line4_remarks = (fieldValues[12] || '').substring(0, 40); + + // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 + // But revision number ALSO appears on line 2 at column 3 + let line2_full = line2; // Revision at column 3 + if (route.length > 40) { + const routeContinuation = route.substring(40, 120); // Next 80 chars + // Pad line2 to exactly 40 chars, then add route continuation + line2_full = line2.padEnd(40) + routeContinuation; + console.log('ReceiveStripItems - Line2 debug:', { + line2: `"${line2}"`, + line2_length: line2.length, + line2_padded_length: line2.padEnd(40).length, + routeContinuation: `"${routeContinuation}"` + }); + } + + console.log('ReceiveStripItems - Total route continuation lines:', route.length > 40 ? 1 : 0); + + // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) + const formattedStrip = + line1_aircraftId + line1_beacon + line1_depPoint + line1_route + '\n' + + line2_full + '\n' + + line3_typeEquip + line3_time + '\n' + + line4_cid + line4_altitude + line4_remarks; + + console.log('ReceiveStripItems - Final formatted strip:', formattedStrip); + + // Move current responseBottom to responseTop and set new strip to responseBottom + setResponseTop(responseBottom); + setResponseBottom(formattedStrip); + } + }); + }; + + hubConnection.on('ReceiveStripItems', handleStripPrint); + + return () => { + hubConnection.off('ReceiveStripItems', handleStripPrint); + }; + }, [hubConnection, responseBottom]); + + + const handleCommandSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (command.trim() && !isProcessing) { + setIsProcessing(true); + setLastFeedback(''); // Clear previous feedback + + try { + const result = await sendCommand(command.trim()); + setLastFeedback(`${command.toUpperCase()}\n\n${result.toUpperCase()}`); + setCommand(''); + } catch (error) { + console.error('Failed to send command:', error); + setLastFeedback(`REJECT ${command.toUpperCase()}\n\n${error}`.toUpperCase()); + } finally { + setIsProcessing(false); + } + } + }; + + const [typedCommand, setTypedCommand] = useState(''); + const terminalInputRef = React.useRef(null); + + useEffect(() => { + terminalInputRef.current?.focus(); + }, []); + + const parseCommand = async (input: string): Promise => { + const [command, ...args] = input.trim().split(/\s+/).map(s => s.toUpperCase()); + + // Helper function to find flightplan by callsign, CID, or beacon code + const findFlightplan = (identifier: string): ApiFlightplan | undefined => { + if (!flightplans) return undefined; + + // Try direct callsign match first + let fp = flightplans.get(identifier); + if (fp) return fp; + + // Search by CID or beacon code + for (const [, flightplan] of flightplans) { + if (flightplan.cid === identifier || + flightplan.assignedBeaconCode?.toString() === identifier) { + return flightplan; + } + } + + return undefined; + }; + + switch (command) { + case 'FP': { + // Flight Plan message - FP + // Format: FP ACID TYPE/EQUIP SPEED FIX TIME ALT ROUTE REMARKS + // Example: FP UAL423 B721/A 450 HAR P1720 170 HAR.V14.DCA O 1 VOR INOP + const fpMatch = /^FP\s+(.+)$/i.exec(input.trim()); + if (!fpMatch) { + return `REJECT 01 MSG INVALID\nMESSAGE TYPE`; + } + + const fields = fpMatch[1].split(/\s+/); + if (fields.length < 7) { + return `REJECT FORMAT - INSUFFICIENT FIELDS\n${input}`; + } + + // Field 02: Aircraft ID + const aircraftId = fields[0]; + if (aircraftId.length < 2 || aircraftId.length > 20) { + return `REJECT 02 AID FLID\nFORMAT`; + } + + // Field 03: Aircraft Type / Equipment Suffix + const typeEquipMatch = fields[1].match(/^([A-Z0-9]+)\/([A-Z])$/); + if (!typeEquipMatch) { + return `REJECT 03 TYP FORMAT`; + } + const aircraftType = typeEquipMatch[1]; + const equipmentSuffix = typeEquipMatch[2]; + + // Field 05: Speed + const speed = parseInt(fields[2]); + if (isNaN(speed) || speed <= 0) { + return `REJECT 05 SPD ILLEGAL`; + } + if (speed > 3700) { + return `REJECT 05 SPD FORMAT`; + } + + // Field 06: Departure Fix (Coordination Fix) + const departureFix = fields[3]; + if (departureFix.length < 2 || departureFix.length > 12) { + return `REJECT 06 FIX FORMAT`; + } + + // Field 07: Time + const timeStr = fields[4]; + let departureTime = 0; + if (timeStr !== 'E' && timeStr !== 'P' && timeStr !== 'D') { + // Parse time - should be 4 or 5 characters (HHMM or PXXDD format) + const timeMatch = timeStr.match(/^[PE]?(\d{4})$/); + if (!timeMatch) { + return `REJECT 07 TIM FORMAT`; + } + departureTime = parseInt(timeMatch[1]); + } + + // Field 08 or 09: Altitude (Assigned or Requested) + const altStr = fields[5]; + let altitude = ''; + if (altStr === 'OTP' || altStr === 'VFR') { + altitude = altStr; + } else { + // Parse altitude - format like 170 (FL170), OTP/115, VFR/75, etc. + const altMatch = altStr.match(/^(\d+|OTP|VFR)(\/(\d+))?$/); + if (!altMatch) { + return `REJECT 08 ALT FORMAT`; + } + altitude = altStr; + } + + // Field 10: Route - everything from field 6 onwards until we hit remarks + // Route ends when we see 'O' or '@' prefix for remarks + let routeEndIdx = 6; + for (let i = 6; i < fields.length; i++) { + if (fields[i].startsWith('O') || fields[i].startsWith('@')) { + routeEndIdx = i; + break; + } + routeEndIdx = i + 1; + } + + const routeParts = fields.slice(6, routeEndIdx); + if (routeParts.length === 0) { + return `REJECT 10 RTE FORMAT`; + } + const route = routeParts.join(' '); + + // Field 11: Remarks (optional) + let remarks = ''; + if (routeEndIdx < fields.length) { + const remarksFields = fields.slice(routeEndIdx); + // Check if remarks start with O or @ + const remarksStr = remarksFields.join(' '); + if (remarksStr.startsWith('O ')) { + remarks = remarksStr.substring(2); // Interfacility remarks + } else if (remarksStr.startsWith('@')) { + remarks = remarksStr.substring(1); // Intrafacility remarks + } else { + remarks = remarksStr; + } + } + + try { + // Check for duplicate active flight plan + const existingFp = flightplans.get(aircraftId); + if (existingFp && existingFp.status === 'Active') { + return `REJECT 02 AID FLID\nDUPLICATION`; + } + + await amendFlightplan({ + aircraftId, + cid: '', + status: 'Proposed', + aircraftType: aircraftType, + faaEquipmentSuffix: equipmentSuffix, + equipment: `${aircraftType}/${equipmentSuffix}`, + icaoEquipmentCodes: '', + icaoSurveillanceCodes: '', + speed, + altitude, + departure: departureFix, + destination: '', + alternate: '', + route, + remarks, + assignedBeaconCode: null, + estimatedDepartureTime: departureTime, + actualDepartureTime: 0, + hoursEnroute: 0, + minutesEnroute: 0, + fuelHours: 0, + fuelMinutes: 0, + pilotCid: '', + holdAnnotations: null, + wakeTurbulenceCode: '', + }); + + return `ACCEPT\n${aircraftId}`; + } catch (error) { + console.error('Failed to create flightplan:', error); + const errorStr = String(error); + if (errorStr.includes('Not your control') || errorStr.includes('inactive session')) { + return `REJECT 01 MSG ILLEGAL\nSOURCE`; + } else { + return `REJECT FP ENTRY FAILED`; + } + } + } + + case 'AM': { + // Amendment Message - AM [ ...] + // Format: AM ACID 06 FIX 10 ROUTE or AM ACID SPD 225 RAL 90 + // Field references: AID, TYP, BCN, SPD, FIX, TIM, ALT, RAL, RTE, RMK + const amMatch = /^AM\s+(.+)$/i.exec(input.trim()); + if (!amMatch) { + return `REJECT 01 MSG INVALID\nMESSAGE TYPE`; + } + + const parts = amMatch[1].split(/\s+/); + if (parts.length < 3) { + return `REJECT FORMAT - INSUFFICIENT FIELDS\n${input}`; + } + + const aircraftId = parts[0]; + + // Find the existing flightplan + const existingFp = findFlightplan(aircraftId); + if (!existingFp) { + return `REJECT 02 FLID NOT\nSTORED`; + } + + // Parse field references and amendments + const amendments: { [key: string]: any } = {}; + let i = 1; + let amendingAircraftId = false; + + while (i < parts.length) { + const fieldRef = parts[i].toUpperCase(); + + // Map field references to field numbers + const fieldMap: { [key: string]: string } = { + 'AID': '02', '02': '02', '2': '02', + 'TYP': '03', '03': '03', '3': '03', + 'BCN': '04', '04': '04', '4': '04', + 'SPD': '05', '05': '05', '5': '05', + 'FIX': '06', '06': '06', '6': '06', + 'TIM': '07', '07': '07', '7': '07', + 'ALT': '08', '08': '08', '8': '08', + 'RAL': '09', '09': '09', '9': '09', + 'RTE': '10', '10': '10', + 'RMK': '11', '11': '11' + }; + + const fieldNum = fieldMap[fieldRef]; + if (!fieldNum) { + return `REJECT INVALID FIELD\nREFERENCE`; + } + + i++; + if (i >= parts.length) { + return `REJECT FORMAT - MISSING AMENDMENT DATA\n${input}`; + } + + // Check if amending Field 02 (Aircraft ID) + if (fieldNum === '02') { + if (Object.keys(amendments).length > 0) { + return `REJECT - INVALID\nAMENDMENT`; + } + amendingAircraftId = true; + amendments['aircraftId'] = parts[i]; + i++; + break; // Only Field 02 can be amended when changing aircraft ID + } + + // Collect amendment data for this field + let amendmentData: string[] = []; + + // For route (10/RTE), collect all remaining parts until we hit another field ref or end + if (fieldNum === '10') { + while (i < parts.length) { + const nextToken = parts[i].toUpperCase(); + if (fieldMap[nextToken]) { + break; // Hit another field reference + } + amendmentData.push(parts[i]); + i++; + } + + if (amendmentData.length === 0) { + return `REJECT 10 RTE FORMAT`; + } + + // Process route amendment based on ERAM rules + const routeStr = amendmentData.join(' '); + const existingRoute = existingFp.route || ''; + const routeElements = routeStr.split(/[\s.]+/).filter(e => e.length > 0); + + // Check for departure fix change (single element followed by ↑) + if (routeStr.endsWith('↑') || routeStr.endsWith('^')) { + const newDepFix = routeStr.slice(0, -1).trim().split(/[\s.]+/)[0]; + amendments['departure'] = newDepFix; + amendments['route'] = routeStr.slice(0, -1).trim(); + } + // Check for complete replacement (ends with ↓) + else if (routeStr.endsWith('↓') || routeStr.endsWith('v')) { + const newRoute = routeStr.slice(0, -1).trim(); + const newRouteElements = newRoute.split(/[\s.]+/).filter(e => e.length > 0); + if (newRouteElements.length > 0) { + // Last element becomes destination + amendments['destination'] = newRouteElements[newRouteElements.length - 1]; + // For active flights, departure fix retained with tailoring symbol (/) + if (existingFp.status === 'Active' && existingFp.departure) { + amendments['route'] = `${existingFp.departure}/.${newRoute}`; + } else { + amendments['route'] = newRoute; + } + } else { + return `REJECT 10 RTE FORMAT`; + } + } + // Tailoring symbol at beginning (/) - insert after departure fix + else if (routeStr.startsWith('/')) { + const tailoredRoute = routeStr.substring(1).trim(); + if (existingFp.departure) { + amendments['route'] = `${existingFp.departure}/.${tailoredRoute}`; + } else { + amendments['route'] = tailoredRoute; + } + } + // Merge with existing route - match first or last unambiguous element + else { + const firstElement = routeElements[0]; + const lastElement = routeElements[routeElements.length - 1]; + const existingElements = existingRoute.split(/[\s.]+/).filter(e => e.length > 0); + + // Try to find first element match + const firstMatchIdx = existingElements.indexOf(firstElement); + const lastMatchIdx = existingElements.lastIndexOf(lastElement); + + // Check if BOTH first and last match (replace between) + if (firstMatchIdx !== -1 && lastMatchIdx !== -1 && firstMatchIdx < lastMatchIdx) { + const before = existingElements.slice(0, firstMatchIdx).join('.'); + const after = existingElements.slice(lastMatchIdx + 1).join('.'); + const merged = [before, routeStr, after].filter(p => p.length > 0).join('.'); + amendments['route'] = merged; + } + // Only first element matches (replace after) + else if (firstMatchIdx !== -1) { + const before = existingElements.slice(0, firstMatchIdx + 1).join('.'); + amendments['route'] = `${before}.${routeElements.slice(1).join('.')}`; + } + // Only last element matches (replace before) + else if (lastMatchIdx !== -1) { + const after = existingElements.slice(lastMatchIdx).join('.'); + // For active flight, add tailoring symbol + if (existingFp.status === 'Active' && existingFp.departure) { + amendments['route'] = `${existingFp.departure}/.${routeElements.slice(0, -1).join('.')}.${after}`; + } else { + amendments['route'] = `${routeElements.slice(0, -1).join('.')}.${after}`; + } + } + // No match - just use new route + else { + amendments['route'] = routeStr; + } + } + + // Check if Field 06 also needs amendment (required for Field 10 amendments per ERAM rules) + // This will be validated when Field 06 is also in the amendment + } + // For other fields, just take the next token + else { + amendmentData.push(parts[i]); + i++; + + const value = amendmentData[0]; + + switch (fieldNum) { + case '03': // Type/Equipment + const typeMatch = value.match(/^([A-Z0-9]+)\/([A-Z])$/); + if (!typeMatch) { + return `REJECT 03 TYP FORMAT`; + } + const newAircraftType = typeMatch[1]; + const newFaaEquipmentSuffix = typeMatch[2]; + + // Update equipment field by replacing only the aircraft type (before first /) + // and preserving everything after the first / + // Format: "B738/M-VGDW/C" -> "B752/M-VGDW/C" + let newEquipment = `${newAircraftType}/${newFaaEquipmentSuffix}`; + if (existingFp.equipment) { + const firstSlashIndex = existingFp.equipment.indexOf('/'); + if (firstSlashIndex > 0) { + // Preserve everything after the first / + const everythingAfterSlash = existingFp.equipment.substring(firstSlashIndex + 1); + newEquipment = `${newAircraftType}/${everythingAfterSlash}`; + } + } + + amendments['equipment'] = newEquipment; + amendments['faaEquipmentSuffix'] = newFaaEquipmentSuffix; + break; + + case '04': // Beacon Code + const beaconCode = parseInt(value); + if (isNaN(beaconCode) || beaconCode < 0 || beaconCode > 7777) { + return `REJECT 04 BCN CODE FORMAT`; + } + amendments['assignedBeaconCode'] = beaconCode; + break; + + case '05': // Speed + const speed = parseInt(value); + if (isNaN(speed) || speed <= 0) { + return `REJECT 05 SPD ILLEGAL`; + } + if (speed > 3700) { + return `REJECT 05 SPD FORMAT`; + } + amendments['speed'] = speed; + break; + + case '06': // Departure Fix + if (value.length < 2 || value.length > 12) { + return `REJECT 06 FIX FORMAT`; + } + amendments['departure'] = value; + break; + + case '07': // Time + if (value !== 'E' && value !== 'P' && value !== 'D') { + const timeMatch = value.match(/^[PE]?(\d{4})$/); + if (!timeMatch) { + return `REJECT 07 TIM FORMAT`; + } + amendments['estimatedDepartureTime'] = parseInt(timeMatch[1]); + } + break; + + case '08': // Assigned Altitude + amendments['altitude'] = value; + break; + + case '09': // Requested Altitude (RAL) + // Store as altitude for now + amendments['altitude'] = value; + break; + + case '11': // Remarks + // Collect all remaining parts as remarks + while (i < parts.length) { + const nextToken = parts[i].toUpperCase(); + if (fieldMap[nextToken]) { + break; + } + amendmentData.push(parts[i]); + i++; + } + let remarks = amendmentData.join(' '); + // Remove O or @ prefix if present + if (remarks.startsWith('O ')) { + remarks = remarks.substring(2); + } else if (remarks.startsWith('@')) { + remarks = remarks.substring(1); + } + amendments['remarks'] = remarks; + break; + } + } + } + + if (Object.keys(amendments).length === 0) { + return `REJECT FORMAT - NO VALID AMENDMENTS\n${input}`; + } + + try { + // Build the amendment DTO, preserving existing values + const amendDto: any = { + aircraftId: amendments['aircraftId'] || existingFp.aircraftId, + cid: existingFp.cid, + status: existingFp.status, + aircraftType: amendments['aircraftType'] || existingFp.aircraftType, + faaEquipmentSuffix: amendments['faaEquipmentSuffix'] || existingFp.faaEquipmentSuffix, + equipment: amendments['equipment'] || existingFp.equipment, + icaoEquipmentCodes: existingFp.icaoEquipmentCodes, + icaoSurveillanceCodes: existingFp.icaoSurveillanceCodes, + speed: amendments['speed'] ?? existingFp.speed, + altitude: amendments['altitude'] || existingFp.altitude, + departure: amendments['departure'] || existingFp.departure, + destination: amendments['destination'] || existingFp.destination, + alternate: existingFp.alternate, + route: amendments['route'] || existingFp.route, + remarks: amendments['remarks'] !== undefined ? amendments['remarks'] : existingFp.remarks, + assignedBeaconCode: amendments['assignedBeaconCode'] ?? existingFp.assignedBeaconCode, + estimatedDepartureTime: amendments['estimatedDepartureTime'] ?? existingFp.estimatedDepartureTime, + actualDepartureTime: existingFp.actualDepartureTime, + hoursEnroute: existingFp.hoursEnroute, + minutesEnroute: existingFp.minutesEnroute, + fuelHours: existingFp.fuelHours, + fuelMinutes: existingFp.fuelMinutes, + pilotCid: existingFp.pilotCid, + holdAnnotations: existingFp.holdAnnotations, + wakeTurbulenceCode: existingFp.wakeTurbulenceCode, + }; + + console.log('AM Command Debug:'); + console.log(' Amendments:', amendments); + console.log(' Existing FP equipment:', existingFp.equipment); + console.log(' Existing FP faaEquipmentSuffix:', existingFp.faaEquipmentSuffix); + console.log(' New equipment:', amendDto.equipment); + console.log(' New faaEquipmentSuffix:', amendDto.faaEquipmentSuffix); + + await amendFlightplan(amendDto); + + return `ACCEPT\n${amendDto.aircraftId}`; + } catch (error) { + console.error('Failed to amend flightplan:', error); + const errorStr = String(error); + if (errorStr.includes('Not your control') || errorStr.includes('inactive session')) { + return `REJECT 01 MSG ILLEGAL\nSOURCE`; + } else { + return `REJECT - INVALID\nAMENDMENT`; + } + } + } + + case 'GI': { + // General Information message - GI + const giMatch = /^GI\s+(\S+)\s+(.+)$/i.exec(input.trim()); + if (giMatch && giMatch.length === 3) { + const recipient = giMatch[1].toUpperCase(); + const message = giMatch[2]; + // TODO: Implement GI message sending via your hub/socket + return `ACCEPT GI TO ${recipient}\n${message}`; + } else { + return `REJECT FORMAT\n${input}`; + } + } + + case 'WR': { + // Weather Request - WR + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const station = args[0]; + // TODO: Implement weather request + return `ACCEPT WEATHER STAT REQ\n${station}`; + } + + case 'SR': { + // Strip Request - SR + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const identifier = args[0]; + + // Find the aircraft (by callsign, CID, or beacon) + let aircraftId = identifier; + let strip = flightStrips?.get(identifier); + + if (!strip && flightStrips) { + // Search by CID or beacon code + for (const [id, s] of flightStrips) { + if (s.fieldValues && ( + s.fieldValues[0] === identifier || + s.fieldValues[4] === identifier || + s.fieldValues[5] === identifier)) { + strip = s; + aircraftId = id; + break; + } + } + } + + if (strip?.fieldValues) { + // Format using fieldValues from strip data based on ERAM strip layout + // fieldValues: [0:callsign, 1:rev, 2:?, 3:type/equip, 4:cid, 5:beacon, 6:proptime, 7:alt, 8:dep/arr, 9-10:?, 11:route, 12:remarks] + const fieldValues = strip.fieldValues; + + // Fixed column positions based on ERAM reference (80 char width) + // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) + const line1_aircraftId = (fieldValues[0] || '').substring(0, 17).padEnd(17); + const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); + const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 11).padEnd(13); + + // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces + let route = (fieldValues[11] || ''); + route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); + const line1_route = route.substring(0, 40); + + // Line 2: Revision Number (starts at position 3) + const line2 = ' ' + (fieldValues[1] || ''); + + // Line 3: Aircraft Type/Equipment (starts at column 1) + const line3_typeEquip = (fieldValues[3] || '').substring(0, 16).padEnd(18); + const line3_time = (fieldValues[6] || '').substring(0, 7).padEnd(22); + + // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) + const line4_cid = (fieldValues[4] || '').substring(0, 17).padEnd(18); + const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(22); + const line4_remarks = (fieldValues[12] || '').substring(0, 40); + + // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 + // But revision number ALSO appears on line 2 at column 3 + let line2_full = line2; // Revision at column 3 + if (route.length > 40) { + const routeContinuation = route.substring(40, 120); // Next 80 chars + // Pad line2 to exactly 40 chars, then add route continuation + line2_full = line2.padEnd(40) + routeContinuation; + } + + // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) + const formattedStrip = + line1_aircraftId + line1_beacon + line1_depPoint + line1_route + '\n' + + line2_full + '\n' + + line3_typeEquip + line3_time + '\n' + + line4_cid + line4_altitude + line4_remarks; + + // Print the strip (move responseBottom to responseTop, set new strip to responseBottom) + setResponseTop(responseBottom); + setResponseBottom(formattedStrip); + + // Also request from server to trigger ReceiveStripItems event (which will overwrite this with server's version) + try { + await requestFlightStrip(aircraftId); + } catch (error) { + console.warn('RequestFlightStrip failed:', error); + } + + // Return the formatted strip data + return formattedStrip; + } else { + return `REJECT\nSTRIP NOT FOUND\n${input}`; + } + } + + case 'FR': { + // Flight Readout - FR + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const identifier = args[0]; + const flightplan = findFlightplan(identifier); + + if (flightplan) { + // Format using ApiFlightplan data + // aircraftID aircraftType assignedBeaconCode speed altitude departure route destination remarks + const aircraftId = flightplan.aircraftId || ''; + const aircraftType = flightplan.aircraftType || ''; + const beaconCode = flightplan.assignedBeaconCode?.toString() || ''; + const speed = flightplan.speed || ''; + const time = ('P' + flightplan.estimatedDepartureTime) || ''; + const altitude = flightplan.altitude || ''; + const departure = flightplan.departure || ''; + const destination = flightplan.destination || ''; + const remarks = flightplan.remarks || ''; + + // Route - break into 80 char chunks + const route = flightplan.route || ''; + const maxLineLength = 80; + const routeLines: string[] = []; + for (let i = 0; i < route.length; i += maxLineLength) { + routeLines.push(route.substring(i, i + maxLineLength)); + } + + return `${aircraftId} ${aircraftType} ${beaconCode} ${speed} ${time} ${altitude} ${departure} ${route} ${destination} ${remarks}`; + + } else { + return `FLID NOT STORED\n${input}`; + } + } + + case 'RS': { + // Remove Strips - RS + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const identifier = args[0]; + const flightplan = findFlightplan(identifier); + if (flightplan) { + try { + await deleteFlightplan(flightplan.aircraftId); + return `${flightplan.aircraftId} ${flightplan.cid}REMOVE \nSTRIPS`; + } catch (error) { + console.error('Failed to delete flightplan:', error); + // Parse the error message to extract relevant info + const errorStr = String(error); + if (errorStr.includes('Not your control')) { + return `REJECT NOT YOUR CONTROL\n${flightplan.aircraftId}`; + } else { + return `REJECT DELETE FAILED\n${flightplan.aircraftId}`; + } + } + } else { + return `REJECT FLID NOT STORED\n${input}`; + } + } + + default: + // Send to ERAM hub for all other commands + return await sendCommand(input); + } + }; + + const handleKeyDown = async (e: React.KeyboardEvent) => { + if (isProcessing) return; + + if (e.key === 'Enter') { + e.preventDefault(); + const command = typedCommand.trim(); + if (!command) return; + + setIsProcessing(true); + setLastFeedback(''); + + try { + const result = await parseCommand(command); + console.log(` Command: ${command}, Result:`, result); + + // Check if result is a REJECT - if so, show ONLY in error area + if (result.toUpperCase().startsWith('REJECT')) { + setLastFeedbackErrorMessage(result); + // Don't update responseBottom or responseTop for errors + } else { + // Success - clear errors and update response areas + setLastFeedbackErrorMessage(''); + + // Move current responseBottom to responseTop + setResponseTop(responseBottom); + + // Set new response to responseBottom + setResponseBottom(result); + } + // then dynamically add a scroll effect for each subsequent response's data to cycle it upwards. + } catch (error) { + const errorMsg = `REJECT ${typedCommand.toUpperCase()}\n\n${String(error).toUpperCase()}`; + setLastFeedbackErrorMessage(errorMsg); + // Don't update responseBottom or responseTop for errors + } finally { + setTypedCommand(''); + setIsProcessing(false); + } + } else if (e.key === 'Backspace') { + e.preventDefault(); + setTypedCommand((prev) => prev.slice(0, -1)); + } else if (e.key.length === 1) { + e.preventDefault(); + setTypedCommand((prev) => prev + e.key.toUpperCase()); + } + }; + + return ( +
+ {/* Terminal Header */} +
+ + {/* Terminal Body */} +
+ {/* Response Section (top half) */} + {/* FDIO max character width is 80 */} +
+
+ {responseTop && '================================================================================\n'}{responseTop} +
+
+ {responseBottom && '================================================================================\n'}{responseBottom} +
+
+ {/* Command Section (Bottom Half) */} +
+ ------------------------------------------------------------------------------------------------ + {isProcessing && ( +
M E S S A G E W A I T I N G . . .
+ )} + {lastFeedbackErrorMessage && ( +
  {lastFeedbackErrorMessage.toUpperCase()}
+ )} +
+ + {/* TO DO: BLINKING CURSOR BOX AND FORCED FOCUS */} +
+
+ {typedCommand} + +
+ + {isProcessing && ( +
+ +
+ )} +
+ + {/* Terminal Footer */} +
+
+
+ VNAS HUB: {hubConnected ? 'CONNECTED' : 'DISCONNECTED'} +
+ +
+ ARTCC: {session?.artccId?.toUpperCase() || 'N/A'} | STATUS: {session?.isActive ? 'ACTIVE' : 'INACTIVE'} +
+ +
+
+
+ ); + }; + + return ( + + + } + /> + + + + ) : ( + + ) + } + /> + + + ); +}; + +export default function App() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/api/vNasDataApi.ts b/src/api/vNasDataApi.ts new file mode 100644 index 0000000..ba41600 --- /dev/null +++ b/src/api/vNasDataApi.ts @@ -0,0 +1,34 @@ +import { VNAS_CONFIG_URL, VATSIM_CLIENT_ID } from '../utils/constants'; + +type LoginDto = { + nasToken: string; + vatsimToken: string; +}; + +export const login = async (apiBaseUrl: string, code: string, redirectUrl: string) => { + return fetch(`${apiBaseUrl}/auth/login?code=${code}&redirectUrl=${redirectUrl}&clientId=${VATSIM_CLIENT_ID}`, { + credentials: "include", + }).then((response) => { + return response.json().then((data: LoginDto) => ({ + ...data, + statusText: response.statusText, + ok: response.ok, + })); + }); +}; + +export const refreshToken = async (apiBaseUrl: string, vatsimToken: string) => { + return fetch(`${apiBaseUrl}/auth/refresh?vatsimToken=${vatsimToken}`).then((r) => + r.text().then((data) => ({ data, statusText: r.statusText, ok: r.ok })) + ); +}; + +export const fetchVnasConfiguration = async () => { + const response = await fetch(VNAS_CONFIG_URL); + + if (!response.ok) { + throw new Error(`Failed to fetch vNAS configuration: ${response.statusText}`); + } + + return await response.json(); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..a0ea1c8 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useRootDispatch } from '../redux/hooks'; +import { logoutThunk } from '../redux/slices/authSlice'; +import { useHubConnector } from '../hooks/useHubConnector'; + +const Header = () => { + const dispatch = useRootDispatch(); + const { disconnectHub } = useHubConnector(); + + const handleLogout = () => { + disconnectHub(); + dispatch(logoutThunk(true)); + }; + + return ( +
+

FDIO ALPHA

+ +
+ + ) +} + +export default Header \ No newline at end of file diff --git a/src/components/InputArea.tsx b/src/components/InputArea.tsx new file mode 100644 index 0000000..a40260e --- /dev/null +++ b/src/components/InputArea.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useRef, useState } from 'react'; + +const CommandInput = ({ isProcessing = false }: { isProcessing?: boolean }) => { + const terminalInputRef = useRef(null); + const [typedCommand, setTypedCommand] = useState(''); + const [cursorVisible, setCursorVisible] = useState(true); + + useEffect(() => { + const focus = () => terminalInputRef.current?.focus(); + focus(); + + const interval = setInterval(() => { + if (document.activeElement !== terminalInputRef.current) { + focus(); + } + }, 500); + + const blink = setInterval(() => { + setCursorVisible((prev) => !prev); + }, 250); + + return () => { + clearInterval(interval); + clearInterval(blink); + }; + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.preventDefault(); + + if (e.key === 'Enter') { + if (typedCommand.trim() !== '') { + console.log('Executing command:', typedCommand); + // Do something here (API call, parsing, etc.) + } + setTypedCommand(''); + } else if (e.key === 'Escape') { + setTypedCommand(''); + } else if (e.key === 'Backspace') { + setTypedCommand((prev) => prev.slice(0, -1)); + } else if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) { + setTypedCommand((prev) => prev + e.key.toUpperCase()); + } + }; + + return ( +
+
+ > + {typedCommand} + +
+ + {isProcessing && ( +
+ PROCESSING... +
+ )} +
+ ); +}; + +export default CommandInput; diff --git a/src/components/Recat.tsx b/src/components/Recat.tsx new file mode 100644 index 0000000..f2537bb --- /dev/null +++ b/src/components/Recat.tsx @@ -0,0 +1,9 @@ +const Recat = () => { + return ( +
+

RECAT: ON v3.0.5

+
+ ) +} + +export default Recat \ No newline at end of file diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx new file mode 100644 index 0000000..320c439 --- /dev/null +++ b/src/contexts/HubContext.tsx @@ -0,0 +1,505 @@ +import type { ReactNode } from "react"; +import React, { createContext, useCallback, useEffect, useRef, useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import type { HubConnection } from "@microsoft/signalr"; +import { HttpTransportType, HubConnectionBuilder } from "@microsoft/signalr"; +import type { Nullable } from "../types/utility-types"; +import { + clearSession, + envSelector, + setSession, + vatsimTokenSelector, + setSessionIsActive, + setHubConnected, + hubConnectedSelector, + logout, +} from "../redux/slices/authSlice"; +import { refreshToken } from "../api/vNasDataApi"; +import type { ApiSessionInfoDto } from "../types/apiTypes/apiSessionInfoDto"; +import type { ApiFlightplan, CreateOrAmendFlightplanDto } from "../types/apiTypes/apiFlightplan"; +import { ApiTopic } from "../types/apiTypes/apiTopic"; +import { updateFlightplanThunk, deleteFlightplanThunk, initThunk } from "../redux/thunks"; +import { openWindowThunk } from "../redux/thunks/windowThunks"; +import { addOutageMessage, delOutageMessage, setFsdIsConnected } from "../redux/slices/appSlice"; +import { setMcaAcceptMessage, setMcaRejectMessage, setMraMessage } from "../redux/slices/mcaSlice"; +import { setArtccId, setSectorId } from "../redux/slices/sectorSlice"; +import { useRootDispatch, useRootSelector } from "../redux/hooks"; +import { useSocketConnector } from "../hooks/useSocketConnector"; +import { VERSION } from "../utils/constants"; +import { OutageEntry } from "../types/outageEntry"; +import { HubConnectionState } from "@microsoft/signalr/dist/esm/HubConnection"; +import { invokeHub } from "../utils/hubUtils"; +import { type ProcessEramMessageDto, type EramMessageProcessingResultDto, EramPositionType } from "../types/apiTypes/eramTypes"; + +// Simple toast for now +const toast = { + error: (message: string) => console.error(message) +}; + +type HubContextValue = { + connectHub: () => Promise; + disconnectHub: () => Promise; + hubConnection: HubConnection | null; + sendEramMessage: (eramMessage: ProcessEramMessageDto) => Promise; + sendCommand: (command: string) => Promise; + amendFlightplan: (fp: CreateOrAmendFlightplanDto) => Promise; + deleteFlightplan: (aircraftId: string) => Promise; + requestFlightStrip: (aircraftId: string) => Promise; + flightplans: Map; + flightStrips: Map; +}; + +export const HubContext = createContext({ + connectHub: () => Promise.reject(new Error('HubContext not initialized')), + disconnectHub: () => Promise.reject(new Error('HubContext not initialized')), + hubConnection: null, + sendEramMessage: () => Promise.reject(new Error('HubContext not initialized')), + sendCommand: () => Promise.reject(new Error('HubContext not initialized')), + amendFlightplan: () => Promise.reject(new Error('HubContext not initialized')), + deleteFlightplan: () => Promise.reject(new Error('HubContext not initialized')), + requestFlightStrip: () => Promise.reject(new Error('HubContext not initialized')), + flightplans: new Map(), + flightStrips: new Map(), +}); + +export const HubContextProvider = ({ children }: { children: ReactNode }) => { + const dispatch = useRootDispatch(); + const vatsimToken = useRootSelector(vatsimTokenSelector)!; + const ref = useRef>(null); + const { disconnectSocket } = useSocketConnector(); + const env = useRootSelector(envSelector); + const navigate = useNavigate(); + const hubConnected = useRootSelector(hubConnectedSelector); + const [flightplans, setFlightplans] = useState>(new Map()); + const [flightStrips, setFlightStrips] = useState>(new Map()); + const [facilityId, setFacilityId] = useState(""); + + const disconnectHub = useCallback(async () => { + try { + await ref.current?.stop(); + dispatch(setHubConnected(false)); + dispatch(setArtccId("")); + dispatch(setSectorId("")); + + try { + disconnectSocket(); + } catch (error) { + console.warn("Error disconnecting socket:", error); + } + + dispatch(clearSession()); + dispatch(logout()); + navigate("/login", { replace: true }); + } catch (error) { + console.error("Error during hub disconnect:", error); + navigate("/login", { replace: true }); + } + }, [disconnectSocket, dispatch, navigate]); + + const handleSessionStart = useCallback( + async (sessionInfo: ApiSessionInfoDto, hubConnection: HubConnection) => { + if (!sessionInfo || sessionInfo.isPseudoController) { + return; + } + + try { + const primaryPosition = sessionInfo.positions.find((p) => p.isPrimary)?.position; + + if (!primaryPosition) { + throw new Error("No primary position found"); + } + + const artccId = sessionInfo.artccId; + // For ERAM positions, use the sectorId; for other positions like tower, use the callsign + const sectorId = primaryPosition.eramConfiguration?.sectorId || primaryPosition.callsign || "UNKNOWN"; + + console.log('Primary position details:', { + callsign: primaryPosition.callsign, + name: primaryPosition.name, + hasEramConfig: !!primaryPosition.eramConfiguration, + eramSectorId: primaryPosition.eramConfiguration?.sectorId, + finalSectorId: sectorId + }); + + dispatch(setArtccId(artccId)); + dispatch(setSectorId(sectorId)); + dispatch(setSession(sessionInfo)); + dispatch(setSessionIsActive(sessionInfo.isActive ?? false)); + dispatch(initThunk()); + + if (hubConnection.state === HubConnectionState.Connected) { + const joinSessionParams = { + sessionId: sessionInfo.id, + clientName: "vEDST", + clientVersion: VERSION, + hasEramConfig: true, + eramSectorId: 99 + }; + console.log('Sending joinSession with params:', joinSessionParams); + await hubConnection.invoke("joinSession", joinSessionParams); + console.log(`Joined session ${sessionInfo.id} with position ${primaryPosition.callsign} (${primaryPosition.name})`); + + const artccFacilityId = sessionInfo.artccId; // Use ARTCC ID instead of facility ID + const primaryFacilityId = sessionInfo.positions.find((p) => p.isPrimary)?.facilityId; + + // Store facility ID for later use + if (primaryFacilityId) { + setFacilityId(primaryFacilityId); + } + + if (artccFacilityId) { + try { + // Subscribe to FlightPlans using ARTCC ID (e.g., ZOA instead of OAK) + const initialFlightplans = await hubConnection.invoke("subscribe", new ApiTopic("FlightPlans", artccFacilityId)); + + if (initialFlightplans && Array.isArray(initialFlightplans)) { + setFlightplans(prev => { + const newMap = new Map(prev); + initialFlightplans.forEach(fp => { + if (fp?.aircraftId) { + newMap.set(fp.aircraftId, fp); + dispatch(updateFlightplanThunk(fp)); + } + }); + return newMap; + }); + } + + // Subscribe to FlightStrips using facility ID (e.g., OAK) + if (primaryFacilityId) { + const initialFlightStrips = await hubConnection.invoke("subscribe", new ApiTopic("FlightStrips", primaryFacilityId)); + console.log("Subscribed to FlightStrips, received:", initialFlightStrips); + + if (initialFlightStrips && Array.isArray(initialFlightStrips)) { + setFlightStrips(prev => { + const newMap = new Map(prev); + initialFlightStrips.forEach(strip => { + if (strip?.aircraftId) { + newMap.set(strip.aircraftId, strip); + } + }); + return newMap; + }); + } + } + + } catch (subscribeError) { + console.warn(`Failed to subscribe: ${subscribeError}`); + } + } + dispatch(setHubConnected(true)); + } else { + throw new Error("Hub connection not in Connected state"); + } + } catch (error: any) { + console.error("Session start failed:", error); + toast.error(error.message); + await disconnectHub(); + } + }, + [dispatch, disconnectHub] + ); + + useEffect(() => { + if (!env || !vatsimToken) { + return; + } + + const hubUrl = env.clientHubUrl; + + const getValidNasToken = async () => { + return refreshToken(env.apiBaseUrl, vatsimToken).then((r) => { + console.log("Refreshed NAS token"); + return r.data; + }); + }; + + ref.current = new HubConnectionBuilder() + .withUrl(hubUrl, { + accessTokenFactory: getValidNasToken, + transport: HttpTransportType.WebSockets, + skipNegotiation: true, + }) + .withAutomaticReconnect() + .build(); + + const hubConnection = ref.current; + + hubConnection.onclose(() => { + dispatch(setArtccId("")); + dispatch(setSectorId("")); + dispatch(setHubConnected(false)); + console.log("ATC hub disconnected"); + navigate("/login", { replace: true }); + }); + + hubConnection.on("HandleSessionStarted", (sessionInfo: ApiSessionInfoDto) => { + console.log("Session started:", sessionInfo); + handleSessionStart(sessionInfo, hubConnection); + }); + + hubConnection.on("HandleSessionEnded", () => { + console.log("clearing session"); + dispatch(clearSession()); + disconnectHub(); + }); + + hubConnection.on("ReceiveFlightPlans", async (topic: ApiTopic, flightplans: ApiFlightplan[]) => { + if (flightplans && Array.isArray(flightplans)) { + setFlightplans(prev => { + const newMap = new Map(prev); + flightplans.forEach(fp => { + if (fp?.aircraftId) { + newMap.set(fp.aircraftId, fp); + dispatch(updateFlightplanThunk(fp)); + } + }); + return newMap; + }); + } + }); + + hubConnection.on("DeleteFlightplans", async (topic: ApiTopic, flightplanIds: string[]) => { + flightplanIds.forEach((flightplanId) => { + setFlightplans(prev => { + const newMap = new Map(prev); + newMap.delete(flightplanId); + return newMap; + }); + dispatch(deleteFlightplanThunk(flightplanId)); + }); + }); + + hubConnection.on("ReceiveStripItems", async (topic: any, stripItems: any[]) => { + console.log("Received ReceiveStripItems:", stripItems); + if (stripItems && Array.isArray(stripItems)) { + setFlightStrips(prev => { + const newMap = new Map(prev); + stripItems.forEach(strip => { + if (strip?.aircraftId) { + newMap.set(strip.aircraftId, strip); + } + }); + return newMap; + }); + } + }); + + hubConnection.on("HandleFsdConnectionStateChanged", (state: boolean) => { + dispatch(setFsdIsConnected(state)); + if (!state) { + dispatch(addOutageMessage(new OutageEntry("FSD_DOWN", "FSD CONNECTION DOWN"))); + } else { + dispatch(delOutageMessage("FSD_DOWN")); + } + }); + + hubConnection.on("SetSessionActive", (isActive) => { + dispatch(setSessionIsActive(isActive)); + sessionStorage.setItem("session-active", `${isActive}`); + }); + + hubConnection.keepAliveIntervalInMilliseconds = 1000; + }, [dispatch, navigate, disconnectHub, handleSessionStart, env, vatsimToken]); + + const connectHub = useCallback(async () => { + if (!env || !vatsimToken || !ref.current) { + if (ref.current?.state === HubConnectionState.Connected) { + dispatch(setHubConnected(true)); + return; + } + dispatch(setHubConnected(false)); + throw new Error(`Cannot connect - env: ${!!env}, token: ${!!vatsimToken}, ref: ${!!ref.current}`); + } + + const hubConnection = ref.current; + + if (hubConnection.state !== HubConnectionState.Connected) { + try { + await hubConnection.start(); + console.log("Connected to hub, waiting for session..."); + + try { + const sessions = await hubConnection.invoke("GetSessions"); + const primarySession = sessions?.find((s) => !s.isPseudoController); + console.log("Available sessions:", sessions); + const primaryPosition = primarySession?.positions.find((p) => p.isPrimary)?.position; + + console.log(sessions); + console.log(primarySession); + + if (primarySession && primaryPosition) { + console.log(`Found primary position: ${primaryPosition.callsign} (${primaryPosition.name})`); + console.log(hubConnection); + await handleSessionStart(primarySession, hubConnection); + console.log(`joined existing session ${primarySession.id}`); + } else { + console.log("No primary session found, waiting for HandleSessionStarted event"); + } + } catch (error) { + console.log(error); + console.log("No active session yet, waiting for HandleSessionStarted event"); + } + } catch (error) { + dispatch(setHubConnected(false)); + throw error; + } + } + }, [dispatch, handleSessionStart, env, vatsimToken]); + + const sendEramMessage = useCallback(async (eramMessage: ProcessEramMessageDto) => { + return invokeHub(ref.current, connectHub, async (connection) => { + const result = await connection.invoke("processEramMessage", eramMessage); + if (result) { + if (result.isSuccess) { + const feedbackMessage = result.feedback.length > 0 ? result.feedback.join("\n") : "Command accepted"; + console.log("ERAM command processed successfully:", feedbackMessage); + + if (result.response) { + dispatch(setMraMessage(result.response)); + dispatch(openWindowThunk("MESSAGE_RESPONSE_AREA")); + } + } else { + const rejectMessage = result.feedback.length > 0 ? `REJECT\n${result.feedback.join("\n")}` : "REJECT\nCommand failed"; + console.log("ERAM command processing failed:", rejectMessage); + } + } + return result; + }); + }, [connectHub, dispatch]); + + const amendFlightplan = useCallback(async (fp: CreateOrAmendFlightplanDto) => { + return invokeHub(ref.current, connectHub, async (connection) => { + await connection.invoke("amendFlightPlan", fp); + }); + }, [connectHub]); + + const deleteFlightplan = useCallback(async (aircraftId: string) => { + // Workaround: "delete" by amending the flightplan to be blank/empty + try { + console.log(`Attempting to delete flightplan by clearing data: ${aircraftId}`); + await amendFlightplan({ + aircraftId: aircraftId, + cid: '', + status: 'Proposed', + aircraftType: '', + faaEquipmentSuffix: '', + equipment: '', + icaoEquipmentCodes: '', + icaoSurveillanceCodes: '', + speed: 0, + altitude: '', + departure: '', + destination: '', + alternate: '', + route: '', + remarks: '', + assignedBeaconCode: null, + estimatedDepartureTime: 0, + actualDepartureTime: 0, + hoursEnroute: 0, + minutesEnroute: 0, + fuelHours: 0, + fuelMinutes: 0, + pilotCid: '', + holdAnnotations: null, + wakeTurbulenceCode: '', + }); + console.log(`Cleared flightplan data for: ${aircraftId}`); + + // Only remove from local state if server operation succeeded + setFlightplans(prev => { + const newMap = new Map(prev); + newMap.delete(aircraftId); + return newMap; + }); + dispatch(deleteFlightplanThunk(aircraftId)); + } catch (error) { + console.error(`Failed to clear flightplan: ${error}`); + // Re-throw the error so the caller can handle it + throw error; + } + }, [amendFlightplan, dispatch]); + + const requestFlightStrip = useCallback(async (aircraftId: string) => { + return invokeHub(ref.current, connectHub, async (connection) => { + await connection.invoke("RequestFlightStrip", facilityId, aircraftId.toUpperCase()); + }); + }, [connectHub, facilityId]); + + const sendCommand = useCallback(async (command: string): Promise => { + const trimmedCommand = command.trim().toUpperCase(); + const parts = trimmedCommand.split(' '); + + try { + const elements = parts.map(token => ({ + token: token + })); + + // Always use DSide (ERAM) position type to allow ERAM commands from any position + const eramMessage: ProcessEramMessageDto = { + source: EramPositionType.DSide, + elements, + invertNumericKeypad: false + }; + + const result = await sendEramMessage(eramMessage); + + if (result) { + if (result.isSuccess) { + console.log(result); + const feedback = result.feedback?.length > 0 ? result.feedback.join("\n") : ""; + const response = result.response || ""; + + if (feedback && response) { + return `${feedback}\n${response}`; + } else if (response) { + return response; + } else if (feedback) { + return feedback; + } else { + return "COMMAND ACCEPTED"; + } + } else { + return `REJECT: ${result.feedback.join("\n") || "COMMAND FAILED"}`; + } + } else { + return "ERROR: NO RESPONSE FROM SERVER"; + } + + } catch (error) { + console.error('Command processing failed:', error); + return `ERROR: ${error}`; + } + }, [flightplans, sendEramMessage]); + + // Auto-connect when the provider is mounted and we have token + env + useEffect(() => { + if (vatsimToken && env) { + const timer = setTimeout(() => { + console.log("Auto-connecting to hub..."); + connectHub().catch((error) => { + console.error("Auto-connect failed:", error); + }); + }, 1000); // Give a moment for the component to settle + + return () => clearTimeout(timer); + } + }, [vatsimToken, env, connectHub]); + + const contextValue = useMemo(() => ({ + hubConnection: ref.current, + hubConnected, + connectHub, + disconnectHub, + sendEramMessage, + sendCommand, + amendFlightplan, + deleteFlightplan, + requestFlightStrip, + flightplans, + flightStrips, + }), [hubConnected, connectHub, disconnectHub, sendEramMessage, sendCommand, amendFlightplan, deleteFlightplan, requestFlightStrip, flightplans, flightStrips]); + + return {children}; +}; diff --git a/src/contexts/SocketContext.tsx b/src/contexts/SocketContext.tsx new file mode 100644 index 0000000..0278bfa --- /dev/null +++ b/src/contexts/SocketContext.tsx @@ -0,0 +1,13 @@ +import React, { createContext, ReactNode } from 'react'; + +type SocketContextValue = { + // Add socket-related properties here +}; + +export const SocketContext = createContext({}); + +export const SocketContextProvider = ({ children }: { children: ReactNode }) => { + const contextValue = {}; + + return {children}; +}; diff --git a/src/hooks/useHubConnector.ts b/src/hooks/useHubConnector.ts new file mode 100644 index 0000000..5fd2ec9 --- /dev/null +++ b/src/hooks/useHubConnector.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { HubContext } from '../contexts/HubContext'; + +export const useHubConnector = () => { + const context = useContext(HubContext); + + if (!context) { + throw new Error('useHubConnector must be used within a HubContextProvider'); + } + + return context; +}; diff --git a/src/hooks/useSocketConnector.ts b/src/hooks/useSocketConnector.ts new file mode 100644 index 0000000..dc327c1 --- /dev/null +++ b/src/hooks/useSocketConnector.ts @@ -0,0 +1,10 @@ +import { useCallback } from 'react'; + +export const useSocketConnector = () => { + const disconnectSocket = useCallback(() => { + console.log('Disconnecting socket'); + // Implementation for socket disconnection + }, []); + + return { disconnectSocket }; +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..eefa8aa --- /dev/null +++ b/src/index.html @@ -0,0 +1,9 @@ + + + Minimal React application + + +
+ + + diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..3057b2a --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App'; + +const container = document.getElementById('root'); + +const root = ReactDOM.createRoot(container); +root.render(); diff --git a/src/login/Login.tsx b/src/login/Login.tsx new file mode 100644 index 0000000..82921f1 --- /dev/null +++ b/src/login/Login.tsx @@ -0,0 +1,142 @@ +import { faGear } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useEffect } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { + configSelector, + envSelector, + login, + setEnv, + vatsimTokenSelector, + logout, + sessionSelector, + logoutThunk +} from "../redux/slices/authSlice"; +import { useRootDispatch, useRootSelector } from "../redux/hooks"; +import { DOMAIN, VATSIM_CLIENT_ID } from "../utils/constants"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; + +// Simple styles for now +const loginStyles = { + bg: { + position: 'fixed' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: '#1a1a1a', + zIndex: -1, + }, + root: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', + color: 'white', + }, + waiting: { + textAlign: 'center' as const, + margin: '20px 0', + }, + logoutButton: { + padding: '10px 20px', + backgroundColor: '#dc3545', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + margin: '10px', + }, +}; + +function redirectLogin() { + window.location.href = `https://auth.vatsim.net/oauth/authorize?client_id=${VATSIM_CLIENT_ID}&redirect_uri=${encodeURIComponent( + `${DOMAIN}/login` + )}&response_type=code&scope=vatsim_details`; +} + +const Login = () => { + const dispatch = useRootDispatch(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const code = searchParams.get("code"); + const vatsimToken = useRootSelector(vatsimTokenSelector); + const config = useRootSelector(configSelector); + const env = useRootSelector(envSelector); + const hubSession = useRootSelector(sessionSelector); + + useEffect(() => { + if (code && env) { + dispatch( + login({ + code, + redirectUrl: encodeURIComponent(`${DOMAIN}/login`), + }) + ); + } + }, [code, dispatch, env]); + + const handleLogout = () => { + dispatch(logoutThunk(true)); + }; + + useEffect(() => { + if (vatsimToken) { + // Navigate to main app after successful login + // The hub connection will be handled by HubContextProvider + navigate("/", { replace: true }); + } + }, [navigate, vatsimToken]); + + return ( + <> +
+
+
+

vFDIO Login

+ {vatsimToken ? ( + <> +
+
+ Login successful! Redirecting... +
+
+ + + ) : ( + <> + + + + )} +
+
+ + ); +}; + +const LoginProvider = () => ( + + + +); + +export default LoginProvider; diff --git a/src/redux/hooks.ts b/src/redux/hooks.ts new file mode 100644 index 0000000..f239d81 --- /dev/null +++ b/src/redux/hooks.ts @@ -0,0 +1,5 @@ +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +export const useRootDispatch = () => useDispatch(); +export const useRootSelector: TypedUseSelectorHook = useSelector; diff --git a/src/redux/slices/appSlice.ts b/src/redux/slices/appSlice.ts new file mode 100644 index 0000000..dbf9aea --- /dev/null +++ b/src/redux/slices/appSlice.ts @@ -0,0 +1,31 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { OutageEntry } from '../../types/outageEntry'; + +interface AppState { + fsdIsConnected: boolean; + outageMessages: OutageEntry[]; +} + +const initialState: AppState = { + fsdIsConnected: false, + outageMessages: [], +}; + +const appSlice = createSlice({ + name: 'app', + initialState, + reducers: { + setFsdIsConnected: (state, action: PayloadAction) => { + state.fsdIsConnected = action.payload; + }, + addOutageMessage: (state, action: PayloadAction) => { + state.outageMessages.push(action.payload); + }, + delOutageMessage: (state, action: PayloadAction) => { + state.outageMessages = state.outageMessages.filter(msg => msg.id !== action.payload); + }, + }, +}); + +export const { setFsdIsConnected, addOutageMessage, delOutageMessage } = appSlice.actions; +export default appSlice.reducer; diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts new file mode 100644 index 0000000..0d4e788 --- /dev/null +++ b/src/redux/slices/authSlice.ts @@ -0,0 +1,214 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { Nullable } from "../../types/utility-types"; +import type { ApiSessionInfoDto } from "../../types/apiTypes/apiSessionInfoDto"; +import { login as apiLogin, fetchVnasConfiguration } from "../../api/vNasDataApi"; +import type { RootState } from "../store"; +import * as jose from "jose"; + +// Simple toast function for now - displays both console error and alert +const toast = { + error: (message: string, options?: any) => { + console.error(message); + // For development, use alert. In production, you'd want to use a proper toast library + if (typeof window !== 'undefined') { + alert(`Error: ${message}`); + } + } +}; + +function tokenHasExpired(token: jose.JWTPayload) { + return token.exp! - Math.trunc(Date.now() / 1000) < 0; +} + +function getLocalVatsimToken() { + const vatsimToken = localStorage.getItem("vatsim-token"); + if (!vatsimToken) { + return null; + } + + const decodedToken = jose.decodeJwt(vatsimToken); + if (!tokenHasExpired(decodedToken)) { + return vatsimToken; + } + + return null; +} + +type Environment = { + name: string; + apiBaseUrl: string; + clientHubUrl: string; + isSweatbox: boolean; + isPrimary?: boolean; + isDisabled?: boolean; +}; + +type AuthState = { + vnasConfiguration: Nullable; + vatsimCode: Nullable; + vatsimToken: Nullable; + environment: Nullable; + session: Nullable; + sessionActive: boolean; + hubConnected: boolean; +}; + +type Config = { + artccBoundariesUrl: string; + artccAoisUrl: string; + environments: Environment[]; +}; + +type CodeExchangeProps = { + code: string; + redirectUrl: string; +}; + +const initialState: AuthState = { + vnasConfiguration: null, + vatsimCode: null, + vatsimToken: getLocalVatsimToken(), + environment: null, + session: null, + sessionActive: false, + hubConnected: false, +}; + +export const getVnasConfig = createAsyncThunk("auth/getVnasConfig", async () => { + try { + const config = await fetchVnasConfiguration(); + return config; + } catch (error) { + console.error('Failed to fetch vNAS configuration:', error); + throw error; + } +}); + +export const login = createAsyncThunk>, CodeExchangeProps, { state: RootState }>( + "auth/login", + async (data, thunkAPI) => { + const environment = thunkAPI.getState().auth.environment; + if (!environment) { + toast.error(`vNAS Environment not set. Failed to log in`); + throw new Error("Environment not set"); + } + return apiLogin(environment.apiBaseUrl, data.code, data.redirectUrl); + } +); + +export const logoutThunk = createAsyncThunk("auth/logoutThunk", async (shouldReload: boolean = false, { dispatch }) => { + dispatch(authSlice.actions.logout()); + dispatch(setEnv("")); + localStorage.removeItem("vatsim-token"); + localStorage.removeItem("vedst-environment"); + + if (shouldReload) { + const currentPath = window.location.pathname; + window.history.replaceState({}, document.title, currentPath); + window.location.reload(); + } +}); + +export const authSlice = createSlice({ + name: "auth", + initialState, + extraReducers: (builder) => { + builder.addCase(getVnasConfig.fulfilled, (state, action) => { + const config = action.payload; + state.vnasConfiguration = action.payload; + const localEnvironment = localStorage.getItem("vedst-environment"); + if (localEnvironment !== null) { + const availableEnvironment = config.environments.find((e) => e.name === localEnvironment); + if (availableEnvironment) { + state.environment = availableEnvironment; + } else { + localStorage.removeItem("vedst-environment"); + state.environment = config.environments[0]; + } + } else { + state.environment = config.environments[0]; + } + }); + builder.addCase(getVnasConfig.rejected, (state, action) => { + console.error('Failed to load vNAS configuration:', action.error.message); + toast.error(`Failed to load vNAS configuration: ${action.error.message}`); + // Fallback to a default configuration for development + state.vnasConfiguration = { + artccBoundariesUrl: "", + artccAoisUrl: "", + environments: [ + { + name: "Test", + apiBaseUrl: "https://test.virtualnas.net/api", + clientHubUrl: "https://test.virtualnas.net/hubs/client", + isSweatbox: true, + isPrimary: true, + isDisabled: false + } + ] + }; + state.environment = state.vnasConfiguration.environments[0]; + }); + builder.addCase(login.fulfilled, (state, action) => { + if (action.payload.ok) { + const newToken = action.payload.vatsimToken; + state.vatsimToken = newToken; + localStorage.setItem("vatsim-token", newToken); + } else { + toast.error(`Failed to log in: ${action.payload.statusText}`); + console.log(`Failed to log in: ${action.payload.statusText}`); + } + }); + builder.addCase(login.rejected, (state, action) => { + state.vatsimToken = null; + state.vatsimCode = null; + localStorage.removeItem("vatsim-token"); + toast.error(`Failed to log in: ${action.error.message}`); + console.log(`Failed to log in: ${action.error.message}`); + }); + }, + reducers: { + setSession(state, action: PayloadAction) { + state.session = action.payload; + }, + clearSession(state) { + state.session = null; + }, + setEnv(state, action: PayloadAction) { + if (state.vnasConfiguration) { + state.environment = state.vnasConfiguration.environments.find((e) => e.name === action.payload) ?? null; + localStorage.setItem("vedst-environment", action.payload); + } + }, + setSessionIsActive(state, action: PayloadAction) { + if (!state.session) { + toast.error(`Failed to set session active status. Session is not defined.`); + return; + } + const active = action.payload; + state.sessionActive = active; + }, + setHubConnected(state, action: PayloadAction) { + state.hubConnected = action.payload; + }, + logout(state) { + state.vatsimCode = null; + state.vatsimToken = null; + state.session = null; + state.environment = null; + state.sessionActive = false; + state.hubConnected = false; + }, + }, +}); + +export const { setSession, clearSession, setEnv, setSessionIsActive, setHubConnected, logout } = authSlice.actions; +export default authSlice.reducer; + +export const vatsimTokenSelector = () => localStorage.getItem("vatsim-token"); +export const configSelector = (state: RootState) => state.auth.vnasConfiguration; +export const envSelector = (state: RootState) => state.auth.environment; +export const sessionActiveSelector = (state: RootState) => state.auth.sessionActive; +export const hubConnectedSelector = (state: RootState) => state.auth.hubConnected; +export const sessionSelector = (state: RootState) => state.auth.session; diff --git a/src/redux/slices/customFlightplanSlice.ts b/src/redux/slices/customFlightplanSlice.ts new file mode 100644 index 0000000..eca048f --- /dev/null +++ b/src/redux/slices/customFlightplanSlice.ts @@ -0,0 +1,209 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { ApiFlightplan } from '../../types/apiTypes/apiFlightplan'; +import type { RootState } from '../store'; + +export interface CustomFlightplanState { + // Current flightplans data - using plain object instead of Map for Redux serialization + flightplans: Record; + + // UI state + selectedFlightplanId: string | null; + viewMode: 'table' | 'detail' | 'stats'; + + // Search and filter state + lastSearchResults: ApiFlightplan[]; + searchCriteria: { + aircraftId?: string; + departure?: string; + destination?: string; + status?: ApiFlightplan['status']; + altitude?: string; + route?: string; + }; + + // Command history + commandHistory: Array<{ + command: string; + timestamp: number; + result: string; + success: boolean; + }>; + + // Loading and error states + isLoading: boolean; + error: string | null; +} + +const initialState: CustomFlightplanState = { + flightplans: {}, + selectedFlightplanId: null, + viewMode: 'table', + lastSearchResults: [], + searchCriteria: {}, + commandHistory: [], + isLoading: false, + error: null, +}; + +const customFlightplanSlice = createSlice({ + name: 'customFlightplan', + initialState, + reducers: { + // Flightplan data management + setFlightplans: (state, action: PayloadAction>) => { + // Payload is already a plain object + state.flightplans = action.payload; + state.error = null; + }, + + addFlightplan: (state, action: PayloadAction) => { + state.flightplans[action.payload.aircraftId] = action.payload; + }, + + updateFlightplan: (state, action: PayloadAction) => { + state.flightplans[action.payload.aircraftId] = action.payload; + }, + + removeFlightplan: (state, action: PayloadAction) => { + delete state.flightplans[action.payload]; + if (state.selectedFlightplanId === action.payload) { + state.selectedFlightplanId = null; + } + }, + + // UI state management + setSelectedFlightplan: (state, action: PayloadAction) => { + state.selectedFlightplanId = action.payload; + }, + + setViewMode: (state, action: PayloadAction<'table' | 'detail' | 'stats'>) => { + state.viewMode = action.payload; + }, + + // Search and filter management + setSearchResults: (state, action: PayloadAction) => { + state.lastSearchResults = action.payload; + }, + + setSearchCriteria: (state, action: PayloadAction) => { + state.searchCriteria = action.payload; + }, + + clearSearchResults: (state) => { + state.lastSearchResults = []; + state.searchCriteria = {}; + }, + + // Command history management + addCommandToHistory: (state, action: PayloadAction<{ + command: string; + result: string; + success: boolean; + }>) => { + const entry = { + ...action.payload, + timestamp: Date.now(), + }; + + state.commandHistory.unshift(entry); + + // Keep only last 50 commands + if (state.commandHistory.length > 50) { + state.commandHistory = state.commandHistory.slice(0, 50); + } + }, + + clearCommandHistory: (state) => { + state.commandHistory = []; + }, + + // Loading and error states + setLoading: (state, action: PayloadAction) => { + state.isLoading = action.payload; + }, + + setError: (state, action: PayloadAction) => { + state.error = action.payload; + state.isLoading = false; + }, + + clearError: (state) => { + state.error = null; + }, + }, +}); + +export const { + setFlightplans, + addFlightplan, + updateFlightplan, + removeFlightplan, + setSelectedFlightplan, + setViewMode, + setSearchResults, + setSearchCriteria, + clearSearchResults, + addCommandToHistory, + clearCommandHistory, + setLoading, + setError, + clearError, +} = customFlightplanSlice.actions; + +// Selectors +export const selectFlightplans = (state: RootState) => state.customFlightplan.flightplans; +export const selectFlightplansArray = (state: RootState) => + Object.values(state.customFlightplan.flightplans); +export const selectSelectedFlightplan = (state: RootState) => { + const id = state.customFlightplan.selectedFlightplanId; + return id ? state.customFlightplan.flightplans[id] : null; +}; +export const selectViewMode = (state: RootState) => state.customFlightplan.viewMode; +export const selectSearchResults = (state: RootState) => state.customFlightplan.lastSearchResults; +export const selectSearchCriteria = (state: RootState) => state.customFlightplan.searchCriteria; +export const selectCommandHistory = (state: RootState) => state.customFlightplan.commandHistory; +export const selectIsLoading = (state: RootState) => state.customFlightplan.isLoading; +export const selectError = (state: RootState) => state.customFlightplan.error; + +// Complex selectors +export const selectFlightplanById = (state: RootState, aircraftId: string) => + state.customFlightplan.flightplans[aircraftId]; + +export const selectFlightplansByStatus = (state: RootState, status: ApiFlightplan['status']) => + Object.values(state.customFlightplan.flightplans).filter((fp: ApiFlightplan) => fp.status === status); + +export const selectFlightplanStatistics = (state: RootState) => { + const flightplans = Object.values(state.customFlightplan.flightplans); + + const stats = { + total: flightplans.length, + byStatus: { + Active: 0, + Proposed: 0, + Tentative: 0, + }, + byEquipmentType: new Map(), + averageSpeed: 0, + altitudeDistribution: new Map(), + }; + + let totalSpeed = 0; + + flightplans.forEach((fp: ApiFlightplan) => { + stats.byStatus[fp.status]++; + + const currentCount = stats.byEquipmentType.get(fp.aircraftType) || 0; + stats.byEquipmentType.set(fp.aircraftType, currentCount + 1); + + totalSpeed += fp.speed; + + const altCount = stats.altitudeDistribution.get(fp.altitude) || 0; + stats.altitudeDistribution.set(fp.altitude, altCount + 1); + }); + + stats.averageSpeed = flightplans.length > 0 ? Math.round(totalSpeed / flightplans.length) : 0; + + return stats; +}; + +export default customFlightplanSlice.reducer; \ No newline at end of file diff --git a/src/redux/slices/mcaSlice.ts b/src/redux/slices/mcaSlice.ts new file mode 100644 index 0000000..026981a --- /dev/null +++ b/src/redux/slices/mcaSlice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface McaState { + acceptMessage: string; + rejectMessage: string; + responseMessage: string; +} + +const initialState: McaState = { + acceptMessage: '', + rejectMessage: '', + responseMessage: '', +}; + +const mcaSlice = createSlice({ + name: 'mca', + initialState, + reducers: { + setMcaAcceptMessage: (state, action: PayloadAction) => { + state.acceptMessage = action.payload; + }, + setMcaRejectMessage: (state, action: PayloadAction) => { + state.rejectMessage = action.payload; + }, + setMraMessage: (state, action: PayloadAction) => { + state.responseMessage = action.payload; + }, + }, +}); + +export const { setMcaAcceptMessage, setMcaRejectMessage, setMraMessage } = mcaSlice.actions; +export default mcaSlice.reducer; diff --git a/src/redux/slices/sectorSlice.ts b/src/redux/slices/sectorSlice.ts new file mode 100644 index 0000000..2840191 --- /dev/null +++ b/src/redux/slices/sectorSlice.ts @@ -0,0 +1,27 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface SectorState { + artccId: string; + sectorId: string; +} + +const initialState: SectorState = { + artccId: '', + sectorId: '', +}; + +const sectorSlice = createSlice({ + name: 'sector', + initialState, + reducers: { + setArtccId: (state, action: PayloadAction) => { + state.artccId = action.payload; + }, + setSectorId: (state, action: PayloadAction) => { + state.sectorId = action.payload; + }, + }, +}); + +export const { setArtccId, setSectorId } = sectorSlice.actions; +export default sectorSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts new file mode 100644 index 0000000..c206ea3 --- /dev/null +++ b/src/redux/store.ts @@ -0,0 +1,19 @@ +import { configureStore } from '@reduxjs/toolkit'; +import authReducer from './slices/authSlice'; +import appReducer from './slices/appSlice'; +import sectorReducer from './slices/sectorSlice'; +import mcaReducer from './slices/mcaSlice'; +import customFlightplanReducer from './slices/customFlightplanSlice'; + +export const store = configureStore({ + reducer: { + auth: authReducer, + app: appReducer, + sector: sectorReducer, + mca: mcaReducer, + customFlightplan: customFlightplanReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/src/redux/thunks/index.ts b/src/redux/thunks/index.ts new file mode 100644 index 0000000..329a093 --- /dev/null +++ b/src/redux/thunks/index.ts @@ -0,0 +1,47 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import type { ApiFlightplan, EramTrackDto } from '../../types/apiTypes'; + +export const updateFlightplanThunk = createAsyncThunk( + 'flightplan/update', + async (flightplan: ApiFlightplan) => { + // Implementation for updating flightplan + console.log('Updating flightplan:', flightplan); + return flightplan; + } +); + +export const deleteFlightplanThunk = createAsyncThunk( + 'flightplan/delete', + async (flightplanId: string) => { + // Implementation for deleting flightplan + console.log('Deleting flightplan:', flightplanId); + return flightplanId; + } +); + +export const updateTrackThunk = createAsyncThunk( + 'track/update', + async (track: EramTrackDto) => { + // Implementation for updating track + console.log('Updating track:', track); + return track; + } +); + +export const deleteTrackThunk = createAsyncThunk( + 'track/delete', + async (trackId: string) => { + // Implementation for deleting track + console.log('Deleting track:', trackId); + return trackId; + } +); + +export const initThunk = createAsyncThunk( + 'app/init', + async () => { + // Implementation for app initialization + console.log('Initializing app'); + return true; + } +); diff --git a/src/redux/thunks/windowThunks.ts b/src/redux/thunks/windowThunks.ts new file mode 100644 index 0000000..0ab2442 --- /dev/null +++ b/src/redux/thunks/windowThunks.ts @@ -0,0 +1,10 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +export const openWindowThunk = createAsyncThunk( + 'window/open', + async (windowType: string) => { + console.log('Opening window:', windowType); + // Implementation for opening windows + return windowType; + } +); diff --git a/src/services/customFlightplanCommandParser.ts b/src/services/customFlightplanCommandParser.ts new file mode 100644 index 0000000..84ebf23 --- /dev/null +++ b/src/services/customFlightplanCommandParser.ts @@ -0,0 +1,818 @@ +import type { ApiFlightplan } from '../types/apiTypes/apiFlightplan';import type { ApiFlightplan } from '../types/apiTypes/apiFlightplan'; + +import { CustomFlightplanService } from './customFlightplanService';import { CustomFlightplanService } from './customFlightplanService'; + + + +export interface CommandResult {export interface CommandResult { + + output: string; output: string; + + success: boolean; success: boolean; + + data?: any; data?: any; + +}} + + + +/**/** + + * Parser for custom flightplan commands * Parser for custom flightplan commands + + * Simplified to only handle FR (Flight Readout) command * Simplified to only handle FR (Flight Readout) command + + */ */ + +export class CustomFlightplanCommandParser {export class CustomFlightplanCommandParser { + + private flightplanService: CustomFlightplanService; private flightplanService: CustomFlightplanService; + + + + constructor(flightplansMap: Map) { constructor(flightplansMap: Map) { + + this.flightplanService = new CustomFlightplanService(flightplansMap); this.flightplanService = new CustomFlightplanService(flightplansMap); + + } } + + + + /** /** + + * Update flightplan data * Update flightplan data + + */ */ + + updateFlightplans(flightplansMap: Map) { updateFlightplans(flightplansMap: Map) { + + this.flightplanService = new CustomFlightplanService(flightplansMap); this.flightplanService = new CustomFlightplanService(flightplansMap); + + } } + + + + /** /** + + * Execute a command and return the result * Execute a command and return the result + + */ */ + + executeCommand(commandLine: string): CommandResult { executeCommand(commandLine: string): CommandResult { + + const parts = commandLine.trim().split(/\s+/); const parts = commandLine.trim().split(/\s+/); + + const command = parts[0].toUpperCase(); const command = parts[0].toUpperCase(); + + + + try { try { + + switch (command) { switch (command) { + + case 'FR': case 'FR': + + return this.handleFlightReadoutCommand(parts.slice(1)); return this.handleFlightReadoutCommand(parts.slice(1)); + + + + default: default: + + return { return { + + output: `Unknown command: ${command}. Only 'FR ' is supported.`, output: `Unknown command: ${command}. Only 'FR ' is supported.`, + + success: false success: false + + }; }; + + } } + + } catch (error) { } catch (error) { + + return { return { + + output: `Error executing command: ${error}`, output: `Error executing command: ${error}`, + + success: false success: false + + }; }; + + } } + + } } + + + + /** /** + + * Handle FR (Flight Readout) command - ERAM style flightplan readout * Handle FR (Flight Readout) command - ERAM style flightplan readout + + * Usage: FR * Usage: FR + + */ */ + + private handleFlightReadoutCommand(args: string[]): CommandResult { private handleFlightReadoutCommand(args: string[]): CommandResult { + + if (args.length === 0) { if (args.length === 0) { + + return { return { + + output: 'Usage: FR ', output: 'Usage: FR ', + + success: false success: false + + }; }; + + } } + + + + const aircraftId = args[0].toUpperCase(); const aircraftId = args[0].toUpperCase(); + + const allFlightplans = this.flightplanService.getAllFlightplans(); const allFlightplans = this.flightplanService.getAllFlightplans(); + + const flightplan = this.flightplanService.getFlightplanById(aircraftId); const flightplan = this.flightplanService.getFlightplanById(aircraftId); + + + + if (!flightplan) { if (!flightplan) { + + // More helpful error message for debugging // More helpful error message for debugging + + const similarIds = allFlightplans const similarIds = allFlightplans + + .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) + + .map(fp => fp.aircraftId) .map(fp => fp.aircraftId) + + .slice(0, 3); .slice(0, 3); + + + + if (similarIds.length > 0) { if (similarIds.length > 0) { + + return { return { + + output: `REJECT - FLID NOT STORED\nSimilar IDs found: ${similarIds.join(', ')}\nTotal flightplans: ${allFlightplans.length}`, output: `REJECT - FLID NOT STORED\\nSimilar IDs found: ${similarIds.join(', ')}\\nTotal flightplans: ${allFlightplans.length}`, + + success: false success: false + + }; }; + + } } + + + + return { return { + + output: `REJECT - FLID NOT STORED\nTotal flightplans: ${allFlightplans.length}`, output: `REJECT - FLID NOT STORED\\nTotal flightplans: ${allFlightplans.length}`, + + success: false success: false + + }; }; + + } } + + + + // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) + + const duplicates = this.flightplanService.getAllFlightplans().filter(fp => const duplicates = this.flightplanService.getAllFlightplans().filter(fp => + + fp.aircraftId.toUpperCase() === aircraftId fp.aircraftId.toUpperCase() === aircraftId + + ); ); + + + + if (duplicates.length > 1) { if (duplicates.length > 1) { + + // Format duplicate list with CID, departure, and ETD // Format duplicate list with CID, departure, and ETD + + const duplicateList = duplicates.map(fp => { const duplicateList = duplicates.map(fp => { + + const etd = new Date(fp.estimatedDepartureTime * 1000); const etd = new Date(fp.estimatedDepartureTime * 1000); + + const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format + + return `${fp.cid} ${fp.departure} ${etdFormatted}`; return `${fp.cid} ${fp.departure} ${etdFormatted}`; + + }).join('\n'); }).join('\\n'); + + + + return { return { + + output: `REJECT - FLID DUPLICATION\n\n${duplicateList}`, output: `REJECT - FLID DUPLICATION\\n\\n${duplicateList}`, + + success: false success: false + + }; }; + + } } + + + + // Format the ERAM-style flight readout // Format the ERAM-style flight readout + + const readout = this.formatERAMFlightReadout(flightplan); const readout = this.formatERAMFlightReadout(flightplan); + + + + return { return { + + output: readout, output: readout, + + success: true, success: true, + + data: flightplan data: flightplan + + }; }; + + } } + + + + /** /** + + * Format flightplan in ERAM FR command style * Format flightplan in ERAM FR command style + + */ */ + + private formatERAMFlightReadout(fp: ApiFlightplan): string { private formatERAMFlightReadout(fp: ApiFlightplan): string { + + // Format estimated departure time // Format estimated departure time + + const etd = new Date(fp.estimatedDepartureTime * 1000); const etd = new Date(fp.estimatedDepartureTime * 1000); + + const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format + + const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format + + + + // Format actual departure time if available // Format actual departure time if available + + let atd = ''; let atd = ''; + + if (fp.actualDepartureTime > 0) { if (fp.actualDepartureTime > 0) { + + const atdDate = new Date(fp.actualDepartureTime * 1000); const atdDate = new Date(fp.actualDepartureTime * 1000); + + atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format + + } } + + + + // Format beacon code // Format beacon code + + const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; + + + + // Format fuel time // Format fuel time + + const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; + + + + // Format enroute time // Format enroute time + + const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; + + + + // Create the ERAM-style readout // Create the ERAM-style readout + + const lines = [ const lines = [ + + `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, + + `${fp.cid.padEnd(8)}`, `${fp.cid.padEnd(8)}`, + + `A${fp.alternate.padEnd(4)}`, `A${fp.alternate.padEnd(4)}`, + + `${fp.equipment.padEnd(10)}`, `${fp.equipment.padEnd(10)}`, + + '', '', + + `${fp.route}`, `${fp.route}`, + + '', '', + + `RMK/${fp.remarks || ''}` `RMK/${fp.remarks || ''}` + + ]; ]; + + + + // Add the formatted header similar to ERAM // Add the formatted header similar to ERAM + + const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; + + + + return [header, '', ...lines].join('\n'); return [header, '', ...lines].join('\\n'); + + } } + +}} + private flightplanService: CustomFlightplanService; + + constructor(flightplansMap: Map) { + this.flightplanService = new CustomFlightplanService(flightplansMap); + } + + /** + * Update the flightplan service with new data + */ + updateFlightplans(flightplansMap: Map) { + this.flightplanService = new CustomFlightplanService(flightplansMap); + } + + /** + * Parse and execute a custom command + */ + executeCommand(commandString: string): CommandResult { + const parts = commandString.trim().split(/\s+/); + const command = parts[0]?.toUpperCase(); + + try { + switch (command) { + case 'FR': + return this.handleFlightReadoutCommand(parts.slice(1)); + + default: + return { + output: `Unknown command: ${command}. Only 'FR ' is supported.`, + success: false + }; + } + } catch (error) { + return { + output: `Error executing command: ${error}`, + success: false + }; + } + } + + /** + * Debug command to show current flightplan data + */ + private handleDebugCommand(): CommandResult { + const allFlightplans = this.flightplanService.getAllFlightplans(); + + if (allFlightplans.length === 0) { + return { + output: [ + 'DEBUG: No flightplans in service', + '', + 'Possible causes:', + '1. Hub not subscribed to FlightPlans topic', + '2. No active flightplans in the system', + '3. Facility ID not set correctly', + '4. Session not active', + '', + 'Check browser console for subscription logs:', + '- "Subscribing to FlightPlans for facility: [ID]"', + '- "received flightplan: [data]"', + '', + 'Try regular ERAM commands to see if any flightplans exist', + 'in the system that should be displayed.' + ].join('\\n'), + success: true + }; + } + + const aircraftIds = allFlightplans.map(fp => fp.aircraftId).slice(0, 10); + const output = [ + `DEBUG: Flightplan Service Status`, + `Total flightplans: ${allFlightplans.length}`, + `First 10 Aircraft IDs: ${aircraftIds.join(', ')}`, + '', + 'Sample flightplan data:', + allFlightplans[0] ? `${allFlightplans[0].aircraftId}: ${allFlightplans[0].departure} -> ${allFlightplans[0].destination}` : 'None', + '', + 'Ready for FR commands with any of the above Aircraft IDs' + ].join('\\n'); + + return { + output, + success: true, + data: allFlightplans + }; + } + + /** + * Handle FR (Flight Readout) command - ERAM style flightplan readout + * Usage: FR + */ + private handleFlightReadoutCommand(args: string[]): CommandResult { + if (args.length === 0) { + return { + output: 'REJECT - FLID NOT STORED', + success: false + }; + } + + const aircraftId = args[0].toUpperCase(); + + // Debug: Check how many flightplans we have and list some IDs + const allFlightplans = this.flightplanService.getAllFlightplans(); + console.log(`FR Debug: Looking for ${aircraftId}, have ${allFlightplans.length} flightplans`); + console.log('Available Aircraft IDs:', allFlightplans.slice(0, 5).map(fp => fp.aircraftId)); + + const flightplan = this.flightplanService.getFlightplanById(aircraftId); + + if (!flightplan) { + // More helpful error message for debugging + const similarIds = allFlightplans + .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) + .map(fp => fp.aircraftId) + .slice(0, 3); + + if (similarIds.length > 0) { + return { + output: `REJECT - FLID NOT STORED\\nSimilar IDs found: ${similarIds.join(', ')}\\nTotal flightplans: ${allFlightplans.length}`, + success: false + }; + } + + return { + output: `REJECT - FLID NOT STORED\\nTotal flightplans: ${allFlightplans.length}`, + success: false + }; + } + + // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) + const duplicates = this.flightplanService.getAllFlightplans().filter(fp => + fp.aircraftId.toUpperCase() === aircraftId + ); + + if (duplicates.length > 1) { + // Format duplicate list with CID, departure, and ETD + const duplicateList = duplicates.map(fp => { + const etd = new Date(fp.estimatedDepartureTime * 1000); + const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format + return `${fp.cid} ${fp.departure} ${etdFormatted}`; + }).join('\\n'); + + return { + output: `REJECT - FLID DUPLICATION\\n\\n${duplicateList}`, + success: false + }; + } + + // Format the ERAM-style flight readout + const readout = this.formatERAMFlightReadout(flightplan); + + return { + output: readout, + success: true, + data: flightplan + }; + } + + /** + * Format flightplan in ERAM FR command style + */ + private formatERAMFlightReadout(fp: ApiFlightplan): string { + // Format estimated departure time + const etd = new Date(fp.estimatedDepartureTime * 1000); + const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format + const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format + + // Format actual departure time if available + let atd = ''; + if (fp.actualDepartureTime > 0) { + const atdDate = new Date(fp.actualDepartureTime * 1000); + atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format + } + + // Format beacon code + const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; + + // Format fuel time + const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; + + // Format enroute time + const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; + + // Create the ERAM-style readout + const lines = [ + `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, + `${fp.cid.padEnd(8)}`, + `A${fp.alternate.padEnd(4)}`, + `${fp.equipment.padEnd(10)}`, + '', + `${fp.route}`, + '', + `RMK/${fp.remarks || ''}` + ]; + + // Add the formatted header similar to ERAM + const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; + + return [header, '', ...lines].join('\\n'); + } +} + + const aircraftId = args[0].toUpperCase(); + const flightplan = this.flightplanService.getFlightplanById(aircraftId); + + if (!flightplan) { + return { + output: `No flightplan found for aircraft: ${aircraftId}`, + success: false + }; + } + + return { + output: this.flightplanService.formatFlightplanForDisplay(flightplan), + success: true, + data: flightplan + }; + } + + /** + * Handle multiple flightplans command with filters + * Usage: FPS [status=] [dep=] [dest=] [alt=] + */ + private handleFlightplansCommand(args: string[]): CommandResult { + const filter: FlightplanFilter = {}; + + // Parse key=value arguments + for (const arg of args) { + const [key, value] = arg.split('='); + if (!key || !value) continue; + + switch (key.toLowerCase()) { + case 'status': + if (['Active', 'Proposed', 'Tentative'].includes(value)) { + filter.status = value as ApiFlightplan['status']; + } + break; + case 'dep': + case 'departure': + filter.departure = value; + break; + case 'dest': + case 'destination': + filter.destination = value; + break; + case 'alt': + case 'altitude': + filter.altitude = value; + break; + case 'route': + filter.route = value; + break; + case 'acid': + case 'aircraftid': + filter.aircraftId = value; + break; + } + } + + const result = this.flightplanService.searchFlightplans(filter); + + if (result.flightplans.length === 0) { + return { + output: 'No flightplans match the specified criteria.', + success: true, + data: result + }; + } + + const output = [ + `Found ${result.filteredCount} of ${result.totalCount} flightplans:`, + '', + this.flightplanService.formatFlightplansTable(result.flightplans) + ].join('\\n'); + + return { + output, + success: true, + data: result + }; + } + + /** + * Handle flightplan list command with simple filters + * Usage: FPL [ALL|ACTIVE|PROPOSED|TENTATIVE] + */ + private handleFlightplanListCommand(args: string[]): CommandResult { + let flightplans: ApiFlightplan[]; + let title = 'All Flightplans'; + + if (args.length > 0) { + const filter = args[0].toUpperCase(); + switch (filter) { + case 'ACTIVE': + flightplans = this.flightplanService.getFlightplansByStatus('Active'); + title = 'Active Flightplans'; + break; + case 'PROPOSED': + flightplans = this.flightplanService.getFlightplansByStatus('Proposed'); + title = 'Proposed Flightplans'; + break; + case 'TENTATIVE': + flightplans = this.flightplanService.getFlightplansByStatus('Tentative'); + title = 'Tentative Flightplans'; + break; + case 'ALL': + default: + flightplans = this.flightplanService.getAllFlightplans(); + break; + } + } else { + flightplans = this.flightplanService.getAllFlightplans(); + } + + if (flightplans.length === 0) { + return { + output: `No ${title.toLowerCase()} found.`, + success: true, + data: flightplans + }; + } + + const output = [ + `${title} (${flightplans.length}):`, + '', + this.flightplanService.formatFlightplansTable(flightplans) + ].join('\\n'); + + return { + output, + success: true, + data: flightplans + }; + } + + /** + * Handle flightplan find command with various search criteria + * Usage: FPF + * Criteria: DEP, DEST, WAYPOINT, ALT, TYPE + */ + private handleFlightplanFindCommand(args: string[]): CommandResult { + if (args.length < 2) { + return { + output: 'Usage: FPF \\nCriteria: DEP, DEST, WAYPOINT, ALT, TYPE\\nExample: FPF DEP KJFK', + success: false + }; + } + + const criteria = args[0].toUpperCase(); + const value = args[1].toUpperCase(); + let flightplans: ApiFlightplan[]; + let description = ''; + + switch (criteria) { + case 'DEP': + case 'DEPARTURE': + flightplans = this.flightplanService.getFlightplansByDeparture(value); + description = `departing from ${value}`; + break; + + case 'DEST': + case 'DESTINATION': + flightplans = this.flightplanService.getFlightplansByDestination(value); + description = `arriving at ${value}`; + break; + + case 'WAYPOINT': + case 'WPT': + flightplans = this.flightplanService.getFlightplansByWaypoint(value); + description = `routing via ${value}`; + break; + + case 'ALT': + case 'ALTITUDE': + flightplans = this.flightplanService.getFlightplansByAltitude(value); + description = `at altitude ${value}`; + break; + + case 'TYPE': + case 'AIRCRAFT': + flightplans = this.flightplanService.getAllFlightplans().filter(fp => + fp.aircraftType.toUpperCase().includes(value) + ); + description = `aircraft type containing ${value}`; + break; + + default: + return { + output: `Unknown search criteria: ${criteria}\\nValid criteria: DEP, DEST, WAYPOINT, ALT, TYPE`, + success: false + }; + } + + if (flightplans.length === 0) { + return { + output: `No flightplans found ${description}.`, + success: true, + data: flightplans + }; + } + + const output = [ + `Found ${flightplans.length} flightplans ${description}:`, + '', + this.flightplanService.formatFlightplansTable(flightplans) + ].join('\\n'); + + return { + output, + success: true, + data: flightplans + }; + } + + /** + * Handle flightplan statistics command + */ + private handleFlightplanStatsCommand(): CommandResult { + const stats = this.flightplanService.getFlightplanStatistics(); + + const equipmentList = Array.from(stats.byEquipmentType.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) // Top 5 + .map(([type, count]) => ` ${type}: ${count}`) + .join('\\n'); + + const altitudeList = Array.from(stats.altitudeDistribution.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) // Top 10 + .map(([alt, count]) => ` ${alt}: ${count}`) + .join('\\n'); + + const output = [ + 'FLIGHTPLAN STATISTICS', + '=====================', + `Total Flightplans: ${stats.total}`, + '', + 'Status Distribution:', + ` Active: ${stats.byStatus.Active}`, + ` Proposed: ${stats.byStatus.Proposed}`, + ` Tentative: ${stats.byStatus.Tentative}`, + '', + 'Top Aircraft Types:', + equipmentList || ' None', + '', + 'Average Speed: ' + stats.averageSpeed + ' knots', + '', + 'Top Altitudes:', + altitudeList || ' None' + ].join('\\n'); + + return { + output, + success: true, + data: stats + }; + } + + /** + * Handle help command + */ + private handleHelpCommand(): CommandResult { + const helpText = [ + 'CUSTOM FLIGHTPLAN COMMANDS', + '==========================', + '', + 'FR - ERAM-style flight readout for specific aircraft', + 'FPDEBUG - Debug current flightplan data and status', + 'FPMOCK - Create mock test data (DAL123, UAL456) for testing', + '', + 'FP - Display detailed flightplan for specific aircraft', + 'FPS [filters] - Display flightplans with optional filters', + ' Filters: status= dep= dest= alt= route=', + ' Example: FPS status=Active dep=KJFK', + '', + 'FPL [status] - List flightplans by status (ALL|ACTIVE|PROPOSED|TENTATIVE)', + 'FPF - Find flightplans by criteria', + ' Criteria: DEP, DEST, WAYPOINT, ALT, TYPE', + ' Example: FPF DEP KJFK', + '', + 'FPSTATS - Display flightplan statistics', + 'FPHELP - Display this help message', + '', + 'Examples:', + ' FPMOCK - Create test data for demonstration', + ' FR DAL123 - ERAM flight readout for DAL123 (after FPMOCK)', + ' FP UAL123 - Show details for UAL123', + ' FPS dep=KJFK dest=KLAX - Find flights from JFK to LAX', + ' FPL ACTIVE - List all active flightplans', + ' FPF WAYPOINT HOFFA - Find flights routing via HOFFA', + ' FPSTATS - Show statistics summary' + ].join('\\n'); + + return { + output: helpText, + success: true + }; + } +} \ No newline at end of file diff --git a/src/services/customFlightplanService.ts b/src/services/customFlightplanService.ts new file mode 100644 index 0000000..1e3b1ec --- /dev/null +++ b/src/services/customFlightplanService.ts @@ -0,0 +1,230 @@ +import type { ApiFlightplan, CreateOrAmendFlightplanDto } from '../types/apiTypes/apiFlightplan'; + +export interface FlightplanFilter { + aircraftId?: string; + callsign?: string; + departure?: string; + destination?: string; + status?: ApiFlightplan['status']; + altitude?: string; + route?: string; +} + +export interface FlightplanSearchResult { + flightplans: ApiFlightplan[]; + totalCount: number; + filteredCount: number; +} + +export class CustomFlightplanService { + private flightplans: Map; + + constructor(flightplansMap: Map) { + this.flightplans = flightplansMap; + } + + /** + * Get all flightplans as an array + */ + getAllFlightplans(): ApiFlightplan[] { + return Array.from(this.flightplans.values()); + } + + /** + * Get a specific flightplan by aircraft ID + */ + getFlightplanById(aircraftId: string): ApiFlightplan | undefined { + return this.flightplans.get(aircraftId); + } + + /** + * Search and filter flightplans based on criteria + */ + searchFlightplans(filter: FlightplanFilter): FlightplanSearchResult { + const allFlightplans = this.getAllFlightplans(); + + let filteredFlightplans = allFlightplans; + + if (filter.aircraftId) { + const aircraftIdUpper = filter.aircraftId.toUpperCase(); + filteredFlightplans = filteredFlightplans.filter(fp => + fp.aircraftId.toUpperCase().includes(aircraftIdUpper) + ); + } + + if (filter.departure) { + const depUpper = filter.departure.toUpperCase(); + filteredFlightplans = filteredFlightplans.filter(fp => + fp.departure.toUpperCase().includes(depUpper) + ); + } + + if (filter.destination) { + const destUpper = filter.destination.toUpperCase(); + filteredFlightplans = filteredFlightplans.filter(fp => + fp.destination.toUpperCase().includes(destUpper) + ); + } + + if (filter.status) { + filteredFlightplans = filteredFlightplans.filter(fp => + fp.status === filter.status + ); + } + + if (filter.altitude) { + filteredFlightplans = filteredFlightplans.filter(fp => + fp.altitude.includes(filter.altitude!) + ); + } + + if (filter.route) { + const routeUpper = filter.route.toUpperCase(); + filteredFlightplans = filteredFlightplans.filter(fp => + fp.route.toUpperCase().includes(routeUpper) + ); + } + + return { + flightplans: filteredFlightplans, + totalCount: allFlightplans.length, + filteredCount: filteredFlightplans.length + }; + } + + /** + * Get flightplans by status + */ + getFlightplansByStatus(status: ApiFlightplan['status']): ApiFlightplan[] { + return this.getAllFlightplans().filter(fp => fp.status === status); + } + + /** + * Get flightplans by departure airport + */ + getFlightplansByDeparture(departure: string): ApiFlightplan[] { + const depUpper = departure.toUpperCase(); + return this.getAllFlightplans().filter(fp => + fp.departure.toUpperCase() === depUpper + ); + } + + /** + * Get flightplans by destination airport + */ + getFlightplansByDestination(destination: string): ApiFlightplan[] { + const destUpper = destination.toUpperCase(); + return this.getAllFlightplans().filter(fp => + fp.destination.toUpperCase() === destUpper + ); + } + + /** + * Get flightplans by route containing a specific waypoint + */ + getFlightplansByWaypoint(waypoint: string): ApiFlightplan[] { + const waypointUpper = waypoint.toUpperCase(); + return this.getAllFlightplans().filter(fp => + fp.route.toUpperCase().includes(waypointUpper) + ); + } + + /** + * Get flightplans by altitude + */ + getFlightplansByAltitude(altitude: string): ApiFlightplan[] { + return this.getAllFlightplans().filter(fp => fp.altitude === altitude); + } + + /** + * Get statistics about current flightplans + */ + getFlightplanStatistics() { + const allFlightplans = this.getAllFlightplans(); + const stats = { + total: allFlightplans.length, + byStatus: { + Active: 0, + Proposed: 0, + Tentative: 0 + }, + byEquipmentType: new Map(), + averageSpeed: 0, + altitudeDistribution: new Map() + }; + + let totalSpeed = 0; + + allFlightplans.forEach(fp => { + // Count by status + stats.byStatus[fp.status]++; + + // Count by aircraft type + const currentCount = stats.byEquipmentType.get(fp.aircraftType) || 0; + stats.byEquipmentType.set(fp.aircraftType, currentCount + 1); + + // Sum speeds for average + totalSpeed += fp.speed; + + // Count by altitude + const altCount = stats.altitudeDistribution.get(fp.altitude) || 0; + stats.altitudeDistribution.set(fp.altitude, altCount + 1); + }); + + stats.averageSpeed = allFlightplans.length > 0 ? Math.round(totalSpeed / allFlightplans.length) : 0; + + return stats; + } + + /** + * Format a flightplan for display + */ + formatFlightplanForDisplay(flightplan: ApiFlightplan): string { + const lines = [ + `Aircraft ID: ${flightplan.aircraftId}`, + `CID: ${flightplan.cid}`, + `Status: ${flightplan.status}`, + `Equipment: ${flightplan.equipment} (${flightplan.aircraftType})`, + `Speed: ${flightplan.speed} knots`, + `Altitude: ${flightplan.altitude}`, + `Route: ${flightplan.departure} -> ${flightplan.destination}`, + `Full Route: ${flightplan.route}`, + `Alternate: ${flightplan.alternate}`, + `Beacon Code: ${flightplan.assignedBeaconCode || 'Not Assigned'}`, + `Pilot CID: ${flightplan.pilotCid}`, + `ETD: ${new Date(flightplan.estimatedDepartureTime * 1000).toISOString()}`, + `Fuel: ${flightplan.fuelHours}:${flightplan.fuelMinutes.toString().padStart(2, '0')}`, + `Enroute Time: ${flightplan.hoursEnroute}:${flightplan.minutesEnroute.toString().padStart(2, '0')}`, + `Remarks: ${flightplan.remarks || 'None'}` + ]; + + return lines.join('\n'); + } + + /** + * Format multiple flightplans in a table-like format + */ + formatFlightplansTable(flightplans: ApiFlightplan[]): string { + if (flightplans.length === 0) { + return 'No flightplans found.'; + } + + const header = 'ACID TYPE DEP DEST ALT SPEED STATUS PILOT'; + const separator = '-'.repeat(header.length); + + const rows = flightplans.map(fp => { + const acid = fp.aircraftId.padEnd(8); + const type = fp.aircraftType.substring(0, 4).padEnd(5); + const dep = fp.departure.padEnd(4); + const dest = fp.destination.padEnd(4); + const alt = fp.altitude.padEnd(6); + const speed = fp.speed.toString().padEnd(5); + const status = fp.status.substring(0, 8).padEnd(9); + const pilot = fp.pilotCid.substring(0, 7); + + return `${acid} ${type} ${dep} ${dest} ${alt} ${speed} ${status} ${pilot}`; + }); + + return [header, separator, ...rows].join('\n'); + } +} \ No newline at end of file diff --git a/src/styles/fonts/FDIOv1.ttf b/src/styles/fonts/FDIOv1.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dd94ca552ae7e71cf2dd1169ef856e9b809229b2 GIT binary patch literal 46876 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 TjS-VFcfwX^9JAg06FmJtYtcA` literal 0 HcmV?d00001 diff --git a/src/styles/terminal.css b/src/styles/terminal.css new file mode 100644 index 0000000..74b7907 --- /dev/null +++ b/src/styles/terminal.css @@ -0,0 +1,11 @@ +@import "tailwindcss"; + +@font-face { + font-family: FDIO; + src: url('./fonts/FDIOv1.ttf'); +} + +@theme { + --font-FDIO: FDIO, 'sans-serif'; + --color-fdio-green: #00e10f; +} \ No newline at end of file diff --git a/src/types/aircraftId.ts b/src/types/aircraftId.ts new file mode 100644 index 0000000..cdeb013 --- /dev/null +++ b/src/types/aircraftId.ts @@ -0,0 +1 @@ +export type AircraftId = string; diff --git a/src/types/apiTypes/apiFlightplan.ts b/src/types/apiTypes/apiFlightplan.ts new file mode 100644 index 0000000..94603ea --- /dev/null +++ b/src/types/apiTypes/apiFlightplan.ts @@ -0,0 +1,59 @@ +import type { HoldAnnotations } from "../hold/holdAnnotations"; +import type { Nullable } from "../utility-types"; + +export type ApiFlightplan = { + aircraftId: string; + cid: string; + status: "Proposed" | "Active" | "Tentative"; + assignedBeaconCode: Nullable; + equipment: string; + aircraftType: string; + icaoEquipmentCodes: string; + icaoSurveillanceCodes: string; + faaEquipmentSuffix: string; + speed: number; + altitude: string; + departure: string; + destination: string; + alternate: string; + route: string; + estimatedDepartureTime: number; + actualDepartureTime: number; + fuelHours: number; + fuelMinutes: number; + hoursEnroute: number; + minutesEnroute: number; + pilotCid: string; + remarks: string; + holdAnnotations: null; + wakeTurbulenceCode: string; +}; + +type CreateOrAmendFlightplanDtoKeys = + | "aircraftId" + | "cid" + | "status" + | "assignedBeaconCode" + | "equipment" + | "aircraftType" + | "icaoEquipmentCodes" + | "icaoSurveillanceCodes" + | "faaEquipmentSuffix" + | "speed" + | "altitude" + | "departure" + | "destination" + | "alternate" + | "route" + | "estimatedDepartureTime" + | "actualDepartureTime" + | "fuelHours" + | "fuelMinutes" + | "hoursEnroute" + | "minutesEnroute" + | "pilotCid" + | "remarks" + | "holdAnnotations" + | "wakeTurbulenceCode"; + +export type CreateOrAmendFlightplanDto = Pick; diff --git a/src/types/apiTypes/apiSessionInfoDto.ts b/src/types/apiTypes/apiSessionInfoDto.ts new file mode 100644 index 0000000..3afdc28 --- /dev/null +++ b/src/types/apiTypes/apiSessionInfoDto.ts @@ -0,0 +1,24 @@ +export interface ApiSessionInfoDto { + id: string; + artccId: string; + isActive?: boolean; + isPseudoController: boolean; + callsign?: string; + role?: string; + positions: Array<{ + isPrimary: boolean; + facilityId: string; + position: { + id: string; + callsign: string; + name: string; + radioName: string; + frequency: number; + starred: boolean; + eramConfiguration: { + sectorId: string; + } | null; + starsConfiguration: any | null; + }; + }>; +} diff --git a/src/types/apiTypes/apiTopic.ts b/src/types/apiTypes/apiTopic.ts new file mode 100644 index 0000000..0eb91ea --- /dev/null +++ b/src/types/apiTypes/apiTopic.ts @@ -0,0 +1,8 @@ +export class ApiTopic { + constructor( + public category: string, + public facilityId: string, + public subset: number | null = null, + public sectorId: string | null = null + ) {} +} \ No newline at end of file diff --git a/src/types/apiTypes/eramTypes.ts b/src/types/apiTypes/eramTypes.ts new file mode 100644 index 0000000..d281d7c --- /dev/null +++ b/src/types/apiTypes/eramTypes.ts @@ -0,0 +1,23 @@ +export enum EramPositionType { + RSide, + DSide +} + +export interface EramMessageElement { + token?: string | null; + targetAircraftId?: string | null; + trackAircraftId?: string | null; +} + +export type ProcessEramMessageDto = { + source: EramPositionType | null; + elements: EramMessageElement[]; + invertNumericKeypad: boolean; +}; + +export interface EramMessageProcessingResultDto { + isSuccess: boolean; + autoRecall: boolean; + feedback: string[]; + response?: string; +} diff --git a/src/types/apiTypes/index.ts b/src/types/apiTypes/index.ts new file mode 100644 index 0000000..c95d22a --- /dev/null +++ b/src/types/apiTypes/index.ts @@ -0,0 +1,19 @@ +export type { ApiFlightplan, CreateOrAmendFlightplanDto } from './apiFlightplan'; + +export interface EramTrackDto { + id: string; + callsign: string; + // Add other track properties as needed +} + +export interface ApiAircraftTrack { + id: string; + callsign: string; + // Add other aircraft track properties as needed +} + +export interface OpenPositionDto { + id: string; + name: string; + // Add other position properties as needed +} diff --git a/src/types/hold/holdAnnotations.ts b/src/types/hold/holdAnnotations.ts new file mode 100644 index 0000000..1b5ffa4 --- /dev/null +++ b/src/types/hold/holdAnnotations.ts @@ -0,0 +1,4 @@ +export interface HoldAnnotations { + // Add hold annotation properties as needed + [key: string]: any; +} diff --git a/src/types/outageEntry.ts b/src/types/outageEntry.ts new file mode 100644 index 0000000..8858086 --- /dev/null +++ b/src/types/outageEntry.ts @@ -0,0 +1,3 @@ +export class OutageEntry { + constructor(public id: string, public message: string) {} +} diff --git a/src/types/utility-types.ts b/src/types/utility-types.ts new file mode 100644 index 0000000..aa32b2d --- /dev/null +++ b/src/types/utility-types.ts @@ -0,0 +1 @@ +export type Nullable = T | null; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..8444045 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,4 @@ +export const DOMAIN = process.env.DOMAIN; +export const VATSIM_CLIENT_ID = process.env.VATSIM_CLIENT_ID; +export const VERSION = process.env.VERSION; +export const VNAS_CONFIG_URL = process.env.VNAS_CONFIG_URL; \ No newline at end of file diff --git a/src/utils/hubUtils.ts b/src/utils/hubUtils.ts new file mode 100644 index 0000000..8a644ee --- /dev/null +++ b/src/utils/hubUtils.ts @@ -0,0 +1,35 @@ +import type { HubConnection } from "@microsoft/signalr"; +import { HubConnectionState } from "@microsoft/signalr/dist/esm/HubConnection"; + +export type HubInvocation = (connection: HubConnection) => Promise; + +const ensureConnected = async ( + hubConnection: HubConnection | null, + connectHub: () => Promise +): Promise => { + if (!hubConnection) { + await connectHub(); + return hubConnection; + } + + if (hubConnection.state !== HubConnectionState.Connected) { + await connectHub(); + } + + return hubConnection; +}; + +export const invokeHub = async ( + hubConnection: HubConnection | null, + connectHub: () => Promise, + invocation: HubInvocation +): Promise => { + const connection = await ensureConnected(hubConnection, connectHub); + if (!connection) return; + + try { + return await invocation(connection); + } catch (error) { + console.log("Hub invocation error:", error); + } +}; From cca7eebf40c41cd8fddebcd273002489aba1fd2e Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Sun, 11 Jan 2026 19:00:00 -0800 Subject: [PATCH 02/27] Route field 10 formatting --- src/App.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b42870d..a685f70 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -720,8 +720,8 @@ const AppContent = () => { // Fixed column positions based on ERAM reference (80 char width) // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) - const line1_aircraftId = (fieldValues[0] || '').substring(0, 17).padEnd(17); - const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); + const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); + const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(7); const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 11).padEnd(13); // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces @@ -743,16 +743,25 @@ const AppContent = () => { // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 // But revision number ALSO appears on line 2 at column 3 - let line2_full = line2; // Revision at column 3 + let line2_full = line2; + let line1_route_display = line1_route; + if (route.length > 40) { - const routeContinuation = route.substring(40, 120); // Next 80 chars - // Pad line2 to exactly 40 chars, then add route continuation - line2_full = line2.padEnd(40) + routeContinuation; + const first40 = route.slice(0, 40); + const lastSpace = first40.lastIndexOf(' '); + + const splitIndex = lastSpace !== -1 ? lastSpace : 40; + + // Line 1 shows route up to the word boundary + line1_route_display = route.slice(0, splitIndex); + // Line 2 shows ONLY the continuation, padded so it aligns at column 41 + const secondLine = route.slice(splitIndex).trimStart(); + line2_full = line2.padEnd(40) + secondLine; } // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) const formattedStrip = - line1_aircraftId + line1_beacon + line1_depPoint + line1_route + '\n' + + line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + line2_full + '\n' + line3_typeEquip + line3_time + '\n' + line4_cid + line4_altitude + line4_remarks; From 10d8c2938e6b30454cd90942f30dc30372c7849e Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Mon, 12 Jan 2026 10:29:49 -0800 Subject: [PATCH 03/27] attempt indentation fix --- src/App.tsx | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a685f70..8c5f11f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -87,9 +87,9 @@ const AppContent = () => { // Fixed column positions based on ERAM reference (80 char width) // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) - const line1_aircraftId = (fieldValues[0] || '').substring(0, 17).padEnd(17); + const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); - const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 11).padEnd(13); + const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 7).padEnd(9); // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces let route = (fieldValues[11] || ''); @@ -102,34 +102,37 @@ const AppContent = () => { const line2 = ' ' + (fieldValues[1] || ''); // Line 3: Aircraft Type/Equipment (starts at column 1) - const line3_typeEquip = (fieldValues[3] || '').substring(0, 16).padEnd(18); - const line3_time = (fieldValues[6] || '').substring(0, 7).padEnd(22); + const line3_typeEquip = (fieldValues[3] || '').substring(0, 14).padEnd(14); + const line3_time = (fieldValues[6] || '').substring(0, 6).padEnd(6); // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) - const line4_cid = (fieldValues[4] || '').substring(0, 17).padEnd(18); - const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(22); + const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); + const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); const line4_remarks = (fieldValues[12] || '').substring(0, 40); // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 // But revision number ALSO appears on line 2 at column 3 - let line2_full = line2; // Revision at column 3 + let line2_full = line2; + let line1_route_display = line1_route; + if (route.length > 40) { - const routeContinuation = route.substring(40, 120); // Next 80 chars - // Pad line2 to exactly 40 chars, then add route continuation - line2_full = line2.padEnd(40) + routeContinuation; - console.log('ReceiveStripItems - Line2 debug:', { - line2: `"${line2}"`, - line2_length: line2.length, - line2_padded_length: line2.padEnd(40).length, - routeContinuation: `"${routeContinuation}"` - }); + const first40 = route.slice(0, 40); + const lastSpace = first40.lastIndexOf(' '); + + const splitIndex = lastSpace !== -1 ? lastSpace : 40; + + // Line 1 shows route up to the word boundary + line1_route_display = route.slice(0, splitIndex); + // Line 2 shows ONLY the continuation, padded so it aligns at column 41 + const secondLine = route.slice(splitIndex).trimStart(); + line2_full = line2.padEnd(40) + secondLine; } console.log('ReceiveStripItems - Total route continuation lines:', route.length > 40 ? 1 : 0); // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) const formattedStrip = - line1_aircraftId + line1_beacon + line1_depPoint + line1_route + '\n' + + line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + line2_full + '\n' + line3_typeEquip + line3_time + '\n' + line4_cid + line4_altitude + line4_remarks; @@ -721,24 +724,26 @@ const AppContent = () => { // Fixed column positions based on ERAM reference (80 char width) // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); - const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(7); - const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 11).padEnd(13); + const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); + const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 7).padEnd(9); // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces let route = (fieldValues[11] || ''); route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); const line1_route = route.substring(0, 40); + console.log('Route info:', { fullRoute: route, length: route.length, needsContinuation: route.length > 40 }); + // Line 2: Revision Number (starts at position 3) const line2 = ' ' + (fieldValues[1] || ''); // Line 3: Aircraft Type/Equipment (starts at column 1) - const line3_typeEquip = (fieldValues[3] || '').substring(0, 16).padEnd(18); - const line3_time = (fieldValues[6] || '').substring(0, 7).padEnd(22); + const line3_typeEquip = (fieldValues[3] || '').substring(0, 14).padEnd(14); + const line3_time = (fieldValues[6] || '').substring(0, 6).padEnd(6); // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) - const line4_cid = (fieldValues[4] || '').substring(0, 17).padEnd(18); - const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(22); + const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); + const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); const line4_remarks = (fieldValues[12] || '').substring(0, 40); // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 From 6fc0711a36cf5266820d9ae04a192ff4a0717478 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Mon, 12 Jan 2026 11:40:18 -0800 Subject: [PATCH 04/27] fix indentation and font spacing --- src/App.tsx | 57 ++++++++++-------- src/contexts/HubContext.tsx | 4 +- .../FDIO-font.ttf\357\200\272Zone.Identifier" | 4 ++ src/styles/fonts/FDIOv2.ttf | Bin 0 -> 46584 bytes .../FDIOv2.ttf\357\200\272Zone.Identifier" | 0 src/styles/terminal.css | 2 +- 6 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 "src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" create mode 100644 src/styles/fonts/FDIOv2.ttf create mode 100644 "src/styles/fonts/FDIOv2.ttf\357\200\272Zone.Identifier" diff --git a/src/App.tsx b/src/App.tsx index 8c5f11f..8421e16 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -81,10 +81,10 @@ const AppContent = () => { const handleStripPrint = (topic: any, stripItems: any[]) => { stripItems.forEach(strip => { if (strip?.fieldValues) { + // Format using fieldValues from strip data based on ERAM strip layout + // fieldValues: [0:callsign, 1:rev, 2:?, 3:type/equip, 4:cid, 5:beacon, 6:proptime, 7:alt, 8:dep/arr, 9-10:?, 11:route, 12:remarks] const fieldValues = strip.fieldValues; - console.log('ReceiveStripItems - Full fieldValues:', fieldValues); - // Fixed column positions based on ERAM reference (80 char width) // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); @@ -94,7 +94,8 @@ const AppContent = () => { // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces let route = (fieldValues[11] || ''); route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); - const line1_route = route.substring(0, 40); + let line1_route = route.substring(0, 40); + let line2_route = ''; console.log('Route info:', { fullRoute: route, length: route.length, needsContinuation: route.length > 40 }); @@ -108,11 +109,14 @@ const AppContent = () => { // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); - const line4_remarks = (fieldValues[12] || '').substring(0, 40); - + let line4_remarks = (fieldValues[12] || '').substring(0, 40); + if (route.split('○').length > 1) { + line4_remarks = `○${route.split('○')[1]}`.substring(0, 40); + route = route.split('○')[0]; // Show only the part before the ○ in the route field + } // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 // But revision number ALSO appears on line 2 at column 3 - let line2_full = line2; + //let line2_full = line2; let line1_route_display = line1_route; if (route.length > 40) { @@ -125,16 +129,14 @@ const AppContent = () => { line1_route_display = route.slice(0, splitIndex); // Line 2 shows ONLY the continuation, padded so it aligns at column 41 const secondLine = route.slice(splitIndex).trimStart(); - line2_full = line2.padEnd(40) + secondLine; + line2_route = secondLine; } - console.log('ReceiveStripItems - Total route continuation lines:', route.length > 40 ? 1 : 0); - // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) const formattedStrip = line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + - line2_full + '\n' + - line3_typeEquip + line3_time + '\n' + + line2 + '\n' + + line3_typeEquip + line3_time + line2_route + '\n\n' + line4_cid + line4_altitude + line4_remarks; console.log('ReceiveStripItems - Final formatted strip:', formattedStrip); @@ -339,7 +341,7 @@ const AppContent = () => { wakeTurbulenceCode: '', }); - return `ACCEPT\n${aircraftId}`; + return `ACCEPT\n${aircraftId}`; } catch (error) { console.error('Failed to create flightplan:', error); const errorStr = String(error); @@ -656,7 +658,7 @@ const AppContent = () => { await amendFlightplan(amendDto); - return `ACCEPT\n${amendDto.aircraftId}`; + return `ACCEPT ${amendDto.aircraftId}/${amendDto.cid}`; } catch (error) { console.error('Failed to amend flightplan:', error); const errorStr = String(error); @@ -730,7 +732,8 @@ const AppContent = () => { // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces let route = (fieldValues[11] || ''); route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); - const line1_route = route.substring(0, 40); + let line1_route = route.substring(0, 40); + let line2_route = ''; console.log('Route info:', { fullRoute: route, length: route.length, needsContinuation: route.length > 40 }); @@ -744,11 +747,14 @@ const AppContent = () => { // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); - const line4_remarks = (fieldValues[12] || '').substring(0, 40); - + let line4_remarks = (fieldValues[12] || '').substring(0, 40); + if (route.split('○').length > 1) { + line4_remarks = `○${route.split('○')[1]}`.substring(0, 40); + route = route.split('○')[0]; // Show only the part before the ○ in the route field + } // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 // But revision number ALSO appears on line 2 at column 3 - let line2_full = line2; + //let line2_full = line2; let line1_route_display = line1_route; if (route.length > 40) { @@ -761,14 +767,14 @@ const AppContent = () => { line1_route_display = route.slice(0, splitIndex); // Line 2 shows ONLY the continuation, padded so it aligns at column 41 const secondLine = route.slice(splitIndex).trimStart(); - line2_full = line2.padEnd(40) + secondLine; + line2_route = secondLine; } // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) const formattedStrip = line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + - line2_full + '\n' + - line3_typeEquip + line3_time + '\n' + + line2 + '\n' + + line3_typeEquip + line3_time + line2_route + '\n\n' + line4_cid + line4_altitude + line4_remarks; // Print the strip (move responseBottom to responseTop, set new strip to responseBottom) @@ -800,6 +806,7 @@ const AppContent = () => { if (flightplan) { // Format using ApiFlightplan data // aircraftID aircraftType assignedBeaconCode speed altitude departure route destination remarks + const cid = flightplan.cid || ''; const aircraftId = flightplan.aircraftId || ''; const aircraftType = flightplan.aircraftType || ''; const beaconCode = flightplan.assignedBeaconCode?.toString() || ''; @@ -818,7 +825,7 @@ const AppContent = () => { routeLines.push(route.substring(i, i + maxLineLength)); } - return `${aircraftId} ${aircraftType} ${beaconCode} ${speed} ${time} ${altitude} ${departure} ${route} ${destination} ${remarks}`; + return `${cid} ${aircraftId} ${aircraftType} ${beaconCode} ${speed} ${time} ${altitude} ${departure} ${route} ${destination} ${remarks}`; } else { return `FLID NOT STORED\n${input}`; @@ -913,17 +920,17 @@ const AppContent = () => {
{/* Response Section (top half) */} {/* FDIO max character width is 80 */} -
-
+
+
{responseTop && '================================================================================\n'}{responseTop}
-
+
{responseBottom && '================================================================================\n'}{responseBottom}
{/* Command Section (Bottom Half) */}
- ------------------------------------------------------------------------------------------------ + -------------------------------------------------------------------------------- {isProcessing && (
M E S S A G E W A I T I N G . . .
)} diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index 320c439..c769200 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -130,8 +130,8 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { if (hubConnection.state === HubConnectionState.Connected) { const joinSessionParams = { sessionId: sessionInfo.id, - clientName: "vEDST", - clientVersion: VERSION, + clientName: "vTDLS", + clientVersion: '1.3.0', hasEramConfig: true, eramSectorId: 99 }; diff --git "a/src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" "b/src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" new file mode 100644 index 0000000..f0e6673 --- /dev/null +++ "b/src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://l.facebook.com/ +HostUrl=https://cdn.fbsbx.com/v/t59.2708-21/62047884_3313478595345010_6921569440815382528_n.ttf/FDIO-font.ttf?_nc_cat=108&_nc_sid=0cab14&_nc_ohc=eW4Wjh1cuVAAX9tOUpN&_nc_ht=cdn.fbsbx.com&oh=fc20d475eb6094fab793ff64b5d5b42e&oe=5EA2C623&dl=1&fbclid=IwAR0cqA8-Uguy2NOWwKvdaA5xTnecwSGPfjshtkR_0YdQAydVZh1N56woiTY diff --git a/src/styles/fonts/FDIOv2.ttf b/src/styles/fonts/FDIOv2.ttf new file mode 100644 index 0000000000000000000000000000000000000000..10e6a3b9ab3ab2a7bdb08b88056e022086975ab2 GIT binary patch literal 46584 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 Date: Fri, 16 Jan 2026 10:55:41 -0800 Subject: [PATCH 05/27] Fixed SR of any strips not just existing ones from vStrips --- src/App.tsx | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8421e16..710a678 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -718,6 +718,20 @@ const AppContent = () => { } } + // Always request from server first - this triggers ReceiveStripItems event + try { + console.log('SR: Requesting flight strip for:', aircraftId); + await requestFlightStrip(aircraftId); + console.log('SR: RequestFlightStrip succeeded for:', aircraftId); + } catch (error) { + console.warn('SR: RequestFlightStrip failed:', error); + // If server request fails and we don't have a local copy, reject + if (!strip?.fieldValues) { + return `REJECT\nSTRIP NOT FOUND\n${input}`; + } + } + + // If we have a local copy, display it immediately (server response will update via ReceiveStripItems) if (strip?.fieldValues) { // Format using fieldValues from strip data based on ERAM strip layout // fieldValues: [0:callsign, 1:rev, 2:?, 3:type/equip, 4:cid, 5:beacon, 6:proptime, 7:alt, 8:dep/arr, 9-10:?, 11:route, 12:remarks] @@ -781,18 +795,12 @@ const AppContent = () => { setResponseTop(responseBottom); setResponseBottom(formattedStrip); - // Also request from server to trigger ReceiveStripItems event (which will overwrite this with server's version) - try { - await requestFlightStrip(aircraftId); - } catch (error) { - console.warn('RequestFlightStrip failed:', error); - } - // Return the formatted strip data return formattedStrip; - } else { - return `REJECT\nSTRIP NOT FOUND\n${input}`; } + + // No local strip but server request succeeded - strip will arrive via ReceiveStripItems event + return `ACCEPT SR ${identifier}\nSTRIP REQUESTED`; } case 'FR': { From 895b44200d820b191c9bd05e5c4488d44b7376aa Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Thu, 19 Feb 2026 23:45:47 -0800 Subject: [PATCH 06/27] Implement WR command using VATSIM METAR API --- src/App.tsx | 28 ++++++++++++++++++++++++---- src/components/Header.tsx | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 710a678..bff42e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -689,8 +689,28 @@ const AppContent = () => { return `REJECT FORMAT\n${input}`; } const station = args[0]; - // TODO: Implement weather request - return `ACCEPT WEATHER STAT REQ\n${station}`; + + try { + // Fetch METAR from VATSIM metar API (same as EDST) + const response = await fetch( + `https://metar.vatsim.net/${station}` + ); + + if (!response.ok) { + return `REJECT WEATHER STAT REQ\nSTATION NOT FOUND`; + } + + const metar = await response.text(); + + if (!metar || metar.trim() === '' || metar.includes('No METAR')) { + return `REJECT WEATHER STAT REQ\nNO DATA FOR ${station}`; + } + + return `ACCEPT WEATHER STAT REQ\n${metar.trim()}`; + } catch (error) { + console.error('Failed to fetch METAR:', error); + return `REJECT WEATHER STAT REQ\nFETCH FAILED`; + } } case 'SR': { @@ -974,11 +994,11 @@ const AppContent = () => {
- VNAS HUB: {hubConnected ? 'CONNECTED' : 'DISCONNECTED'} + {/* VNAS HUB: {hubConnected ? 'CONNECTED' : 'DISCONNECTED'} */}
- ARTCC: {session?.artccId?.toUpperCase() || 'N/A'} | STATUS: {session?.isActive ? 'ACTIVE' : 'INACTIVE'} + {/* ARTCC: {session?.artccId?.toUpperCase() || 'N/A'} | STATUS: {session?.isActive ? 'ACTIVE' : 'INACTIVE'} */}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a0ea1c8..ef1430a 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -14,7 +14,7 @@ const Header = () => { return (
-

FDIO ALPHA

+ {/*

FDIO ALPHA

*/} From e94b1f870b7bfa473590891af592e0d71579a890 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:39:04 -0700 Subject: [PATCH 07/27] Remove unused isTypedArray import from App.tsx Removes Node.js built-in 'util/types' import that is unavailable in browser and was unused. This avoids bundling issues and unnecessary polyfills. Resolves Copilot review comment on App.tsx line 14. --- src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index bff42e4..f0a2ec5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,6 @@ import Header from './components/Header'; import InputArea from './components/InputArea'; import Recat from './components/Recat'; import './styles/terminal.css'; -import { isTypedArray } from 'util/types'; import type { ApiFlightplan } from './types/apiTypes/apiFlightplan'; const AppContent = () => { From d79882dd62f9ce845806c13d61ad6ff3e1f2556c Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:39:28 -0700 Subject: [PATCH 08/27] Add null check for root container element in index.tsx document.getElementById can return null, but ReactDOM.createRoot requires a non-null container. Add a guard with a clear error message to prevent a confusing runtime crash if the element is missing. Resolves Copilot review comment on index.tsx lines 6-7. --- src/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 3057b2a..1c94f29 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,5 +5,9 @@ import App from './App'; const container = document.getElementById('root'); +if (!container) { + throw new Error("Root container element with id 'root' not found."); +} + const root = ReactDOM.createRoot(container); root.render(); From d2727cf2f4aff2ab4201ee1d58183397baa62260 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:39:55 -0700 Subject: [PATCH 09/27] Fix holdAnnotations typing to use Nullable holdAnnotations was typed as literal 'null', preventing actual hold annotations from being represented and making the imported HoldAnnotations type unused. Changed to Nullable for proper type support. Resolves Copilot review comment on apiFlightplan.ts lines 1-29. --- src/types/apiTypes/apiFlightplan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/apiTypes/apiFlightplan.ts b/src/types/apiTypes/apiFlightplan.ts index 94603ea..a9baead 100644 --- a/src/types/apiTypes/apiFlightplan.ts +++ b/src/types/apiTypes/apiFlightplan.ts @@ -25,7 +25,7 @@ export type ApiFlightplan = { minutesEnroute: number; pilotCid: string; remarks: string; - holdAnnotations: null; + holdAnnotations: Nullable; wakeTurbulenceCode: string; }; From eb5b566596f018257946313ea673e06128adacd1 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:40:22 -0700 Subject: [PATCH 10/27] Fix HubConnectionState import to use public @microsoft/signalr entrypoint Import was using internal path @microsoft/signalr/dist/esm/HubConnection which can break with dependency/bundler changes. Changed to the public @microsoft/signalr entrypoint for upgrade safety. Resolves Copilot review comment on HubContext.tsx line 30. --- src/contexts/HubContext.tsx | 2 +- src/utils/hubUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index c769200..4734932 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -27,7 +27,7 @@ import { useRootDispatch, useRootSelector } from "../redux/hooks"; import { useSocketConnector } from "../hooks/useSocketConnector"; import { VERSION } from "../utils/constants"; import { OutageEntry } from "../types/outageEntry"; -import { HubConnectionState } from "@microsoft/signalr/dist/esm/HubConnection"; +import { HubConnectionState } from "@microsoft/signalr"; import { invokeHub } from "../utils/hubUtils"; import { type ProcessEramMessageDto, type EramMessageProcessingResultDto, EramPositionType } from "../types/apiTypes/eramTypes"; diff --git a/src/utils/hubUtils.ts b/src/utils/hubUtils.ts index 8a644ee..8371ec6 100644 --- a/src/utils/hubUtils.ts +++ b/src/utils/hubUtils.ts @@ -1,5 +1,5 @@ import type { HubConnection } from "@microsoft/signalr"; -import { HubConnectionState } from "@microsoft/signalr/dist/esm/HubConnection"; +import { HubConnectionState } from "@microsoft/signalr"; export type HubInvocation = (connection: HubConnection) => Promise; From 20e6f2b4ff08e2240c4a4aaa3afc79cabca118e7 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:40:53 -0700 Subject: [PATCH 11/27] Fix hardcoded joinSession params to use real values Replace placeholder clientVersion '1.3.0' with VERSION constant, hardcoded eramSectorId 99 with actual position config value, clientName 'vTDLS' with 'vFDIO', and hardcoded hasEramConfig with actual position config check. Resolves Copilot + jlefkoff review comments on HubContext.tsx lines 134-136. --- src/contexts/HubContext.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index 4734932..bb98ec7 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -130,10 +130,10 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { if (hubConnection.state === HubConnectionState.Connected) { const joinSessionParams = { sessionId: sessionInfo.id, - clientName: "vTDLS", - clientVersion: '1.3.0', - hasEramConfig: true, - eramSectorId: 99 + clientName: "vFDIO", + clientVersion: VERSION, + hasEramConfig: !!primaryPosition.eramConfiguration, + eramSectorId: primaryPosition.eramConfiguration?.sectorId ?? 0 }; console.log('Sending joinSession with params:', joinSessionParams); await hubConnection.invoke("joinSession", joinSessionParams); From 5745ac2b3f700d3e1a385b12531a578005ee6982 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:41:36 -0700 Subject: [PATCH 12/27] Clean up CUSTOM_FLIGHTPLAN_COMMANDS.md - Remove references to non-existent files (useCustomFlightplanCommands.ts, FlightplanVisualization.tsx) - Remove AI-generated filler sections (Features, Integration, Data Sources, Status Display) that don't provide useful reference information - Rename 'Files Created' to 'Files' since these are existing files - Keep command reference content that is useful for end-users Resolves CrazyKidJack + Copilot review comments on CUSTOM_FLIGHTPLAN_COMMANDS.md. --- CUSTOM_FLIGHTPLAN_COMMANDS.md | 42 +---------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/CUSTOM_FLIGHTPLAN_COMMANDS.md b/CUSTOM_FLIGHTPLAN_COMMANDS.md index 5c9f042..ed8b342 100644 --- a/CUSTOM_FLIGHTPLAN_COMMANDS.md +++ b/CUSTOM_FLIGHTPLAN_COMMANDS.md @@ -60,49 +60,9 @@ FPF DEP KORD # Find all departures from Chicago O'Hare FPSTATS # Show statistics summary ``` -## Features - -### Direct DTO Access -- Access to complete `ApiFlightplan` objects -- No dependency on hub-specific commands -- Full control over data filtering and display - -### Rich Filtering -- Filter by aircraft ID, departure, destination, altitude, status, route -- Combine multiple filters in search commands -- Case-insensitive searching - -### Multiple Display Modes -- Table view for multiple flightplans -- Detailed view for individual flightplans -- Statistics view for data analysis - -### Real-time Updates -- Automatically syncs with hub flightplan data -- Redux state management for consistency -- Command history tracking - ## Implementation Details -### Files Created +### Files - `src/services/customFlightplanService.ts` - Core flightplan operations - `src/services/customFlightplanCommandParser.ts` - Command parsing logic -- `src/hooks/useCustomFlightplanCommands.ts` - React hook for integration -- `src/components/FlightplanVisualization.tsx` - UI components - `src/redux/slices/customFlightplanSlice.ts` - State management - -### Integration -The system integrates seamlessly with your existing terminal interface in `App.tsx`. Commands are automatically detected and routed to the custom parser while preserving normal hub command functionality. - -### Data Sources -- Pulls flightplan data from your existing `HubContext` -- Works with the existing `Map` structure -- Maintains compatibility with current hub operations - -## Status Display -The terminal now shows: -- Current flightplan count -- Hub connection status -- Reminder to use `FPHELP` for custom commands - -This gives you complete control over flightplan data visualization and manipulation while maintaining integration with your existing vFDIO system. \ No newline at end of file From 8ff3b39b444fccb6bdc3b317df55f73189bde1aa Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:42:03 -0700 Subject: [PATCH 13/27] Add font-display: swap to custom font face Improves load performance by allowing the browser to show fallback text while the custom FDIO font loads, preventing invisible text (FOIT). Resolves Copilot review comment on terminal.css lines 3-6. --- src/styles/terminal.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/terminal.css b/src/styles/terminal.css index 10e24ae..e711f3d 100644 --- a/src/styles/terminal.css +++ b/src/styles/terminal.css @@ -3,6 +3,7 @@ @font-face { font-family: FDIO; src: url('./fonts/FDIOv2.ttf'); + font-display: swap; } @theme { From f3e9eed52516be813dd9e46f599b8e22590e04bf Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:42:22 -0700 Subject: [PATCH 14/27] Remove duplicate toast+console logging in authSlice.ts Login failure handlers were logging the same message to both toast.error() and console.log(). Keep only toast.error() (which already calls console.error internally) to avoid redundant output. Resolves jlefkoff review comment on authSlice.ts lines 167-168. --- src/redux/slices/authSlice.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts index 0d4e788..12a0c81 100644 --- a/src/redux/slices/authSlice.ts +++ b/src/redux/slices/authSlice.ts @@ -157,7 +157,6 @@ export const authSlice = createSlice({ localStorage.setItem("vatsim-token", newToken); } else { toast.error(`Failed to log in: ${action.payload.statusText}`); - console.log(`Failed to log in: ${action.payload.statusText}`); } }); builder.addCase(login.rejected, (state, action) => { @@ -165,7 +164,6 @@ export const authSlice = createSlice({ state.vatsimCode = null; localStorage.removeItem("vatsim-token"); toast.error(`Failed to log in: ${action.error.message}`); - console.log(`Failed to log in: ${action.error.message}`); }); }, reducers: { From 4ba93bdcb455b4c023d93e1ad00dadc4e6503d1c Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:42:54 -0700 Subject: [PATCH 15/27] Fix vatsimTokenSelector to read from Redux state vatsimTokenSelector was a zero-arg function reading directly from localStorage, bypassing Redux state and causing type issues with useSelector. Changed to proper (state: RootState) => T selector shape using state.auth.vatsimToken (which is already synced with localStorage in thunks/reducers). Resolves Copilot review comment on authSlice.ts lines 209-214. --- src/redux/slices/authSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts index 12a0c81..342b5b2 100644 --- a/src/redux/slices/authSlice.ts +++ b/src/redux/slices/authSlice.ts @@ -204,7 +204,7 @@ export const authSlice = createSlice({ export const { setSession, clearSession, setEnv, setSessionIsActive, setHubConnected, logout } = authSlice.actions; export default authSlice.reducer; -export const vatsimTokenSelector = () => localStorage.getItem("vatsim-token"); +export const vatsimTokenSelector = (state: RootState) => state.auth.vatsimToken; export const configSelector = (state: RootState) => state.auth.vnasConfiguration; export const envSelector = (state: RootState) => state.auth.environment; export const sessionActiveSelector = (state: RootState) => state.auth.sessionActive; From a7031b45792e2046af5502b9b19c0a768c925210 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:44:24 -0700 Subject: [PATCH 16/27] Fix ensureConnected to return updated connection after reconnect ensureConnected was returning the original (stale) hubConnection reference after calling connectHub(), which meant it would always return null when starting from a null connection. Changed to accept a getter function () => HubConnection | null so it can retrieve the updated ref after connectHub() populates it. Resolves Copilot review comment on hubUtils.ts lines 6-19. --- src/contexts/HubContext.tsx | 6 +++--- src/utils/hubUtils.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index bb98ec7..e76d73f 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -348,7 +348,7 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { }, [dispatch, handleSessionStart, env, vatsimToken]); const sendEramMessage = useCallback(async (eramMessage: ProcessEramMessageDto) => { - return invokeHub(ref.current, connectHub, async (connection) => { + return invokeHub(() => ref.current, connectHub, async (connection) => { const result = await connection.invoke("processEramMessage", eramMessage); if (result) { if (result.isSuccess) { @@ -369,7 +369,7 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { }, [connectHub, dispatch]); const amendFlightplan = useCallback(async (fp: CreateOrAmendFlightplanDto) => { - return invokeHub(ref.current, connectHub, async (connection) => { + return invokeHub(() => ref.current, connectHub, async (connection) => { await connection.invoke("amendFlightPlan", fp); }); }, [connectHub]); @@ -422,7 +422,7 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { }, [amendFlightplan, dispatch]); const requestFlightStrip = useCallback(async (aircraftId: string) => { - return invokeHub(ref.current, connectHub, async (connection) => { + return invokeHub(() => ref.current, connectHub, async (connection) => { await connection.invoke("RequestFlightStrip", facilityId, aircraftId.toUpperCase()); }); }, [connectHub, facilityId]); diff --git a/src/utils/hubUtils.ts b/src/utils/hubUtils.ts index 8371ec6..ac7e5e8 100644 --- a/src/utils/hubUtils.ts +++ b/src/utils/hubUtils.ts @@ -4,27 +4,29 @@ import { HubConnectionState } from "@microsoft/signalr"; export type HubInvocation = (connection: HubConnection) => Promise; const ensureConnected = async ( - hubConnection: HubConnection | null, + getHubConnection: () => HubConnection | null, connectHub: () => Promise ): Promise => { + let hubConnection = getHubConnection(); + if (!hubConnection) { await connectHub(); - return hubConnection; + return getHubConnection(); } if (hubConnection.state !== HubConnectionState.Connected) { await connectHub(); } - return hubConnection; + return getHubConnection(); }; export const invokeHub = async ( - hubConnection: HubConnection | null, + getHubConnection: () => HubConnection | null, connectHub: () => Promise, invocation: HubInvocation ): Promise => { - const connection = await ensureConnected(hubConnection, connectHub); + const connection = await ensureConnected(getHubConnection, connectHub); if (!connection) return; try { From de5906108dd358515c0caf407ea5e9c7f4680c33 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:44:48 -0700 Subject: [PATCH 17/27] Fix HubContext default to null for proper useHubConnector guard HubContext was created with a non-null default value, making the null-guard in useHubConnector ineffective (useContext would never return falsy). Changed createContext default to null so the runtime check in useHubConnector properly detects missing Provider. Resolves Copilot review comment on useHubConnector.ts lines 6-10. --- src/contexts/HubContext.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index e76d73f..67fe14f 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -49,18 +49,7 @@ type HubContextValue = { flightStrips: Map; }; -export const HubContext = createContext({ - connectHub: () => Promise.reject(new Error('HubContext not initialized')), - disconnectHub: () => Promise.reject(new Error('HubContext not initialized')), - hubConnection: null, - sendEramMessage: () => Promise.reject(new Error('HubContext not initialized')), - sendCommand: () => Promise.reject(new Error('HubContext not initialized')), - amendFlightplan: () => Promise.reject(new Error('HubContext not initialized')), - deleteFlightplan: () => Promise.reject(new Error('HubContext not initialized')), - requestFlightStrip: () => Promise.reject(new Error('HubContext not initialized')), - flightplans: new Map(), - flightStrips: new Map(), -}); +export const HubContext = createContext(null); export const HubContextProvider = ({ children }: { children: ReactNode }) => { const dispatch = useRootDispatch(); From 7d2af8d4c472de42b24062a9b2bdf99e353a5afd Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:45:49 -0700 Subject: [PATCH 18/27] Remove unused track DTOs and track thunks EramTrackDto, ApiAircraftTrack interfaces and updateTrackThunk, deleteTrackThunk were placeholder stubs with no subscribers to track topics. Also removed placeholder console.log statements and AI-generated comments from remaining thunks. Resolves jlefkoff review comment on apiTypes/index.ts lines 3-13. --- src/redux/thunks/index.ts | 26 +------------------------- src/types/apiTypes/index.ts | 13 ------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/src/redux/thunks/index.ts b/src/redux/thunks/index.ts index 329a093..b4bfab2 100644 --- a/src/redux/thunks/index.ts +++ b/src/redux/thunks/index.ts @@ -1,11 +1,9 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import type { ApiFlightplan, EramTrackDto } from '../../types/apiTypes'; +import type { ApiFlightplan } from '../../types/apiTypes'; export const updateFlightplanThunk = createAsyncThunk( 'flightplan/update', async (flightplan: ApiFlightplan) => { - // Implementation for updating flightplan - console.log('Updating flightplan:', flightplan); return flightplan; } ); @@ -13,35 +11,13 @@ export const updateFlightplanThunk = createAsyncThunk( export const deleteFlightplanThunk = createAsyncThunk( 'flightplan/delete', async (flightplanId: string) => { - // Implementation for deleting flightplan - console.log('Deleting flightplan:', flightplanId); return flightplanId; } ); -export const updateTrackThunk = createAsyncThunk( - 'track/update', - async (track: EramTrackDto) => { - // Implementation for updating track - console.log('Updating track:', track); - return track; - } -); - -export const deleteTrackThunk = createAsyncThunk( - 'track/delete', - async (trackId: string) => { - // Implementation for deleting track - console.log('Deleting track:', trackId); - return trackId; - } -); - export const initThunk = createAsyncThunk( 'app/init', async () => { - // Implementation for app initialization - console.log('Initializing app'); return true; } ); diff --git a/src/types/apiTypes/index.ts b/src/types/apiTypes/index.ts index c95d22a..f16e2da 100644 --- a/src/types/apiTypes/index.ts +++ b/src/types/apiTypes/index.ts @@ -1,19 +1,6 @@ export type { ApiFlightplan, CreateOrAmendFlightplanDto } from './apiFlightplan'; -export interface EramTrackDto { - id: string; - callsign: string; - // Add other track properties as needed -} - -export interface ApiAircraftTrack { - id: string; - callsign: string; - // Add other aircraft track properties as needed -} - export interface OpenPositionDto { id: string; name: string; - // Add other position properties as needed } From e7744ea09c2eb58525771820cce051abd7a2c891 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:55:28 -0700 Subject: [PATCH 19/27] Remove .Zone.Identifier files and add .gitignore entry These are junk files inserted by WSL that should never be committed. Added *Zone.Identifier pattern to .gitignore to prevent future occurrences. Resolves jlefkoff review comment about .Zone.Identifier files. --- .gitignore | 1 + ".postcssrc\357\200\272Zone.Identifier" | 3 --- "src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" | 4 ---- "src/styles/fonts/FDIOv2.ttf\357\200\272Zone.Identifier" | 0 4 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 ".postcssrc\357\200\272Zone.Identifier" delete mode 100644 "src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" delete mode 100644 "src/styles/fonts/FDIOv2.ttf\357\200\272Zone.Identifier" diff --git a/.gitignore b/.gitignore index 35630d1..79fc686 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ *.log .vscode/ coverage/ +*Zone.Identifier diff --git "a/.postcssrc\357\200\272Zone.Identifier" "b/.postcssrc\357\200\272Zone.Identifier" deleted file mode 100644 index ccde74c..0000000 --- "a/.postcssrc\357\200\272Zone.Identifier" +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -HostUrl=https://drive.usercontent.google.com/download?id=1iGkHH39rEFOHEDjznbzXhyafayt5nyhG&export=download&authuser=0&confirm=t&uuid=fb85a7a6-76ac-48f7-8912-7c768145bc9a&at=AN8xHoq109b4hfEhXsocz57Dilfh%3A1758315864130 diff --git "a/src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" "b/src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" deleted file mode 100644 index f0e6673..0000000 --- "a/src/styles/fonts/FDIO-font.ttf\357\200\272Zone.Identifier" +++ /dev/null @@ -1,4 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=https://l.facebook.com/ -HostUrl=https://cdn.fbsbx.com/v/t59.2708-21/62047884_3313478595345010_6921569440815382528_n.ttf/FDIO-font.ttf?_nc_cat=108&_nc_sid=0cab14&_nc_ohc=eW4Wjh1cuVAAX9tOUpN&_nc_ht=cdn.fbsbx.com&oh=fc20d475eb6094fab793ff64b5d5b42e&oe=5EA2C623&dl=1&fbclid=IwAR0cqA8-Uguy2NOWwKvdaA5xTnecwSGPfjshtkR_0YdQAydVZh1N56woiTY diff --git "a/src/styles/fonts/FDIOv2.ttf\357\200\272Zone.Identifier" "b/src/styles/fonts/FDIOv2.ttf\357\200\272Zone.Identifier" deleted file mode 100644 index e69de29..0000000 From ec8070481a6aa54833a11159582742a28c17626c Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:57:25 -0700 Subject: [PATCH 20/27] Remove unused windowThunks, SocketContext, and useSocketConnector - Delete windowThunks.ts (no window system implemented) - Delete SocketContext.tsx (empty stub, not doing shared-state) - Delete useSocketConnector.ts (console.log-only stub) - Remove all imports and usages from HubContext.tsx Resolves jlefkoff review comments on windowThunks.ts and SocketContext.tsx. --- src/contexts/HubContext.tsx | 12 +----------- src/contexts/SocketContext.tsx | 13 ------------- src/hooks/useSocketConnector.ts | 10 ---------- src/redux/thunks/windowThunks.ts | 10 ---------- 4 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 src/contexts/SocketContext.tsx delete mode 100644 src/hooks/useSocketConnector.ts delete mode 100644 src/redux/thunks/windowThunks.ts diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index 67fe14f..c9ace79 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -19,12 +19,10 @@ import type { ApiSessionInfoDto } from "../types/apiTypes/apiSessionInfoDto"; import type { ApiFlightplan, CreateOrAmendFlightplanDto } from "../types/apiTypes/apiFlightplan"; import { ApiTopic } from "../types/apiTypes/apiTopic"; import { updateFlightplanThunk, deleteFlightplanThunk, initThunk } from "../redux/thunks"; -import { openWindowThunk } from "../redux/thunks/windowThunks"; import { addOutageMessage, delOutageMessage, setFsdIsConnected } from "../redux/slices/appSlice"; import { setMcaAcceptMessage, setMcaRejectMessage, setMraMessage } from "../redux/slices/mcaSlice"; import { setArtccId, setSectorId } from "../redux/slices/sectorSlice"; import { useRootDispatch, useRootSelector } from "../redux/hooks"; -import { useSocketConnector } from "../hooks/useSocketConnector"; import { VERSION } from "../utils/constants"; import { OutageEntry } from "../types/outageEntry"; import { HubConnectionState } from "@microsoft/signalr"; @@ -55,7 +53,6 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { const dispatch = useRootDispatch(); const vatsimToken = useRootSelector(vatsimTokenSelector)!; const ref = useRef>(null); - const { disconnectSocket } = useSocketConnector(); const env = useRootSelector(envSelector); const navigate = useNavigate(); const hubConnected = useRootSelector(hubConnectedSelector); @@ -70,12 +67,6 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { dispatch(setArtccId("")); dispatch(setSectorId("")); - try { - disconnectSocket(); - } catch (error) { - console.warn("Error disconnecting socket:", error); - } - dispatch(clearSession()); dispatch(logout()); navigate("/login", { replace: true }); @@ -83,7 +74,7 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { console.error("Error during hub disconnect:", error); navigate("/login", { replace: true }); } - }, [disconnectSocket, dispatch, navigate]); + }, [dispatch, navigate]); const handleSessionStart = useCallback( async (sessionInfo: ApiSessionInfoDto, hubConnection: HubConnection) => { @@ -346,7 +337,6 @@ export const HubContextProvider = ({ children }: { children: ReactNode }) => { if (result.response) { dispatch(setMraMessage(result.response)); - dispatch(openWindowThunk("MESSAGE_RESPONSE_AREA")); } } else { const rejectMessage = result.feedback.length > 0 ? `REJECT\n${result.feedback.join("\n")}` : "REJECT\nCommand failed"; diff --git a/src/contexts/SocketContext.tsx b/src/contexts/SocketContext.tsx deleted file mode 100644 index 0278bfa..0000000 --- a/src/contexts/SocketContext.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { createContext, ReactNode } from 'react'; - -type SocketContextValue = { - // Add socket-related properties here -}; - -export const SocketContext = createContext({}); - -export const SocketContextProvider = ({ children }: { children: ReactNode }) => { - const contextValue = {}; - - return {children}; -}; diff --git a/src/hooks/useSocketConnector.ts b/src/hooks/useSocketConnector.ts deleted file mode 100644 index dc327c1..0000000 --- a/src/hooks/useSocketConnector.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useCallback } from 'react'; - -export const useSocketConnector = () => { - const disconnectSocket = useCallback(() => { - console.log('Disconnecting socket'); - // Implementation for socket disconnection - }, []); - - return { disconnectSocket }; -}; diff --git a/src/redux/thunks/windowThunks.ts b/src/redux/thunks/windowThunks.ts deleted file mode 100644 index 0ab2442..0000000 --- a/src/redux/thunks/windowThunks.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const openWindowThunk = createAsyncThunk( - 'window/open', - async (windowType: string) => { - console.log('Opening window:', windowType); - // Implementation for opening windows - return windowType; - } -); From ed9d0346a6bd856b6187739dcacafe6291c157a2 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 11:58:08 -0700 Subject: [PATCH 21/27] Add runtime validation for required environment variables Fail fast with a clear error listing any missing env vars instead of silently running with undefined values that cause confusing errors later. Resolves jlefkoff review comment on constants.ts lines 1-4. --- src/utils/constants.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8444045..43f16e3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,13 @@ export const DOMAIN = process.env.DOMAIN; export const VATSIM_CLIENT_ID = process.env.VATSIM_CLIENT_ID; export const VERSION = process.env.VERSION; -export const VNAS_CONFIG_URL = process.env.VNAS_CONFIG_URL; \ No newline at end of file +export const VNAS_CONFIG_URL = process.env.VNAS_CONFIG_URL; + +const requiredEnvVars = { DOMAIN, VATSIM_CLIENT_ID, VERSION, VNAS_CONFIG_URL } as const; +const missing = Object.entries(requiredEnvVars) + .filter(([, value]) => !value) + .map(([key]) => key); + +if (missing.length > 0) { + throw new Error(`Missing required environment variables: ${missing.join(", ")}`); +} \ No newline at end of file From 236087ddf0b2251397c2e8d8f47eca19f8096375 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 12:02:22 -0700 Subject: [PATCH 22/27] Fix customFlightplanCommandParser.ts duplicate line corruption Every line in the file was duplicated on the same line (e.g., 'import X;import X;'). Deduplicated all lines and cleaned up extra blank lines that were artifacts of the corruption. Resolves jlefkoff review comment on customFlightplanCommandParser.ts. --- src/services/customFlightplanCommandParser.ts | 492 +++++------------- 1 file changed, 131 insertions(+), 361 deletions(-) diff --git a/src/services/customFlightplanCommandParser.ts b/src/services/customFlightplanCommandParser.ts index 84ebf23..0bbe39d 100644 --- a/src/services/customFlightplanCommandParser.ts +++ b/src/services/customFlightplanCommandParser.ts @@ -1,326 +1,147 @@ -import type { ApiFlightplan } from '../types/apiTypes/apiFlightplan';import type { ApiFlightplan } from '../types/apiTypes/apiFlightplan'; +import type { ApiFlightplan } from '../types/apiTypes/apiFlightplan'; +import { CustomFlightplanService } from './customFlightplanService'; -import { CustomFlightplanService } from './customFlightplanService';import { CustomFlightplanService } from './customFlightplanService'; - - - -export interface CommandResult {export interface CommandResult { - - output: string; output: string; - - success: boolean; success: boolean; - - data?: any; data?: any; - -}} - - - -/**/** - - * Parser for custom flightplan commands * Parser for custom flightplan commands - - * Simplified to only handle FR (Flight Readout) command * Simplified to only handle FR (Flight Readout) command - - */ */ - -export class CustomFlightplanCommandParser {export class CustomFlightplanCommandParser { - - private flightplanService: CustomFlightplanService; private flightplanService: CustomFlightplanService; - - - - constructor(flightplansMap: Map) { constructor(flightplansMap: Map) { - - this.flightplanService = new CustomFlightplanService(flightplansMap); this.flightplanService = new CustomFlightplanService(flightplansMap); - - } } - - - - /** /** - - * Update flightplan data * Update flightplan data - - */ */ - - updateFlightplans(flightplansMap: Map) { updateFlightplans(flightplansMap: Map) { - - this.flightplanService = new CustomFlightplanService(flightplansMap); this.flightplanService = new CustomFlightplanService(flightplansMap); - - } } - - - - /** /** - - * Execute a command and return the result * Execute a command and return the result - - */ */ - - executeCommand(commandLine: string): CommandResult { executeCommand(commandLine: string): CommandResult { - - const parts = commandLine.trim().split(/\s+/); const parts = commandLine.trim().split(/\s+/); - - const command = parts[0].toUpperCase(); const command = parts[0].toUpperCase(); - - - - try { try { - - switch (command) { switch (command) { - - case 'FR': case 'FR': - - return this.handleFlightReadoutCommand(parts.slice(1)); return this.handleFlightReadoutCommand(parts.slice(1)); - - - - default: default: - - return { return { - - output: `Unknown command: ${command}. Only 'FR ' is supported.`, output: `Unknown command: ${command}. Only 'FR ' is supported.`, - - success: false success: false - - }; }; - - } } - - } catch (error) { } catch (error) { - - return { return { - - output: `Error executing command: ${error}`, output: `Error executing command: ${error}`, - - success: false success: false - - }; }; - - } } - - } } - - - - /** /** - - * Handle FR (Flight Readout) command - ERAM style flightplan readout * Handle FR (Flight Readout) command - ERAM style flightplan readout - - * Usage: FR * Usage: FR - - */ */ - - private handleFlightReadoutCommand(args: string[]): CommandResult { private handleFlightReadoutCommand(args: string[]): CommandResult { - - if (args.length === 0) { if (args.length === 0) { - - return { return { - - output: 'Usage: FR ', output: 'Usage: FR ', - - success: false success: false - - }; }; - - } } - - - - const aircraftId = args[0].toUpperCase(); const aircraftId = args[0].toUpperCase(); - - const allFlightplans = this.flightplanService.getAllFlightplans(); const allFlightplans = this.flightplanService.getAllFlightplans(); - - const flightplan = this.flightplanService.getFlightplanById(aircraftId); const flightplan = this.flightplanService.getFlightplanById(aircraftId); - - - - if (!flightplan) { if (!flightplan) { - - // More helpful error message for debugging // More helpful error message for debugging - - const similarIds = allFlightplans const similarIds = allFlightplans - - .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) - - .map(fp => fp.aircraftId) .map(fp => fp.aircraftId) - - .slice(0, 3); .slice(0, 3); +export interface CommandResult { + output: string; + success: boolean; + data?: any; +} - +/** + * Parser for custom flightplan commands + * Simplified to only handle FR (Flight Readout) command + */ +export class CustomFlightplanCommandParser { + private flightplanService: CustomFlightplanService; + constructor(flightplansMap: Map) { + this.flightplanService = new CustomFlightplanService(flightplansMap); + } - if (similarIds.length > 0) { if (similarIds.length > 0) { + /** + * Update flightplan data + */ + updateFlightplans(flightplansMap: Map) { + this.flightplanService = new CustomFlightplanService(flightplansMap); + } - return { return { + /** + * Execute a command and return the result + */ + executeCommand(commandLine: string): CommandResult { + const parts = commandLine.trim().split(/\s+/); + const command = parts[0].toUpperCase(); + try { + switch (command) { + case 'FR': + return this.handleFlightReadoutCommand(parts.slice(1)); + default: + return { + output: `Unknown command: ${command}. Only 'FR ' is supported.`, + success: false + }; + } + } catch (error) { + return { + output: `Error executing command: ${error}`, + success: false + }; + } + } + /** + * Handle FR (Flight Readout) command - ERAM style flightplan readout + * Usage: FR + */ + private handleFlightReadoutCommand(args: string[]): CommandResult { + if (args.length === 0) { + return { + output: 'Usage: FR ', + success: false + }; + } + const aircraftId = args[0].toUpperCase(); + const allFlightplans = this.flightplanService.getAllFlightplans(); + const flightplan = this.flightplanService.getFlightplanById(aircraftId); + if (!flightplan) { + // More helpful error message for debugging + const similarIds = allFlightplans + .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) + .map(fp => fp.aircraftId) + .slice(0, 3); + if (similarIds.length > 0) { + return { output: `REJECT - FLID NOT STORED\nSimilar IDs found: ${similarIds.join(', ')}\nTotal flightplans: ${allFlightplans.length}`, output: `REJECT - FLID NOT STORED\\nSimilar IDs found: ${similarIds.join(', ')}\\nTotal flightplans: ${allFlightplans.length}`, - - success: false success: false - - }; }; - - } } - - - - return { return { - + success: false + }; + } + return { output: `REJECT - FLID NOT STORED\nTotal flightplans: ${allFlightplans.length}`, output: `REJECT - FLID NOT STORED\\nTotal flightplans: ${allFlightplans.length}`, - - success: false success: false - - }; }; - - } } - - - - // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) - - const duplicates = this.flightplanService.getAllFlightplans().filter(fp => const duplicates = this.flightplanService.getAllFlightplans().filter(fp => - - fp.aircraftId.toUpperCase() === aircraftId fp.aircraftId.toUpperCase() === aircraftId - - ); ); - - - - if (duplicates.length > 1) { if (duplicates.length > 1) { - - // Format duplicate list with CID, departure, and ETD // Format duplicate list with CID, departure, and ETD - - const duplicateList = duplicates.map(fp => { const duplicateList = duplicates.map(fp => { - - const etd = new Date(fp.estimatedDepartureTime * 1000); const etd = new Date(fp.estimatedDepartureTime * 1000); - - const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format - - return `${fp.cid} ${fp.departure} ${etdFormatted}`; return `${fp.cid} ${fp.departure} ${etdFormatted}`; - + success: false + }; + } + // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) + const duplicates = this.flightplanService.getAllFlightplans().filter(fp => + fp.aircraftId.toUpperCase() === aircraftId + ); + if (duplicates.length > 1) { + // Format duplicate list with CID, departure, and ETD + const duplicateList = duplicates.map(fp => { + const etd = new Date(fp.estimatedDepartureTime * 1000); + const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format + return `${fp.cid} ${fp.departure} ${etdFormatted}`; }).join('\n'); }).join('\\n'); - - - - return { return { - + return { output: `REJECT - FLID DUPLICATION\n\n${duplicateList}`, output: `REJECT - FLID DUPLICATION\\n\\n${duplicateList}`, + success: false + }; + } + // Format the ERAM-style flight readout + const readout = this.formatERAMFlightReadout(flightplan); + return { + output: readout, + success: true, + data: flightplan + }; + } - success: false success: false - - }; }; - - } } - - - - // Format the ERAM-style flight readout // Format the ERAM-style flight readout - - const readout = this.formatERAMFlightReadout(flightplan); const readout = this.formatERAMFlightReadout(flightplan); - - - - return { return { - - output: readout, output: readout, - - success: true, success: true, - - data: flightplan data: flightplan - - }; }; - - } } - - - - /** /** - - * Format flightplan in ERAM FR command style * Format flightplan in ERAM FR command style - - */ */ - - private formatERAMFlightReadout(fp: ApiFlightplan): string { private formatERAMFlightReadout(fp: ApiFlightplan): string { - - // Format estimated departure time // Format estimated departure time - - const etd = new Date(fp.estimatedDepartureTime * 1000); const etd = new Date(fp.estimatedDepartureTime * 1000); - - const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format - - const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format - - - - // Format actual departure time if available // Format actual departure time if available - - let atd = ''; let atd = ''; - - if (fp.actualDepartureTime > 0) { if (fp.actualDepartureTime > 0) { - - const atdDate = new Date(fp.actualDepartureTime * 1000); const atdDate = new Date(fp.actualDepartureTime * 1000); - - atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format - - } } - - - - // Format beacon code // Format beacon code - - const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; - - - - // Format fuel time // Format fuel time - - const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; - - - - // Format enroute time // Format enroute time - - const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; - - - - // Create the ERAM-style readout // Create the ERAM-style readout - - const lines = [ const lines = [ - - `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, - - `${fp.cid.padEnd(8)}`, `${fp.cid.padEnd(8)}`, - - `A${fp.alternate.padEnd(4)}`, `A${fp.alternate.padEnd(4)}`, - - `${fp.equipment.padEnd(10)}`, `${fp.equipment.padEnd(10)}`, - - '', '', - - `${fp.route}`, `${fp.route}`, - - '', '', - - `RMK/${fp.remarks || ''}` `RMK/${fp.remarks || ''}` - - ]; ]; - - - - // Add the formatted header similar to ERAM // Add the formatted header similar to ERAM - - const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; - - - + /** + * Format flightplan in ERAM FR command style + */ + private formatERAMFlightReadout(fp: ApiFlightplan): string { + // Format estimated departure time + const etd = new Date(fp.estimatedDepartureTime * 1000); + const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format + const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format + // Format actual departure time if available + let atd = ''; + if (fp.actualDepartureTime > 0) { + const atdDate = new Date(fp.actualDepartureTime * 1000); + atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format + } + // Format beacon code + const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; + // Format fuel time + const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; + // Format enroute time + const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; + // Create the ERAM-style readout + const lines = [ + `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, + `${fp.cid.padEnd(8)}`, + `A${fp.alternate.padEnd(4)}`, + `${fp.equipment.padEnd(10)}`, + '', + `${fp.route}`, + '', + `RMK/${fp.remarks || ''}` + ]; + // Add the formatted header similar to ERAM + const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; return [header, '', ...lines].join('\n'); return [header, '', ...lines].join('\\n'); + } +} - } } - -}} private flightplanService: CustomFlightplanService; - constructor(flightplansMap: Map) { this.flightplanService = new CustomFlightplanService(flightplansMap); } @@ -338,12 +159,10 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand executeCommand(commandString: string): CommandResult { const parts = commandString.trim().split(/\s+/); const command = parts[0]?.toUpperCase(); - try { switch (command) { case 'FR': return this.handleFlightReadoutCommand(parts.slice(1)); - default: return { output: `Unknown command: ${command}. Only 'FR ' is supported.`, @@ -363,7 +182,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand */ private handleDebugCommand(): CommandResult { const allFlightplans = this.flightplanService.getAllFlightplans(); - if (allFlightplans.length === 0) { return { output: [ @@ -385,7 +203,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand success: true }; } - const aircraftIds = allFlightplans.map(fp => fp.aircraftId).slice(0, 10); const output = [ `DEBUG: Flightplan Service Status`, @@ -397,7 +214,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand '', 'Ready for FR commands with any of the above Aircraft IDs' ].join('\\n'); - return { output, success: true, @@ -416,41 +232,33 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand success: false }; } - const aircraftId = args[0].toUpperCase(); - // Debug: Check how many flightplans we have and list some IDs const allFlightplans = this.flightplanService.getAllFlightplans(); console.log(`FR Debug: Looking for ${aircraftId}, have ${allFlightplans.length} flightplans`); console.log('Available Aircraft IDs:', allFlightplans.slice(0, 5).map(fp => fp.aircraftId)); - const flightplan = this.flightplanService.getFlightplanById(aircraftId); - if (!flightplan) { // More helpful error message for debugging const similarIds = allFlightplans .filter(fp => fp.aircraftId.toUpperCase().includes(aircraftId)) .map(fp => fp.aircraftId) .slice(0, 3); - if (similarIds.length > 0) { return { output: `REJECT - FLID NOT STORED\\nSimilar IDs found: ${similarIds.join(', ')}\\nTotal flightplans: ${allFlightplans.length}`, success: false }; } - return { output: `REJECT - FLID NOT STORED\\nTotal flightplans: ${allFlightplans.length}`, success: false }; } - // Check for duplicate ACIDs (simplified - in real ERAM this would check CID combinations) const duplicates = this.flightplanService.getAllFlightplans().filter(fp => fp.aircraftId.toUpperCase() === aircraftId ); - if (duplicates.length > 1) { // Format duplicate list with CID, departure, and ETD const duplicateList = duplicates.map(fp => { @@ -458,16 +266,13 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand const etdFormatted = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format return `${fp.cid} ${fp.departure} ${etdFormatted}`; }).join('\\n'); - return { output: `REJECT - FLID DUPLICATION\\n\\n${duplicateList}`, success: false }; } - // Format the ERAM-style flight readout const readout = this.formatERAMFlightReadout(flightplan); - return { output: readout, success: true, @@ -483,23 +288,18 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand const etd = new Date(fp.estimatedDepartureTime * 1000); const etdDate = etd.toISOString().substring(5, 10).replace('-', ''); // MMDD format const etdTime = etd.toISOString().substring(11, 16).replace(':', ''); // HHMM format - // Format actual departure time if available let atd = ''; if (fp.actualDepartureTime > 0) { const atdDate = new Date(fp.actualDepartureTime * 1000); atd = atdDate.toISOString().substring(11, 16).replace(':', ''); // HHMM format } - // Format beacon code const beacon = fp.assignedBeaconCode ? fp.assignedBeaconCode.toString().padStart(4, '0') : ''; - // Format fuel time const fuelTime = `${fp.fuelHours.toString().padStart(2, '0')}${fp.fuelMinutes.toString().padStart(2, '0')}`; - // Format enroute time const enrouteTime = `${fp.hoursEnroute.toString().padStart(2, '0')}${fp.minutesEnroute.toString().padStart(2, '0')}`; - // Create the ERAM-style readout const lines = [ `${fp.aircraftId.padEnd(8)} ${fp.aircraftType.padEnd(4)} ${fp.departure.padEnd(4)} ${fp.destination.padEnd(4)} ${atd.padEnd(4)} ${fp.altitude.padEnd(6)} ${fp.speed.toString().padEnd(4)}`, @@ -511,24 +311,19 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand '', `RMK/${fp.remarks || ''}` ]; - // Add the formatted header similar to ERAM const header = `${fp.aircraftId} ${etdDate} ${etdTime} ${beacon} ${fuelTime} ${enrouteTime}`; - return [header, '', ...lines].join('\\n'); } } - const aircraftId = args[0].toUpperCase(); const flightplan = this.flightplanService.getFlightplanById(aircraftId); - if (!flightplan) { return { output: `No flightplan found for aircraft: ${aircraftId}`, success: false }; } - return { output: this.flightplanService.formatFlightplanForDisplay(flightplan), success: true, @@ -542,12 +337,10 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand */ private handleFlightplansCommand(args: string[]): CommandResult { const filter: FlightplanFilter = {}; - // Parse key=value arguments for (const arg of args) { const [key, value] = arg.split('='); if (!key || !value) continue; - switch (key.toLowerCase()) { case 'status': if (['Active', 'Proposed', 'Tentative'].includes(value)) { @@ -575,9 +368,7 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand break; } } - const result = this.flightplanService.searchFlightplans(filter); - if (result.flightplans.length === 0) { return { output: 'No flightplans match the specified criteria.', @@ -585,13 +376,11 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand data: result }; } - const output = [ `Found ${result.filteredCount} of ${result.totalCount} flightplans:`, '', this.flightplanService.formatFlightplansTable(result.flightplans) ].join('\\n'); - return { output, success: true, @@ -606,7 +395,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand private handleFlightplanListCommand(args: string[]): CommandResult { let flightplans: ApiFlightplan[]; let title = 'All Flightplans'; - if (args.length > 0) { const filter = args[0].toUpperCase(); switch (filter) { @@ -630,7 +418,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand } else { flightplans = this.flightplanService.getAllFlightplans(); } - if (flightplans.length === 0) { return { output: `No ${title.toLowerCase()} found.`, @@ -638,13 +425,11 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand data: flightplans }; } - const output = [ `${title} (${flightplans.length}):`, '', this.flightplanService.formatFlightplansTable(flightplans) ].join('\\n'); - return { output, success: true, @@ -664,37 +449,31 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand success: false }; } - const criteria = args[0].toUpperCase(); const value = args[1].toUpperCase(); let flightplans: ApiFlightplan[]; let description = ''; - switch (criteria) { case 'DEP': case 'DEPARTURE': flightplans = this.flightplanService.getFlightplansByDeparture(value); description = `departing from ${value}`; break; - case 'DEST': case 'DESTINATION': flightplans = this.flightplanService.getFlightplansByDestination(value); description = `arriving at ${value}`; break; - case 'WAYPOINT': case 'WPT': flightplans = this.flightplanService.getFlightplansByWaypoint(value); description = `routing via ${value}`; break; - case 'ALT': case 'ALTITUDE': flightplans = this.flightplanService.getFlightplansByAltitude(value); description = `at altitude ${value}`; break; - case 'TYPE': case 'AIRCRAFT': flightplans = this.flightplanService.getAllFlightplans().filter(fp => @@ -702,14 +481,12 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand ); description = `aircraft type containing ${value}`; break; - default: return { output: `Unknown search criteria: ${criteria}\\nValid criteria: DEP, DEST, WAYPOINT, ALT, TYPE`, success: false }; } - if (flightplans.length === 0) { return { output: `No flightplans found ${description}.`, @@ -717,13 +494,11 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand data: flightplans }; } - const output = [ `Found ${flightplans.length} flightplans ${description}:`, '', this.flightplanService.formatFlightplansTable(flightplans) ].join('\\n'); - return { output, success: true, @@ -736,19 +511,16 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand */ private handleFlightplanStatsCommand(): CommandResult { const stats = this.flightplanService.getFlightplanStatistics(); - const equipmentList = Array.from(stats.byEquipmentType.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5) // Top 5 .map(([type, count]) => ` ${type}: ${count}`) .join('\\n'); - const altitudeList = Array.from(stats.altitudeDistribution.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) // Top 10 .map(([alt, count]) => ` ${alt}: ${count}`) .join('\\n'); - const output = [ 'FLIGHTPLAN STATISTICS', '=====================', @@ -767,7 +539,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand 'Top Altitudes:', altitudeList || ' None' ].join('\\n'); - return { output, success: true, @@ -809,7 +580,6 @@ export class CustomFlightplanCommandParser {export class CustomFlightplanCommand ' FPF WAYPOINT HOFFA - Find flights routing via HOFFA', ' FPSTATS - Show statistics summary' ].join('\\n'); - return { output: helpText, success: true From bdb299670c25e6c43e48357792cc23f85dd78605 Mon Sep 17 00:00:00 2001 From: DivineWind04 Date: Wed, 11 Mar 2026 12:04:24 -0700 Subject: [PATCH 23/27] Remove unhelpful AI-generated comments across files Remove placeholder comments like 'Simple toast for now', 'Add properties as needed', 'For development, use alert', 'end blinking cursor section', and aspirational TODO comments that don't add value. Keep domain-specific comments that explain ERAM formatting and column positions since those provide genuine documentation value. Resolves jlefkoff review comment to remove bogus AI-inserted comments. --- src/App.tsx | 5 +---- src/contexts/HubContext.tsx | 1 - src/login/Login.tsx | 1 - src/redux/slices/authSlice.ts | 2 -- src/types/hold/holdAnnotations.ts | 1 - 5 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f0a2ec5..85d4910 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,9 +52,7 @@ const AppContent = () => { clearInterval(focusInterval); }; }, []); - // end blinking cursor/focus section - // handle ESC clear, need to define response areas handling, REMOVE placeholder text and replace with '' when ready to deploy. const [responseTop, setResponseTop] = useState(''); const [responseBottom, setResponseBottom] = useState(''); @@ -675,7 +673,7 @@ const AppContent = () => { if (giMatch && giMatch.length === 3) { const recipient = giMatch[1].toUpperCase(); const message = giMatch[2]; - // TODO: Implement GI message sending via your hub/socket + // TODO: Implement GI message sending return `ACCEPT GI TO ${recipient}\n${message}`; } else { return `REJECT FORMAT\n${input}`; @@ -920,7 +918,6 @@ const AppContent = () => { // Set new response to responseBottom setResponseBottom(result); } - // then dynamically add a scroll effect for each subsequent response's data to cycle it upwards. } catch (error) { const errorMsg = `REJECT ${typedCommand.toUpperCase()}\n\n${String(error).toUpperCase()}`; setLastFeedbackErrorMessage(errorMsg); diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index c9ace79..2736685 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -29,7 +29,6 @@ import { HubConnectionState } from "@microsoft/signalr"; import { invokeHub } from "../utils/hubUtils"; import { type ProcessEramMessageDto, type EramMessageProcessingResultDto, EramPositionType } from "../types/apiTypes/eramTypes"; -// Simple toast for now const toast = { error: (message: string) => console.error(message) }; diff --git a/src/login/Login.tsx b/src/login/Login.tsx index 82921f1..361ba14 100644 --- a/src/login/Login.tsx +++ b/src/login/Login.tsx @@ -16,7 +16,6 @@ import { useRootDispatch, useRootSelector } from "../redux/hooks"; import { DOMAIN, VATSIM_CLIENT_ID } from "../utils/constants"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; -// Simple styles for now const loginStyles = { bg: { position: 'fixed' as const, diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts index 342b5b2..ee58234 100644 --- a/src/redux/slices/authSlice.ts +++ b/src/redux/slices/authSlice.ts @@ -6,11 +6,9 @@ import { login as apiLogin, fetchVnasConfiguration } from "../../api/vNasDataApi import type { RootState } from "../store"; import * as jose from "jose"; -// Simple toast function for now - displays both console error and alert const toast = { error: (message: string, options?: any) => { console.error(message); - // For development, use alert. In production, you'd want to use a proper toast library if (typeof window !== 'undefined') { alert(`Error: ${message}`); } diff --git a/src/types/hold/holdAnnotations.ts b/src/types/hold/holdAnnotations.ts index 1b5ffa4..ea7bdd3 100644 --- a/src/types/hold/holdAnnotations.ts +++ b/src/types/hold/holdAnnotations.ts @@ -1,4 +1,3 @@ export interface HoldAnnotations { - // Add hold annotation properties as needed [key: string]: any; } From 33ff3d6bcdf70c11baef6cde7a4805792f1ca02f Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 11 Mar 2026 14:27:35 -0700 Subject: [PATCH 24/27] refactor: extract strip formatting into shared utility Extract duplicated strip formatting logic from App.tsx ReceiveStripItems handler and SR command into a shared formatStripFromFieldValues function in src/utils/stripFormatter.ts. --- src/types/apiTypes/eramTypes.ts | 23 -------------- src/utils/stripFormatter.ts | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 23 deletions(-) delete mode 100644 src/types/apiTypes/eramTypes.ts create mode 100644 src/utils/stripFormatter.ts diff --git a/src/types/apiTypes/eramTypes.ts b/src/types/apiTypes/eramTypes.ts deleted file mode 100644 index d281d7c..0000000 --- a/src/types/apiTypes/eramTypes.ts +++ /dev/null @@ -1,23 +0,0 @@ -export enum EramPositionType { - RSide, - DSide -} - -export interface EramMessageElement { - token?: string | null; - targetAircraftId?: string | null; - trackAircraftId?: string | null; -} - -export type ProcessEramMessageDto = { - source: EramPositionType | null; - elements: EramMessageElement[]; - invertNumericKeypad: boolean; -}; - -export interface EramMessageProcessingResultDto { - isSuccess: boolean; - autoRecall: boolean; - feedback: string[]; - response?: string; -} diff --git a/src/utils/stripFormatter.ts b/src/utils/stripFormatter.ts new file mode 100644 index 0000000..be62cb1 --- /dev/null +++ b/src/utils/stripFormatter.ts @@ -0,0 +1,54 @@ +/** + * Formats flight strip data from field values into an ERAM-style multi-line display string. + * Used by both the ReceiveStripItems hub handler and the SR (Strip Request) command. + */ +export function formatStripFromFieldValues(fieldValues: string[]): string { + // Fixed column positions based on ERAM reference (80 char width) + // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) + const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); + const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); + const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 7).padEnd(9); + + // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces + let route = (fieldValues[11] || ''); + route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); + let line2_route = ''; + + // Line 2: Revision Number (starts at position 3) + const line2 = ' ' + (fieldValues[1] || ''); + + // Line 3: Aircraft Type/Equipment (starts at column 1) + const line3_typeEquip = (fieldValues[3] || '').substring(0, 14).padEnd(14); + const line3_time = (fieldValues[6] || '').substring(0, 6).padEnd(6); + + // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) + const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); + const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); + let line4_remarks = (fieldValues[12] || '').substring(0, 40); + if (route.split('○').length > 1) { + line4_remarks = `○${route.split('○')[1]}`.substring(0, 40); + route = route.split('○')[0]; // Show only the part before the ○ in the route field + } + + let line1_route_display = route.substring(0, 40); + + if (route.length > 40) { + const first40 = route.slice(0, 40); + const lastSpace = first40.lastIndexOf(' '); + const splitIndex = lastSpace !== -1 ? lastSpace : 40; + + // Line 1 shows route up to the word boundary + line1_route_display = route.slice(0, splitIndex); + // Line 2 shows ONLY the continuation, padded so it aligns at column 41 + const secondLine = route.slice(splitIndex).trimStart(); + line2_route = secondLine; + } + + // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) + return ( + line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + + line2 + '\n' + + line3_typeEquip + line3_time + line2_route + '\n\n' + + line4_cid + line4_altitude + line4_remarks + ); +} From fb144152b6ef190138517e26b7bb13f9af0402ee Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 11 Mar 2026 14:28:03 -0700 Subject: [PATCH 25/27] refactor: extract parseCommand into commandParser service Move parseCommand function and all command handlers (FP, AM, GI, WR, SR, FR, RS) from App.tsx into src/services/commandParser.ts as individual functions. App.tsx now imports parseCommand and passes a CommandContext object with all required dependencies. Follows EDST frontend pattern of dedicated handler functions per command type. --- src/App.tsx | 1293 +++++++-------------------------- src/services/commandParser.ts | 624 ++++++++++++++++ 2 files changed, 882 insertions(+), 1035 deletions(-) create mode 100644 src/services/commandParser.ts diff --git a/src/App.tsx b/src/App.tsx index 85d4910..399cc6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,1036 +1,259 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { Provider } from 'react-redux'; -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { store } from './redux/store'; -import { HubContextProvider } from './contexts/HubContext'; -import LoginProvider from './login/Login'; -import { useRootDispatch, useRootSelector } from './redux/hooks'; -import { getVnasConfig, vatsimTokenSelector, sessionSelector, logoutThunk, hubConnectedSelector } from './redux/slices/authSlice'; -import { useHubConnector } from './hooks/useHubConnector'; -import Header from './components/Header'; -import InputArea from './components/InputArea'; -import Recat from './components/Recat'; -import './styles/terminal.css'; -import type { ApiFlightplan } from './types/apiTypes/apiFlightplan'; - -const AppContent = () => { - const dispatch = useRootDispatch(); - const vatsimToken = useRootSelector(vatsimTokenSelector); - const session = useRootSelector(sessionSelector); - - useEffect(() => { - dispatch(getVnasConfig()); - }, [dispatch]); - - const MainApp = () => { - const [command, setCommand] = useState(''); - const [lastFeedback, setLastFeedback] = useState(''); - const [lastFeedbackErrorMessage, setLastFeedbackErrorMessage] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - const { sendCommand, disconnectHub, deleteFlightplan, amendFlightplan, requestFlightStrip, flightplans, flightStrips, hubConnection } = useHubConnector(); - const hubConnected = useRootSelector(hubConnectedSelector); - - - // Blink cursor + maintain focus - const [cursorVisible, setCursorVisible] = useState(true); - useEffect(() => { - const blinkInterval = setInterval(() => { - setCursorVisible((prev) => !prev); - }, 300); - - const ensureFocus = () => { - if (document.activeElement !== terminalInputRef.current) { - terminalInputRef.current?.focus(); - } - }; - - const focusInterval = setInterval(ensureFocus, 500); - ensureFocus(); - - return () => { - clearInterval(blinkInterval); - clearInterval(focusInterval); - }; - }, []); - - const [responseTop, setResponseTop] = useState(''); - const [responseBottom, setResponseBottom] = useState(''); - - const handleEscapeClear = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - setTypedCommand(''); - setResponseTop(''); - setResponseBottom(''); - setLastFeedback(''); - } - }; - - useEffect(() => { - window.addEventListener('keydown', handleEscapeClear); - return () => window.removeEventListener('keydown', handleEscapeClear); - }, []); - - // Listen for ReceiveStripItems events and display formatted strips - useEffect(() => { - if (!hubConnection) return; - - const handleStripPrint = (topic: any, stripItems: any[]) => { - stripItems.forEach(strip => { - if (strip?.fieldValues) { - // Format using fieldValues from strip data based on ERAM strip layout - // fieldValues: [0:callsign, 1:rev, 2:?, 3:type/equip, 4:cid, 5:beacon, 6:proptime, 7:alt, 8:dep/arr, 9-10:?, 11:route, 12:remarks] - const fieldValues = strip.fieldValues; - - // Fixed column positions based on ERAM reference (80 char width) - // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) - const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); - const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); - const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 7).padEnd(9); - - // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces - let route = (fieldValues[11] || ''); - route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); - let line1_route = route.substring(0, 40); - let line2_route = ''; - - console.log('Route info:', { fullRoute: route, length: route.length, needsContinuation: route.length > 40 }); - - // Line 2: Revision Number (starts at position 3) - const line2 = ' ' + (fieldValues[1] || ''); - - // Line 3: Aircraft Type/Equipment (starts at column 1) - const line3_typeEquip = (fieldValues[3] || '').substring(0, 14).padEnd(14); - const line3_time = (fieldValues[6] || '').substring(0, 6).padEnd(6); - - // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) - const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); - const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); - let line4_remarks = (fieldValues[12] || '').substring(0, 40); - if (route.split('○').length > 1) { - line4_remarks = `○${route.split('○')[1]}`.substring(0, 40); - route = route.split('○')[0]; // Show only the part before the ○ in the route field - } - // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 - // But revision number ALSO appears on line 2 at column 3 - //let line2_full = line2; - let line1_route_display = line1_route; - - if (route.length > 40) { - const first40 = route.slice(0, 40); - const lastSpace = first40.lastIndexOf(' '); - - const splitIndex = lastSpace !== -1 ? lastSpace : 40; - - // Line 1 shows route up to the word boundary - line1_route_display = route.slice(0, splitIndex); - // Line 2 shows ONLY the continuation, padded so it aligns at column 41 - const secondLine = route.slice(splitIndex).trimStart(); - line2_route = secondLine; - } - - // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) - const formattedStrip = - line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + - line2 + '\n' + - line3_typeEquip + line3_time + line2_route + '\n\n' + - line4_cid + line4_altitude + line4_remarks; - - console.log('ReceiveStripItems - Final formatted strip:', formattedStrip); - - // Move current responseBottom to responseTop and set new strip to responseBottom - setResponseTop(responseBottom); - setResponseBottom(formattedStrip); - } - }); - }; - - hubConnection.on('ReceiveStripItems', handleStripPrint); - - return () => { - hubConnection.off('ReceiveStripItems', handleStripPrint); - }; - }, [hubConnection, responseBottom]); - - - const handleCommandSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (command.trim() && !isProcessing) { - setIsProcessing(true); - setLastFeedback(''); // Clear previous feedback - - try { - const result = await sendCommand(command.trim()); - setLastFeedback(`${command.toUpperCase()}\n\n${result.toUpperCase()}`); - setCommand(''); - } catch (error) { - console.error('Failed to send command:', error); - setLastFeedback(`REJECT ${command.toUpperCase()}\n\n${error}`.toUpperCase()); - } finally { - setIsProcessing(false); - } - } - }; - - const [typedCommand, setTypedCommand] = useState(''); - const terminalInputRef = React.useRef(null); - - useEffect(() => { - terminalInputRef.current?.focus(); - }, []); - - const parseCommand = async (input: string): Promise => { - const [command, ...args] = input.trim().split(/\s+/).map(s => s.toUpperCase()); - - // Helper function to find flightplan by callsign, CID, or beacon code - const findFlightplan = (identifier: string): ApiFlightplan | undefined => { - if (!flightplans) return undefined; - - // Try direct callsign match first - let fp = flightplans.get(identifier); - if (fp) return fp; - - // Search by CID or beacon code - for (const [, flightplan] of flightplans) { - if (flightplan.cid === identifier || - flightplan.assignedBeaconCode?.toString() === identifier) { - return flightplan; - } - } - - return undefined; - }; - - switch (command) { - case 'FP': { - // Flight Plan message - FP - // Format: FP ACID TYPE/EQUIP SPEED FIX TIME ALT ROUTE REMARKS - // Example: FP UAL423 B721/A 450 HAR P1720 170 HAR.V14.DCA O 1 VOR INOP - const fpMatch = /^FP\s+(.+)$/i.exec(input.trim()); - if (!fpMatch) { - return `REJECT 01 MSG INVALID\nMESSAGE TYPE`; - } - - const fields = fpMatch[1].split(/\s+/); - if (fields.length < 7) { - return `REJECT FORMAT - INSUFFICIENT FIELDS\n${input}`; - } - - // Field 02: Aircraft ID - const aircraftId = fields[0]; - if (aircraftId.length < 2 || aircraftId.length > 20) { - return `REJECT 02 AID FLID\nFORMAT`; - } - - // Field 03: Aircraft Type / Equipment Suffix - const typeEquipMatch = fields[1].match(/^([A-Z0-9]+)\/([A-Z])$/); - if (!typeEquipMatch) { - return `REJECT 03 TYP FORMAT`; - } - const aircraftType = typeEquipMatch[1]; - const equipmentSuffix = typeEquipMatch[2]; - - // Field 05: Speed - const speed = parseInt(fields[2]); - if (isNaN(speed) || speed <= 0) { - return `REJECT 05 SPD ILLEGAL`; - } - if (speed > 3700) { - return `REJECT 05 SPD FORMAT`; - } - - // Field 06: Departure Fix (Coordination Fix) - const departureFix = fields[3]; - if (departureFix.length < 2 || departureFix.length > 12) { - return `REJECT 06 FIX FORMAT`; - } - - // Field 07: Time - const timeStr = fields[4]; - let departureTime = 0; - if (timeStr !== 'E' && timeStr !== 'P' && timeStr !== 'D') { - // Parse time - should be 4 or 5 characters (HHMM or PXXDD format) - const timeMatch = timeStr.match(/^[PE]?(\d{4})$/); - if (!timeMatch) { - return `REJECT 07 TIM FORMAT`; - } - departureTime = parseInt(timeMatch[1]); - } - - // Field 08 or 09: Altitude (Assigned or Requested) - const altStr = fields[5]; - let altitude = ''; - if (altStr === 'OTP' || altStr === 'VFR') { - altitude = altStr; - } else { - // Parse altitude - format like 170 (FL170), OTP/115, VFR/75, etc. - const altMatch = altStr.match(/^(\d+|OTP|VFR)(\/(\d+))?$/); - if (!altMatch) { - return `REJECT 08 ALT FORMAT`; - } - altitude = altStr; - } - - // Field 10: Route - everything from field 6 onwards until we hit remarks - // Route ends when we see 'O' or '@' prefix for remarks - let routeEndIdx = 6; - for (let i = 6; i < fields.length; i++) { - if (fields[i].startsWith('O') || fields[i].startsWith('@')) { - routeEndIdx = i; - break; - } - routeEndIdx = i + 1; - } - - const routeParts = fields.slice(6, routeEndIdx); - if (routeParts.length === 0) { - return `REJECT 10 RTE FORMAT`; - } - const route = routeParts.join(' '); - - // Field 11: Remarks (optional) - let remarks = ''; - if (routeEndIdx < fields.length) { - const remarksFields = fields.slice(routeEndIdx); - // Check if remarks start with O or @ - const remarksStr = remarksFields.join(' '); - if (remarksStr.startsWith('O ')) { - remarks = remarksStr.substring(2); // Interfacility remarks - } else if (remarksStr.startsWith('@')) { - remarks = remarksStr.substring(1); // Intrafacility remarks - } else { - remarks = remarksStr; - } - } - - try { - // Check for duplicate active flight plan - const existingFp = flightplans.get(aircraftId); - if (existingFp && existingFp.status === 'Active') { - return `REJECT 02 AID FLID\nDUPLICATION`; - } - - await amendFlightplan({ - aircraftId, - cid: '', - status: 'Proposed', - aircraftType: aircraftType, - faaEquipmentSuffix: equipmentSuffix, - equipment: `${aircraftType}/${equipmentSuffix}`, - icaoEquipmentCodes: '', - icaoSurveillanceCodes: '', - speed, - altitude, - departure: departureFix, - destination: '', - alternate: '', - route, - remarks, - assignedBeaconCode: null, - estimatedDepartureTime: departureTime, - actualDepartureTime: 0, - hoursEnroute: 0, - minutesEnroute: 0, - fuelHours: 0, - fuelMinutes: 0, - pilotCid: '', - holdAnnotations: null, - wakeTurbulenceCode: '', - }); - - return `ACCEPT\n${aircraftId}`; - } catch (error) { - console.error('Failed to create flightplan:', error); - const errorStr = String(error); - if (errorStr.includes('Not your control') || errorStr.includes('inactive session')) { - return `REJECT 01 MSG ILLEGAL\nSOURCE`; - } else { - return `REJECT FP ENTRY FAILED`; - } - } - } - - case 'AM': { - // Amendment Message - AM [ ...] - // Format: AM ACID 06 FIX 10 ROUTE or AM ACID SPD 225 RAL 90 - // Field references: AID, TYP, BCN, SPD, FIX, TIM, ALT, RAL, RTE, RMK - const amMatch = /^AM\s+(.+)$/i.exec(input.trim()); - if (!amMatch) { - return `REJECT 01 MSG INVALID\nMESSAGE TYPE`; - } - - const parts = amMatch[1].split(/\s+/); - if (parts.length < 3) { - return `REJECT FORMAT - INSUFFICIENT FIELDS\n${input}`; - } - - const aircraftId = parts[0]; - - // Find the existing flightplan - const existingFp = findFlightplan(aircraftId); - if (!existingFp) { - return `REJECT 02 FLID NOT\nSTORED`; - } - - // Parse field references and amendments - const amendments: { [key: string]: any } = {}; - let i = 1; - let amendingAircraftId = false; - - while (i < parts.length) { - const fieldRef = parts[i].toUpperCase(); - - // Map field references to field numbers - const fieldMap: { [key: string]: string } = { - 'AID': '02', '02': '02', '2': '02', - 'TYP': '03', '03': '03', '3': '03', - 'BCN': '04', '04': '04', '4': '04', - 'SPD': '05', '05': '05', '5': '05', - 'FIX': '06', '06': '06', '6': '06', - 'TIM': '07', '07': '07', '7': '07', - 'ALT': '08', '08': '08', '8': '08', - 'RAL': '09', '09': '09', '9': '09', - 'RTE': '10', '10': '10', - 'RMK': '11', '11': '11' - }; - - const fieldNum = fieldMap[fieldRef]; - if (!fieldNum) { - return `REJECT INVALID FIELD\nREFERENCE`; - } - - i++; - if (i >= parts.length) { - return `REJECT FORMAT - MISSING AMENDMENT DATA\n${input}`; - } - - // Check if amending Field 02 (Aircraft ID) - if (fieldNum === '02') { - if (Object.keys(amendments).length > 0) { - return `REJECT - INVALID\nAMENDMENT`; - } - amendingAircraftId = true; - amendments['aircraftId'] = parts[i]; - i++; - break; // Only Field 02 can be amended when changing aircraft ID - } - - // Collect amendment data for this field - let amendmentData: string[] = []; - - // For route (10/RTE), collect all remaining parts until we hit another field ref or end - if (fieldNum === '10') { - while (i < parts.length) { - const nextToken = parts[i].toUpperCase(); - if (fieldMap[nextToken]) { - break; // Hit another field reference - } - amendmentData.push(parts[i]); - i++; - } - - if (amendmentData.length === 0) { - return `REJECT 10 RTE FORMAT`; - } - - // Process route amendment based on ERAM rules - const routeStr = amendmentData.join(' '); - const existingRoute = existingFp.route || ''; - const routeElements = routeStr.split(/[\s.]+/).filter(e => e.length > 0); - - // Check for departure fix change (single element followed by ↑) - if (routeStr.endsWith('↑') || routeStr.endsWith('^')) { - const newDepFix = routeStr.slice(0, -1).trim().split(/[\s.]+/)[0]; - amendments['departure'] = newDepFix; - amendments['route'] = routeStr.slice(0, -1).trim(); - } - // Check for complete replacement (ends with ↓) - else if (routeStr.endsWith('↓') || routeStr.endsWith('v')) { - const newRoute = routeStr.slice(0, -1).trim(); - const newRouteElements = newRoute.split(/[\s.]+/).filter(e => e.length > 0); - if (newRouteElements.length > 0) { - // Last element becomes destination - amendments['destination'] = newRouteElements[newRouteElements.length - 1]; - // For active flights, departure fix retained with tailoring symbol (/) - if (existingFp.status === 'Active' && existingFp.departure) { - amendments['route'] = `${existingFp.departure}/.${newRoute}`; - } else { - amendments['route'] = newRoute; - } - } else { - return `REJECT 10 RTE FORMAT`; - } - } - // Tailoring symbol at beginning (/) - insert after departure fix - else if (routeStr.startsWith('/')) { - const tailoredRoute = routeStr.substring(1).trim(); - if (existingFp.departure) { - amendments['route'] = `${existingFp.departure}/.${tailoredRoute}`; - } else { - amendments['route'] = tailoredRoute; - } - } - // Merge with existing route - match first or last unambiguous element - else { - const firstElement = routeElements[0]; - const lastElement = routeElements[routeElements.length - 1]; - const existingElements = existingRoute.split(/[\s.]+/).filter(e => e.length > 0); - - // Try to find first element match - const firstMatchIdx = existingElements.indexOf(firstElement); - const lastMatchIdx = existingElements.lastIndexOf(lastElement); - - // Check if BOTH first and last match (replace between) - if (firstMatchIdx !== -1 && lastMatchIdx !== -1 && firstMatchIdx < lastMatchIdx) { - const before = existingElements.slice(0, firstMatchIdx).join('.'); - const after = existingElements.slice(lastMatchIdx + 1).join('.'); - const merged = [before, routeStr, after].filter(p => p.length > 0).join('.'); - amendments['route'] = merged; - } - // Only first element matches (replace after) - else if (firstMatchIdx !== -1) { - const before = existingElements.slice(0, firstMatchIdx + 1).join('.'); - amendments['route'] = `${before}.${routeElements.slice(1).join('.')}`; - } - // Only last element matches (replace before) - else if (lastMatchIdx !== -1) { - const after = existingElements.slice(lastMatchIdx).join('.'); - // For active flight, add tailoring symbol - if (existingFp.status === 'Active' && existingFp.departure) { - amendments['route'] = `${existingFp.departure}/.${routeElements.slice(0, -1).join('.')}.${after}`; - } else { - amendments['route'] = `${routeElements.slice(0, -1).join('.')}.${after}`; - } - } - // No match - just use new route - else { - amendments['route'] = routeStr; - } - } - - // Check if Field 06 also needs amendment (required for Field 10 amendments per ERAM rules) - // This will be validated when Field 06 is also in the amendment - } - // For other fields, just take the next token - else { - amendmentData.push(parts[i]); - i++; - - const value = amendmentData[0]; - - switch (fieldNum) { - case '03': // Type/Equipment - const typeMatch = value.match(/^([A-Z0-9]+)\/([A-Z])$/); - if (!typeMatch) { - return `REJECT 03 TYP FORMAT`; - } - const newAircraftType = typeMatch[1]; - const newFaaEquipmentSuffix = typeMatch[2]; - - // Update equipment field by replacing only the aircraft type (before first /) - // and preserving everything after the first / - // Format: "B738/M-VGDW/C" -> "B752/M-VGDW/C" - let newEquipment = `${newAircraftType}/${newFaaEquipmentSuffix}`; - if (existingFp.equipment) { - const firstSlashIndex = existingFp.equipment.indexOf('/'); - if (firstSlashIndex > 0) { - // Preserve everything after the first / - const everythingAfterSlash = existingFp.equipment.substring(firstSlashIndex + 1); - newEquipment = `${newAircraftType}/${everythingAfterSlash}`; - } - } - - amendments['equipment'] = newEquipment; - amendments['faaEquipmentSuffix'] = newFaaEquipmentSuffix; - break; - - case '04': // Beacon Code - const beaconCode = parseInt(value); - if (isNaN(beaconCode) || beaconCode < 0 || beaconCode > 7777) { - return `REJECT 04 BCN CODE FORMAT`; - } - amendments['assignedBeaconCode'] = beaconCode; - break; - - case '05': // Speed - const speed = parseInt(value); - if (isNaN(speed) || speed <= 0) { - return `REJECT 05 SPD ILLEGAL`; - } - if (speed > 3700) { - return `REJECT 05 SPD FORMAT`; - } - amendments['speed'] = speed; - break; - - case '06': // Departure Fix - if (value.length < 2 || value.length > 12) { - return `REJECT 06 FIX FORMAT`; - } - amendments['departure'] = value; - break; - - case '07': // Time - if (value !== 'E' && value !== 'P' && value !== 'D') { - const timeMatch = value.match(/^[PE]?(\d{4})$/); - if (!timeMatch) { - return `REJECT 07 TIM FORMAT`; - } - amendments['estimatedDepartureTime'] = parseInt(timeMatch[1]); - } - break; - - case '08': // Assigned Altitude - amendments['altitude'] = value; - break; - - case '09': // Requested Altitude (RAL) - // Store as altitude for now - amendments['altitude'] = value; - break; - - case '11': // Remarks - // Collect all remaining parts as remarks - while (i < parts.length) { - const nextToken = parts[i].toUpperCase(); - if (fieldMap[nextToken]) { - break; - } - amendmentData.push(parts[i]); - i++; - } - let remarks = amendmentData.join(' '); - // Remove O or @ prefix if present - if (remarks.startsWith('O ')) { - remarks = remarks.substring(2); - } else if (remarks.startsWith('@')) { - remarks = remarks.substring(1); - } - amendments['remarks'] = remarks; - break; - } - } - } - - if (Object.keys(amendments).length === 0) { - return `REJECT FORMAT - NO VALID AMENDMENTS\n${input}`; - } - - try { - // Build the amendment DTO, preserving existing values - const amendDto: any = { - aircraftId: amendments['aircraftId'] || existingFp.aircraftId, - cid: existingFp.cid, - status: existingFp.status, - aircraftType: amendments['aircraftType'] || existingFp.aircraftType, - faaEquipmentSuffix: amendments['faaEquipmentSuffix'] || existingFp.faaEquipmentSuffix, - equipment: amendments['equipment'] || existingFp.equipment, - icaoEquipmentCodes: existingFp.icaoEquipmentCodes, - icaoSurveillanceCodes: existingFp.icaoSurveillanceCodes, - speed: amendments['speed'] ?? existingFp.speed, - altitude: amendments['altitude'] || existingFp.altitude, - departure: amendments['departure'] || existingFp.departure, - destination: amendments['destination'] || existingFp.destination, - alternate: existingFp.alternate, - route: amendments['route'] || existingFp.route, - remarks: amendments['remarks'] !== undefined ? amendments['remarks'] : existingFp.remarks, - assignedBeaconCode: amendments['assignedBeaconCode'] ?? existingFp.assignedBeaconCode, - estimatedDepartureTime: amendments['estimatedDepartureTime'] ?? existingFp.estimatedDepartureTime, - actualDepartureTime: existingFp.actualDepartureTime, - hoursEnroute: existingFp.hoursEnroute, - minutesEnroute: existingFp.minutesEnroute, - fuelHours: existingFp.fuelHours, - fuelMinutes: existingFp.fuelMinutes, - pilotCid: existingFp.pilotCid, - holdAnnotations: existingFp.holdAnnotations, - wakeTurbulenceCode: existingFp.wakeTurbulenceCode, - }; - - console.log('AM Command Debug:'); - console.log(' Amendments:', amendments); - console.log(' Existing FP equipment:', existingFp.equipment); - console.log(' Existing FP faaEquipmentSuffix:', existingFp.faaEquipmentSuffix); - console.log(' New equipment:', amendDto.equipment); - console.log(' New faaEquipmentSuffix:', amendDto.faaEquipmentSuffix); - - await amendFlightplan(amendDto); - - return `ACCEPT ${amendDto.aircraftId}/${amendDto.cid}`; - } catch (error) { - console.error('Failed to amend flightplan:', error); - const errorStr = String(error); - if (errorStr.includes('Not your control') || errorStr.includes('inactive session')) { - return `REJECT 01 MSG ILLEGAL\nSOURCE`; - } else { - return `REJECT - INVALID\nAMENDMENT`; - } - } - } - - case 'GI': { - // General Information message - GI - const giMatch = /^GI\s+(\S+)\s+(.+)$/i.exec(input.trim()); - if (giMatch && giMatch.length === 3) { - const recipient = giMatch[1].toUpperCase(); - const message = giMatch[2]; - // TODO: Implement GI message sending - return `ACCEPT GI TO ${recipient}\n${message}`; - } else { - return `REJECT FORMAT\n${input}`; - } - } - - case 'WR': { - // Weather Request - WR - if (args.length !== 1) { - return `REJECT FORMAT\n${input}`; - } - const station = args[0]; - - try { - // Fetch METAR from VATSIM metar API (same as EDST) - const response = await fetch( - `https://metar.vatsim.net/${station}` - ); - - if (!response.ok) { - return `REJECT WEATHER STAT REQ\nSTATION NOT FOUND`; - } - - const metar = await response.text(); - - if (!metar || metar.trim() === '' || metar.includes('No METAR')) { - return `REJECT WEATHER STAT REQ\nNO DATA FOR ${station}`; - } - - return `ACCEPT WEATHER STAT REQ\n${metar.trim()}`; - } catch (error) { - console.error('Failed to fetch METAR:', error); - return `REJECT WEATHER STAT REQ\nFETCH FAILED`; - } - } - - case 'SR': { - // Strip Request - SR - if (args.length !== 1) { - return `REJECT FORMAT\n${input}`; - } - const identifier = args[0]; - - // Find the aircraft (by callsign, CID, or beacon) - let aircraftId = identifier; - let strip = flightStrips?.get(identifier); - - if (!strip && flightStrips) { - // Search by CID or beacon code - for (const [id, s] of flightStrips) { - if (s.fieldValues && ( - s.fieldValues[0] === identifier || - s.fieldValues[4] === identifier || - s.fieldValues[5] === identifier)) { - strip = s; - aircraftId = id; - break; - } - } - } - - // Always request from server first - this triggers ReceiveStripItems event - try { - console.log('SR: Requesting flight strip for:', aircraftId); - await requestFlightStrip(aircraftId); - console.log('SR: RequestFlightStrip succeeded for:', aircraftId); - } catch (error) { - console.warn('SR: RequestFlightStrip failed:', error); - // If server request fails and we don't have a local copy, reject - if (!strip?.fieldValues) { - return `REJECT\nSTRIP NOT FOUND\n${input}`; - } - } - - // If we have a local copy, display it immediately (server response will update via ReceiveStripItems) - if (strip?.fieldValues) { - // Format using fieldValues from strip data based on ERAM strip layout - // fieldValues: [0:callsign, 1:rev, 2:?, 3:type/equip, 4:cid, 5:beacon, 6:proptime, 7:alt, 8:dep/arr, 9-10:?, 11:route, 12:remarks] - const fieldValues = strip.fieldValues; - - // Fixed column positions based on ERAM reference (80 char width) - // Line 1: Aircraft ID (1-17), Beacon (19-23), Departure Point (25-36), Route (41-80) - const line1_aircraftId = (fieldValues[0] || '').substring(0, 7).padEnd(14); - const line1_beacon = (fieldValues[5] || '').substring(0, 4).padEnd(6); - const line1_depPoint = (fieldValues[8]?.split(' ')[0] || '').substring(0, 7).padEnd(9); - - // Remove embedded newlines from route (both literal \n and escaped \\n) and replace with spaces - let route = (fieldValues[11] || ''); - route = route.replace(/\\n/g, ' ').replace(/\n/g, ' '); - let line1_route = route.substring(0, 40); - let line2_route = ''; - - console.log('Route info:', { fullRoute: route, length: route.length, needsContinuation: route.length > 40 }); - - // Line 2: Revision Number (starts at position 3) - const line2 = ' ' + (fieldValues[1] || ''); - - // Line 3: Aircraft Type/Equipment (starts at column 1) - const line3_typeEquip = (fieldValues[3] || '').substring(0, 14).padEnd(14); - const line3_time = (fieldValues[6] || '').substring(0, 6).padEnd(6); - - // Line 4: CID (1-17), Altitude (19-23), Remarks (41-80) - const line4_cid = (fieldValues[4] || '').substring(0, 4).padEnd(14); - const line4_altitude = (fieldValues[7] || '').substring(0, 4).padEnd(15); - let line4_remarks = (fieldValues[12] || '').substring(0, 40); - if (route.split('○').length > 1) { - line4_remarks = `○${route.split('○')[1]}`.substring(0, 40); - route = route.split('○')[0]; // Show only the part before the ○ in the route field - } - // Route continuation - if route > 40 chars, continuation appears at column 41 on line 2 - // But revision number ALSO appears on line 2 at column 3 - //let line2_full = line2; - let line1_route_display = line1_route; - - if (route.length > 40) { - const first40 = route.slice(0, 40); - const lastSpace = first40.lastIndexOf(' '); - - const splitIndex = lastSpace !== -1 ? lastSpace : 40; - - // Line 1 shows route up to the word boundary - line1_route_display = route.slice(0, splitIndex); - // Line 2 shows ONLY the continuation, padded so it aligns at column 41 - const secondLine = route.slice(splitIndex).trimStart(); - line2_route = secondLine; - } - - // Build strip: Line1 + Line2(revision + route cont) + Line3(type/time) + Line4(cid/alt/remarks) - const formattedStrip = - line1_aircraftId + line1_beacon + line1_depPoint + line1_route_display + '\n' + - line2 + '\n' + - line3_typeEquip + line3_time + line2_route + '\n\n' + - line4_cid + line4_altitude + line4_remarks; - - // Print the strip (move responseBottom to responseTop, set new strip to responseBottom) - setResponseTop(responseBottom); - setResponseBottom(formattedStrip); - - // Return the formatted strip data - return formattedStrip; - } - - // No local strip but server request succeeded - strip will arrive via ReceiveStripItems event - return `ACCEPT SR ${identifier}\nSTRIP REQUESTED`; - } - - case 'FR': { - // Flight Readout - FR - if (args.length !== 1) { - return `REJECT FORMAT\n${input}`; - } - const identifier = args[0]; - const flightplan = findFlightplan(identifier); - - if (flightplan) { - // Format using ApiFlightplan data - // aircraftID aircraftType assignedBeaconCode speed altitude departure route destination remarks - const cid = flightplan.cid || ''; - const aircraftId = flightplan.aircraftId || ''; - const aircraftType = flightplan.aircraftType || ''; - const beaconCode = flightplan.assignedBeaconCode?.toString() || ''; - const speed = flightplan.speed || ''; - const time = ('P' + flightplan.estimatedDepartureTime) || ''; - const altitude = flightplan.altitude || ''; - const departure = flightplan.departure || ''; - const destination = flightplan.destination || ''; - const remarks = flightplan.remarks || ''; - - // Route - break into 80 char chunks - const route = flightplan.route || ''; - const maxLineLength = 80; - const routeLines: string[] = []; - for (let i = 0; i < route.length; i += maxLineLength) { - routeLines.push(route.substring(i, i + maxLineLength)); - } - - return `${cid} ${aircraftId} ${aircraftType} ${beaconCode} ${speed} ${time} ${altitude} ${departure} ${route} ${destination} ${remarks}`; - - } else { - return `FLID NOT STORED\n${input}`; - } - } - - case 'RS': { - // Remove Strips - RS - if (args.length !== 1) { - return `REJECT FORMAT\n${input}`; - } - const identifier = args[0]; - const flightplan = findFlightplan(identifier); - if (flightplan) { - try { - await deleteFlightplan(flightplan.aircraftId); - return `${flightplan.aircraftId} ${flightplan.cid}REMOVE \nSTRIPS`; - } catch (error) { - console.error('Failed to delete flightplan:', error); - // Parse the error message to extract relevant info - const errorStr = String(error); - if (errorStr.includes('Not your control')) { - return `REJECT NOT YOUR CONTROL\n${flightplan.aircraftId}`; - } else { - return `REJECT DELETE FAILED\n${flightplan.aircraftId}`; - } - } - } else { - return `REJECT FLID NOT STORED\n${input}`; - } - } - - default: - // Send to ERAM hub for all other commands - return await sendCommand(input); - } - }; - - const handleKeyDown = async (e: React.KeyboardEvent) => { - if (isProcessing) return; - - if (e.key === 'Enter') { - e.preventDefault(); - const command = typedCommand.trim(); - if (!command) return; - - setIsProcessing(true); - setLastFeedback(''); - - try { - const result = await parseCommand(command); - console.log(` Command: ${command}, Result:`, result); - - // Check if result is a REJECT - if so, show ONLY in error area - if (result.toUpperCase().startsWith('REJECT')) { - setLastFeedbackErrorMessage(result); - // Don't update responseBottom or responseTop for errors - } else { - // Success - clear errors and update response areas - setLastFeedbackErrorMessage(''); - - // Move current responseBottom to responseTop - setResponseTop(responseBottom); - - // Set new response to responseBottom - setResponseBottom(result); - } - } catch (error) { - const errorMsg = `REJECT ${typedCommand.toUpperCase()}\n\n${String(error).toUpperCase()}`; - setLastFeedbackErrorMessage(errorMsg); - // Don't update responseBottom or responseTop for errors - } finally { - setTypedCommand(''); - setIsProcessing(false); - } - } else if (e.key === 'Backspace') { - e.preventDefault(); - setTypedCommand((prev) => prev.slice(0, -1)); - } else if (e.key.length === 1) { - e.preventDefault(); - setTypedCommand((prev) => prev + e.key.toUpperCase()); - } - }; - - return ( -
- {/* Terminal Header */} -
- - {/* Terminal Body */} -
- {/* Response Section (top half) */} - {/* FDIO max character width is 80 */} -
-
- {responseTop && '================================================================================\n'}{responseTop} -
-
- {responseBottom && '================================================================================\n'}{responseBottom} -
-
- {/* Command Section (Bottom Half) */} -
- -------------------------------------------------------------------------------- - {isProcessing && ( -
M E S S A G E W A I T I N G . . .
- )} - {lastFeedbackErrorMessage && ( -
  {lastFeedbackErrorMessage.toUpperCase()}
- )} -
- - {/* TO DO: BLINKING CURSOR BOX AND FORCED FOCUS */} -
-
- {typedCommand} - -
- - {isProcessing && ( -
- -
- )} -
- - {/* Terminal Footer */} -
-
-
- {/* VNAS HUB: {hubConnected ? 'CONNECTED' : 'DISCONNECTED'} */} -
- -
- {/* ARTCC: {session?.artccId?.toUpperCase() || 'N/A'} | STATUS: {session?.isActive ? 'ACTIVE' : 'INACTIVE'} */} -
- -
-
-
- ); - }; - - return ( - - - } - /> - - - - ) : ( - - ) - } - /> - - - ); -}; - -export default function App() { - return ( - - - - ); +import React, { useEffect, useState, useRef } from 'react'; +import { Provider } from 'react-redux'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { store } from './redux/store'; +import { HubContextProvider } from './contexts/HubContext'; +import LoginProvider from './login/Login'; +import { useRootDispatch, useRootSelector } from './redux/hooks'; +import { getVnasConfig, vatsimTokenSelector, sessionSelector, logoutThunk, hubConnectedSelector } from './redux/slices/authSlice'; +import { useHubConnector } from './hooks/useHubConnector'; +import Header from './components/Header'; +import InputArea from './components/InputArea'; +import Recat from './components/Recat'; +import './styles/terminal.css'; +import { parseCommand } from './services/commandParser'; +import { formatStripFromFieldValues } from './utils/stripFormatter'; + +const AppContent = () => { + const dispatch = useRootDispatch(); + const vatsimToken = useRootSelector(vatsimTokenSelector); + const session = useRootSelector(sessionSelector); + + useEffect(() => { + dispatch(getVnasConfig()); + }, [dispatch]); + + const MainApp = () => { + const [lastFeedbackErrorMessage, setLastFeedbackErrorMessage] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const { sendCommand, disconnectHub, deleteFlightplan, amendFlightplan, requestFlightStrip, flightplans, flightStrips, hubConnection } = useHubConnector(); + const hubConnected = useRootSelector(hubConnectedSelector); + + + // Blink cursor + maintain focus + const [cursorVisible, setCursorVisible] = useState(true); + useEffect(() => { + const blinkInterval = setInterval(() => { + setCursorVisible((prev) => !prev); + }, 300); + + const ensureFocus = () => { + if (document.activeElement !== terminalInputRef.current) { + terminalInputRef.current?.focus(); + } + }; + + const focusInterval = setInterval(ensureFocus, 500); + ensureFocus(); + + return () => { + clearInterval(blinkInterval); + clearInterval(focusInterval); + }; + }, []); + + const [responseTop, setResponseTop] = useState(''); + const [responseBottom, setResponseBottom] = useState(''); + + const handleEscapeClear = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + setTypedCommand(''); + setResponseTop(''); + setResponseBottom(''); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleEscapeClear); + return () => window.removeEventListener('keydown', handleEscapeClear); + }, []); + + // Listen for ReceiveStripItems events and display formatted strips + useEffect(() => { + if (!hubConnection) return; + + const handleStripPrint = (topic: any, stripItems: any[]) => { + stripItems.forEach(strip => { + if (strip?.fieldValues) { + const formattedStrip = formatStripFromFieldValues(strip.fieldValues); + console.log('ReceiveStripItems - Final formatted strip:', formattedStrip); + + // Move current responseBottom to responseTop and set new strip to responseBottom + setResponseTop(responseBottom); + setResponseBottom(formattedStrip); + } + }); + }; + + hubConnection.on('ReceiveStripItems', handleStripPrint); + + return () => { + hubConnection.off('ReceiveStripItems', handleStripPrint); + }; + }, [hubConnection, responseBottom]); + + + const [typedCommand, setTypedCommand] = useState(''); + const terminalInputRef = React.useRef(null); + + useEffect(() => { + terminalInputRef.current?.focus(); + }, []); + + const commandContext = { + flightplans, + flightStrips, + amendFlightplan, + deleteFlightplan, + requestFlightStrip, + sendCommand, + responseBottom, + setResponseTop, + setResponseBottom, + }; + + const handleKeyDown = async (e: React.KeyboardEvent) => { + if (isProcessing) return; + + if (e.key === 'Enter') { + e.preventDefault(); + const command = typedCommand.trim(); + if (!command) return; + + setIsProcessing(true); + + try { + const result = await parseCommand(command, commandContext); + console.log(` Command: ${command}, Result:`, result); + + // Check if result is a REJECT - if so, show ONLY in error area + if (result.toUpperCase().startsWith('REJECT')) { + setLastFeedbackErrorMessage(result); + // Don't update responseBottom or responseTop for errors + } else { + // Success - clear errors and update response areas + setLastFeedbackErrorMessage(''); + + // Move current responseBottom to responseTop + setResponseTop(responseBottom); + + // Set new response to responseBottom + setResponseBottom(result); + } + } catch (error) { + const errorMsg = `REJECT ${typedCommand.toUpperCase()}\n\n${String(error).toUpperCase()}`; + setLastFeedbackErrorMessage(errorMsg); + // Don't update responseBottom or responseTop for errors + } finally { + setTypedCommand(''); + setIsProcessing(false); + } + } else if (e.key === 'Backspace') { + e.preventDefault(); + setTypedCommand((prev) => prev.slice(0, -1)); + } else if (e.key.length === 1) { + e.preventDefault(); + setTypedCommand((prev) => prev + e.key.toUpperCase()); + } + }; + + return ( +
+ {/* Terminal Header */} +
+ + {/* Terminal Body */} +
+ {/* Response Section (top half) */} + {/* FDIO max character width is 80 */} +
+
+ {responseTop && '================================================================================\n'}{responseTop} +
+
+ {responseBottom && '================================================================================\n'}{responseBottom} +
+
+ {/* Command Section (Bottom Half) */} +
+ -------------------------------------------------------------------------------- + {isProcessing && ( +
M E S S A G E W A I T I N G . . .
+ )} + {lastFeedbackErrorMessage && ( +
  {lastFeedbackErrorMessage.toUpperCase()}
+ )} +
+ + {/* TO DO: BLINKING CURSOR BOX AND FORCED FOCUS */} +
+
+ {typedCommand} + +
+ + {isProcessing && ( +
+ +
+ )} +
+ + {/* Terminal Footer */} +
+
+
+ {/* VNAS HUB: {hubConnected ? 'CONNECTED' : 'DISCONNECTED'} */} +
+ +
+ {/* ARTCC: {session?.artccId?.toUpperCase() || 'N/A'} | STATUS: {session?.isActive ? 'ACTIVE' : 'INACTIVE'} */} +
+ +
+
+
+ ); + }; + + return ( + + + } + /> + + + + ) : ( + + ) + } + /> + + + ); +}; + +export default function App() { + return ( + + + + ); } \ No newline at end of file diff --git a/src/services/commandParser.ts b/src/services/commandParser.ts new file mode 100644 index 0000000..4828d0d --- /dev/null +++ b/src/services/commandParser.ts @@ -0,0 +1,624 @@ +import type { ApiFlightplan, CreateOrAmendFlightplanDto } from '../types/apiTypes/apiFlightplan'; +import { formatStripFromFieldValues } from '../utils/stripFormatter'; + +export interface CommandContext { + flightplans: Map; + flightStrips: Map; + amendFlightplan: (fp: CreateOrAmendFlightplanDto) => Promise; + deleteFlightplan: (aircraftId: string) => Promise; + requestFlightStrip: (aircraftId: string) => Promise; + sendCommand: (command: string) => Promise; + responseBottom: string; + setResponseTop: (value: string) => void; + setResponseBottom: (value: string) => void; +} + +function findFlightplan(identifier: string, flightplans: Map): ApiFlightplan | undefined { + // Try direct callsign match first + let fp = flightplans.get(identifier); + if (fp) return fp; + + // Search by CID or beacon code + for (const [, flightplan] of flightplans) { + if (flightplan.cid === identifier || + flightplan.assignedBeaconCode?.toString() === identifier) { + return flightplan; + } + } + + return undefined; +} + +async function handleFP(input: string, ctx: CommandContext): Promise { + const fpMatch = /^FP\s+(.+)$/i.exec(input.trim()); + if (!fpMatch) { + return `REJECT 01 MSG INVALID\nMESSAGE TYPE`; + } + + const fields = fpMatch[1].split(/\s+/); + if (fields.length < 7) { + return `REJECT FORMAT - INSUFFICIENT FIELDS\n${input}`; + } + + // Field 02: Aircraft ID + const aircraftId = fields[0]; + if (aircraftId.length < 2 || aircraftId.length > 20) { + return `REJECT 02 AID FLID\nFORMAT`; + } + + // Field 03: Aircraft Type / Equipment Suffix + const typeEquipMatch = fields[1].match(/^([A-Z0-9]+)\/([A-Z])$/); + if (!typeEquipMatch) { + return `REJECT 03 TYP FORMAT`; + } + const aircraftType = typeEquipMatch[1]; + const equipmentSuffix = typeEquipMatch[2]; + + // Field 05: Speed + const speed = parseInt(fields[2]); + if (isNaN(speed) || speed <= 0) { + return `REJECT 05 SPD ILLEGAL`; + } + if (speed > 3700) { + return `REJECT 05 SPD FORMAT`; + } + + // Field 06: Departure Fix (Coordination Fix) + const departureFix = fields[3]; + if (departureFix.length < 2 || departureFix.length > 12) { + return `REJECT 06 FIX FORMAT`; + } + + // Field 07: Time + const timeStr = fields[4]; + let departureTime = 0; + if (timeStr !== 'E' && timeStr !== 'P' && timeStr !== 'D') { + const timeMatch = timeStr.match(/^[PE]?(\d{4})$/); + if (!timeMatch) { + return `REJECT 07 TIM FORMAT`; + } + departureTime = parseInt(timeMatch[1]); + } + + // Field 08 or 09: Altitude (Assigned or Requested) + const altStr = fields[5]; + let altitude = ''; + if (altStr === 'OTP' || altStr === 'VFR') { + altitude = altStr; + } else { + const altMatch = altStr.match(/^(\d+|OTP|VFR)(\/(\d+))?$/); + if (!altMatch) { + return `REJECT 08 ALT FORMAT`; + } + altitude = altStr; + } + + // Field 10: Route - everything from field 6 onwards until we hit remarks + let routeEndIdx = 6; + for (let i = 6; i < fields.length; i++) { + if (fields[i].startsWith('O') || fields[i].startsWith('@')) { + routeEndIdx = i; + break; + } + routeEndIdx = i + 1; + } + + const routeParts = fields.slice(6, routeEndIdx); + if (routeParts.length === 0) { + return `REJECT 10 RTE FORMAT`; + } + const route = routeParts.join(' '); + + // Field 11: Remarks (optional) + let remarks = ''; + if (routeEndIdx < fields.length) { + const remarksFields = fields.slice(routeEndIdx); + const remarksStr = remarksFields.join(' '); + if (remarksStr.startsWith('O ')) { + remarks = remarksStr.substring(2); + } else if (remarksStr.startsWith('@')) { + remarks = remarksStr.substring(1); + } else { + remarks = remarksStr; + } + } + + try { + const existingFp = ctx.flightplans.get(aircraftId); + if (existingFp && existingFp.status === 'Active') { + return `REJECT 02 AID FLID\nDUPLICATION`; + } + + await ctx.amendFlightplan({ + aircraftId, + cid: '', + status: 'Proposed', + aircraftType, + faaEquipmentSuffix: equipmentSuffix, + equipment: `${aircraftType}/${equipmentSuffix}`, + icaoEquipmentCodes: '', + icaoSurveillanceCodes: '', + speed, + altitude, + departure: departureFix, + destination: '', + alternate: '', + route, + remarks, + assignedBeaconCode: null, + estimatedDepartureTime: departureTime, + actualDepartureTime: 0, + hoursEnroute: 0, + minutesEnroute: 0, + fuelHours: 0, + fuelMinutes: 0, + pilotCid: '', + holdAnnotations: null, + wakeTurbulenceCode: '', + }); + + return `ACCEPT\n${aircraftId}`; + } catch (error) { + console.error('Failed to create flightplan:', error); + const errorStr = String(error); + if (errorStr.includes('Not your control') || errorStr.includes('inactive session')) { + return `REJECT 01 MSG ILLEGAL\nSOURCE`; + } else { + return `REJECT FP ENTRY FAILED`; + } + } +} + +async function handleAM(input: string, ctx: CommandContext): Promise { + const amMatch = /^AM\s+(.+)$/i.exec(input.trim()); + if (!amMatch) { + return `REJECT 01 MSG INVALID\nMESSAGE TYPE`; + } + + const parts = amMatch[1].split(/\s+/); + if (parts.length < 3) { + return `REJECT FORMAT - INSUFFICIENT FIELDS\n${input}`; + } + + const aircraftId = parts[0]; + const existingFp = findFlightplan(aircraftId, ctx.flightplans); + if (!existingFp) { + return `REJECT 02 FLID NOT\nSTORED`; + } + + const amendments: { [key: string]: any } = {}; + let i = 1; + + const fieldMap: { [key: string]: string } = { + 'AID': '02', '02': '02', '2': '02', + 'TYP': '03', '03': '03', '3': '03', + 'BCN': '04', '04': '04', '4': '04', + 'SPD': '05', '05': '05', '5': '05', + 'FIX': '06', '06': '06', '6': '06', + 'TIM': '07', '07': '07', '7': '07', + 'ALT': '08', '08': '08', '8': '08', + 'RAL': '09', '09': '09', '9': '09', + 'RTE': '10', '10': '10', + 'RMK': '11', '11': '11' + }; + + while (i < parts.length) { + const fieldRef = parts[i].toUpperCase(); + const fieldNum = fieldMap[fieldRef]; + if (!fieldNum) { + return `REJECT INVALID FIELD\nREFERENCE`; + } + + i++; + if (i >= parts.length) { + return `REJECT FORMAT - MISSING AMENDMENT DATA\n${input}`; + } + + // Check if amending Field 02 (Aircraft ID) + if (fieldNum === '02') { + if (Object.keys(amendments).length > 0) { + return `REJECT - INVALID\nAMENDMENT`; + } + amendments['aircraftId'] = parts[i]; + i++; + break; // Only Field 02 can be amended when changing aircraft ID + } + + // Collect amendment data for this field + let amendmentData: string[] = []; + + // For route (10/RTE), collect all remaining parts until we hit another field ref or end + if (fieldNum === '10') { + while (i < parts.length) { + const nextToken = parts[i].toUpperCase(); + if (fieldMap[nextToken]) { + break; + } + amendmentData.push(parts[i]); + i++; + } + + if (amendmentData.length === 0) { + return `REJECT 10 RTE FORMAT`; + } + + const routeStr = amendmentData.join(' '); + const existingRoute = existingFp.route || ''; + const routeElements = routeStr.split(/[\s.]+/).filter(e => e.length > 0); + + // Check for departure fix change (single element followed by ↑) + if (routeStr.endsWith('↑') || routeStr.endsWith('^')) { + const newDepFix = routeStr.slice(0, -1).trim().split(/[\s.]+/)[0]; + amendments['departure'] = newDepFix; + amendments['route'] = routeStr.slice(0, -1).trim(); + } + // Check for complete replacement (ends with ↓) + else if (routeStr.endsWith('↓') || routeStr.endsWith('v')) { + const newRoute = routeStr.slice(0, -1).trim(); + const newRouteElements = newRoute.split(/[\s.]+/).filter(e => e.length > 0); + if (newRouteElements.length > 0) { + amendments['destination'] = newRouteElements[newRouteElements.length - 1]; + if (existingFp.status === 'Active' && existingFp.departure) { + amendments['route'] = `${existingFp.departure}/.${newRoute}`; + } else { + amendments['route'] = newRoute; + } + } else { + return `REJECT 10 RTE FORMAT`; + } + } + // Tailoring symbol at beginning (/) - insert after departure fix + else if (routeStr.startsWith('/')) { + const tailoredRoute = routeStr.substring(1).trim(); + if (existingFp.departure) { + amendments['route'] = `${existingFp.departure}/.${tailoredRoute}`; + } else { + amendments['route'] = tailoredRoute; + } + } + // Merge with existing route + else { + const firstElement = routeElements[0]; + const lastElement = routeElements[routeElements.length - 1]; + const existingElements = existingRoute.split(/[\s.]+/).filter(e => e.length > 0); + + const firstMatchIdx = existingElements.indexOf(firstElement); + const lastMatchIdx = existingElements.lastIndexOf(lastElement); + + if (firstMatchIdx !== -1 && lastMatchIdx !== -1 && firstMatchIdx < lastMatchIdx) { + const before = existingElements.slice(0, firstMatchIdx).join('.'); + const after = existingElements.slice(lastMatchIdx + 1).join('.'); + const merged = [before, routeStr, after].filter(p => p.length > 0).join('.'); + amendments['route'] = merged; + } + else if (firstMatchIdx !== -1) { + const before = existingElements.slice(0, firstMatchIdx + 1).join('.'); + amendments['route'] = `${before}.${routeElements.slice(1).join('.')}`; + } + else if (lastMatchIdx !== -1) { + const after = existingElements.slice(lastMatchIdx).join('.'); + if (existingFp.status === 'Active' && existingFp.departure) { + amendments['route'] = `${existingFp.departure}/.${routeElements.slice(0, -1).join('.')}.${after}`; + } else { + amendments['route'] = `${routeElements.slice(0, -1).join('.')}.${after}`; + } + } + else { + amendments['route'] = routeStr; + } + } + } + // For other fields, just take the next token + else { + amendmentData.push(parts[i]); + i++; + + const value = amendmentData[0]; + + switch (fieldNum) { + case '03': { // Type/Equipment + const typeMatch = value.match(/^([A-Z0-9]+)\/([A-Z])$/); + if (!typeMatch) { + return `REJECT 03 TYP FORMAT`; + } + const newAircraftType = typeMatch[1]; + const newFaaEquipmentSuffix = typeMatch[2]; + + let newEquipment = `${newAircraftType}/${newFaaEquipmentSuffix}`; + if (existingFp.equipment) { + const firstSlashIndex = existingFp.equipment.indexOf('/'); + if (firstSlashIndex > 0) { + const everythingAfterSlash = existingFp.equipment.substring(firstSlashIndex + 1); + newEquipment = `${newAircraftType}/${everythingAfterSlash}`; + } + } + + amendments['equipment'] = newEquipment; + amendments['faaEquipmentSuffix'] = newFaaEquipmentSuffix; + break; + } + + case '04': { // Beacon Code + const beaconCode = parseInt(value); + if (isNaN(beaconCode) || beaconCode < 0 || beaconCode > 7777) { + return `REJECT 04 BCN CODE FORMAT`; + } + amendments['assignedBeaconCode'] = beaconCode; + break; + } + + case '05': { // Speed + const speed = parseInt(value); + if (isNaN(speed) || speed <= 0) { + return `REJECT 05 SPD ILLEGAL`; + } + if (speed > 3700) { + return `REJECT 05 SPD FORMAT`; + } + amendments['speed'] = speed; + break; + } + + case '06': // Departure Fix + if (value.length < 2 || value.length > 12) { + return `REJECT 06 FIX FORMAT`; + } + amendments['departure'] = value; + break; + + case '07': { // Time + if (value !== 'E' && value !== 'P' && value !== 'D') { + const timeMatch = value.match(/^[PE]?(\d{4})$/); + if (!timeMatch) { + return `REJECT 07 TIM FORMAT`; + } + amendments['estimatedDepartureTime'] = parseInt(timeMatch[1]); + } + break; + } + + case '08': // Assigned Altitude + amendments['altitude'] = value; + break; + + case '09': // Requested Altitude (RAL) + amendments['altitude'] = value; + break; + + case '11': { // Remarks + while (i < parts.length) { + const nextToken = parts[i].toUpperCase(); + if (fieldMap[nextToken]) { + break; + } + amendmentData.push(parts[i]); + i++; + } + let remarks = amendmentData.join(' '); + if (remarks.startsWith('O ')) { + remarks = remarks.substring(2); + } else if (remarks.startsWith('@')) { + remarks = remarks.substring(1); + } + amendments['remarks'] = remarks; + break; + } + } + } + } + + if (Object.keys(amendments).length === 0) { + return `REJECT FORMAT - NO VALID AMENDMENTS\n${input}`; + } + + try { + const amendDto: CreateOrAmendFlightplanDto = { + aircraftId: amendments['aircraftId'] || existingFp.aircraftId, + cid: existingFp.cid, + status: existingFp.status, + aircraftType: amendments['aircraftType'] || existingFp.aircraftType, + faaEquipmentSuffix: amendments['faaEquipmentSuffix'] || existingFp.faaEquipmentSuffix, + equipment: amendments['equipment'] || existingFp.equipment, + icaoEquipmentCodes: existingFp.icaoEquipmentCodes, + icaoSurveillanceCodes: existingFp.icaoSurveillanceCodes, + speed: amendments['speed'] ?? existingFp.speed, + altitude: amendments['altitude'] || existingFp.altitude, + departure: amendments['departure'] || existingFp.departure, + destination: amendments['destination'] || existingFp.destination, + alternate: existingFp.alternate, + route: amendments['route'] || existingFp.route, + remarks: amendments['remarks'] !== undefined ? amendments['remarks'] : existingFp.remarks, + assignedBeaconCode: amendments['assignedBeaconCode'] ?? existingFp.assignedBeaconCode, + estimatedDepartureTime: amendments['estimatedDepartureTime'] ?? existingFp.estimatedDepartureTime, + actualDepartureTime: existingFp.actualDepartureTime, + hoursEnroute: existingFp.hoursEnroute, + minutesEnroute: existingFp.minutesEnroute, + fuelHours: existingFp.fuelHours, + fuelMinutes: existingFp.fuelMinutes, + pilotCid: existingFp.pilotCid, + holdAnnotations: existingFp.holdAnnotations, + wakeTurbulenceCode: existingFp.wakeTurbulenceCode, + }; + + console.log('AM Command Debug:'); + console.log(' Amendments:', amendments); + console.log(' Existing FP equipment:', existingFp.equipment); + console.log(' Existing FP faaEquipmentSuffix:', existingFp.faaEquipmentSuffix); + console.log(' New equipment:', amendDto.equipment); + console.log(' New faaEquipmentSuffix:', amendDto.faaEquipmentSuffix); + + await ctx.amendFlightplan(amendDto); + + return `ACCEPT ${amendDto.aircraftId}/${amendDto.cid}`; + } catch (error) { + console.error('Failed to amend flightplan:', error); + const errorStr = String(error); + if (errorStr.includes('Not your control') || errorStr.includes('inactive session')) { + return `REJECT 01 MSG ILLEGAL\nSOURCE`; + } else { + return `REJECT - INVALID\nAMENDMENT`; + } + } +} + +function handleGI(input: string): string { + const giMatch = /^GI\s+(\S+)\s+(.+)$/i.exec(input.trim()); + if (giMatch && giMatch.length === 3) { + const recipient = giMatch[1].toUpperCase(); + const message = giMatch[2]; + return `ACCEPT GI TO ${recipient}\n${message}`; + } else { + return `REJECT FORMAT\n${input}`; + } +} + +async function handleWR(input: string, args: string[]): Promise { + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const station = args[0]; + + try { + const response = await fetch( + `https://metar.vatsim.net/${encodeURIComponent(station)}` + ); + + if (!response.ok) { + return `REJECT WEATHER STAT REQ\nSTATION NOT FOUND`; + } + + const metar = await response.text(); + + if (!metar || metar.trim() === '' || metar.includes('No METAR')) { + return `REJECT WEATHER STAT REQ\nNO DATA FOR ${station}`; + } + + return `ACCEPT WEATHER STAT REQ\n${metar.trim()}`; + } catch (error) { + console.error('Failed to fetch METAR:', error); + return `REJECT WEATHER STAT REQ\nFETCH FAILED`; + } +} + +async function handleSR(input: string, args: string[], ctx: CommandContext): Promise { + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const identifier = args[0]; + + let aircraftId = identifier; + let strip = ctx.flightStrips?.get(identifier); + + if (!strip && ctx.flightStrips) { + for (const [id, s] of ctx.flightStrips) { + if (s.fieldValues && ( + s.fieldValues[0] === identifier || + s.fieldValues[4] === identifier || + s.fieldValues[5] === identifier)) { + strip = s; + aircraftId = id; + break; + } + } + } + + // Always request from server first - this triggers ReceiveStripItems event + try { + console.log('SR: Requesting flight strip for:', aircraftId); + await ctx.requestFlightStrip(aircraftId); + console.log('SR: RequestFlightStrip succeeded for:', aircraftId); + } catch (error) { + console.warn('SR: RequestFlightStrip failed:', error); + if (!strip?.fieldValues) { + return `REJECT\nSTRIP NOT FOUND\n${input}`; + } + } + + // If we have a local copy, display it immediately (server response will update via ReceiveStripItems) + if (strip?.fieldValues) { + const formattedStrip = formatStripFromFieldValues(strip.fieldValues); + + // Print the strip (move responseBottom to responseTop, set new strip to responseBottom) + ctx.setResponseTop(ctx.responseBottom); + ctx.setResponseBottom(formattedStrip); + + return formattedStrip; + } + + // No local strip but server request succeeded - strip will arrive via ReceiveStripItems event + return `ACCEPT SR ${identifier}\nSTRIP REQUESTED`; +} + +function handleFR(input: string, args: string[], ctx: CommandContext): string { + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const identifier = args[0]; + const flightplan = findFlightplan(identifier, ctx.flightplans); + + if (flightplan) { + const cid = flightplan.cid || ''; + const aircraftId = flightplan.aircraftId || ''; + const aircraftType = flightplan.aircraftType || ''; + const beaconCode = flightplan.assignedBeaconCode?.toString() || ''; + const speed = flightplan.speed || ''; + const time = ('P' + flightplan.estimatedDepartureTime) || ''; + const altitude = flightplan.altitude || ''; + const departure = flightplan.departure || ''; + const destination = flightplan.destination || ''; + const remarks = flightplan.remarks || ''; + const route = flightplan.route || ''; + + return `${cid} ${aircraftId} ${aircraftType} ${beaconCode} ${speed} ${time} ${altitude} ${departure} ${route} ${destination} ${remarks}`; + } else { + return `FLID NOT STORED\n${input}`; + } +} + +async function handleRS(input: string, args: string[], ctx: CommandContext): Promise { + if (args.length !== 1) { + return `REJECT FORMAT\n${input}`; + } + const identifier = args[0]; + const flightplan = findFlightplan(identifier, ctx.flightplans); + if (flightplan) { + try { + await ctx.deleteFlightplan(flightplan.aircraftId); + return `${flightplan.aircraftId} ${flightplan.cid}REMOVE \nSTRIPS`; + } catch (error) { + console.error('Failed to delete flightplan:', error); + const errorStr = String(error); + if (errorStr.includes('Not your control')) { + return `REJECT NOT YOUR CONTROL\n${flightplan.aircraftId}`; + } else { + return `REJECT DELETE FAILED\n${flightplan.aircraftId}`; + } + } + } else { + return `REJECT FLID NOT STORED\n${input}`; + } +} + +export async function parseCommand(input: string, ctx: CommandContext): Promise { + const [command, ...args] = input.trim().split(/\s+/).map(s => s.toUpperCase()); + + switch (command) { + case 'FP': + return handleFP(input, ctx); + case 'AM': + return handleAM(input, ctx); + case 'GI': + return handleGI(input); + case 'WR': + return handleWR(input, args); + case 'SR': + return handleSR(input, args, ctx); + case 'FR': + return handleFR(input, args, ctx); + case 'RS': + return handleRS(input, args, ctx); + default: + // Send to ERAM hub for all other commands + return ctx.sendCommand(input); + } +} From 58345b426dac2559d8a7746d3c3451994cf2423f Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 11 Mar 2026 14:28:17 -0700 Subject: [PATCH 26/27] refactor: remove ProcessEramMessageDto and sendEramMessage Remove sendEramMessage from HubContext public API and merge its logic into sendCommand. Remove ProcessEramMessageDto, EramMessageProcessingResultDto, and EramPositionType imports. Delete src/types/apiTypes/eramTypes.ts (done in previous commit). Hub message now uses inline object literal instead of typed DTO. --- src/contexts/HubContext.tsx | 960 ++++++++++++++++++------------------ 1 file changed, 477 insertions(+), 483 deletions(-) diff --git a/src/contexts/HubContext.tsx b/src/contexts/HubContext.tsx index 2736685..b1f660a 100644 --- a/src/contexts/HubContext.tsx +++ b/src/contexts/HubContext.tsx @@ -1,483 +1,477 @@ -import type { ReactNode } from "react"; -import React, { createContext, useCallback, useEffect, useRef, useState, useMemo } from "react"; -import { useNavigate } from "react-router-dom"; -import type { HubConnection } from "@microsoft/signalr"; -import { HttpTransportType, HubConnectionBuilder } from "@microsoft/signalr"; -import type { Nullable } from "../types/utility-types"; -import { - clearSession, - envSelector, - setSession, - vatsimTokenSelector, - setSessionIsActive, - setHubConnected, - hubConnectedSelector, - logout, -} from "../redux/slices/authSlice"; -import { refreshToken } from "../api/vNasDataApi"; -import type { ApiSessionInfoDto } from "../types/apiTypes/apiSessionInfoDto"; -import type { ApiFlightplan, CreateOrAmendFlightplanDto } from "../types/apiTypes/apiFlightplan"; -import { ApiTopic } from "../types/apiTypes/apiTopic"; -import { updateFlightplanThunk, deleteFlightplanThunk, initThunk } from "../redux/thunks"; -import { addOutageMessage, delOutageMessage, setFsdIsConnected } from "../redux/slices/appSlice"; -import { setMcaAcceptMessage, setMcaRejectMessage, setMraMessage } from "../redux/slices/mcaSlice"; -import { setArtccId, setSectorId } from "../redux/slices/sectorSlice"; -import { useRootDispatch, useRootSelector } from "../redux/hooks"; -import { VERSION } from "../utils/constants"; -import { OutageEntry } from "../types/outageEntry"; -import { HubConnectionState } from "@microsoft/signalr"; -import { invokeHub } from "../utils/hubUtils"; -import { type ProcessEramMessageDto, type EramMessageProcessingResultDto, EramPositionType } from "../types/apiTypes/eramTypes"; - -const toast = { - error: (message: string) => console.error(message) -}; - -type HubContextValue = { - connectHub: () => Promise; - disconnectHub: () => Promise; - hubConnection: HubConnection | null; - sendEramMessage: (eramMessage: ProcessEramMessageDto) => Promise; - sendCommand: (command: string) => Promise; - amendFlightplan: (fp: CreateOrAmendFlightplanDto) => Promise; - deleteFlightplan: (aircraftId: string) => Promise; - requestFlightStrip: (aircraftId: string) => Promise; - flightplans: Map; - flightStrips: Map; -}; - -export const HubContext = createContext(null); - -export const HubContextProvider = ({ children }: { children: ReactNode }) => { - const dispatch = useRootDispatch(); - const vatsimToken = useRootSelector(vatsimTokenSelector)!; - const ref = useRef>(null); - const env = useRootSelector(envSelector); - const navigate = useNavigate(); - const hubConnected = useRootSelector(hubConnectedSelector); - const [flightplans, setFlightplans] = useState>(new Map()); - const [flightStrips, setFlightStrips] = useState>(new Map()); - const [facilityId, setFacilityId] = useState(""); - - const disconnectHub = useCallback(async () => { - try { - await ref.current?.stop(); - dispatch(setHubConnected(false)); - dispatch(setArtccId("")); - dispatch(setSectorId("")); - - dispatch(clearSession()); - dispatch(logout()); - navigate("/login", { replace: true }); - } catch (error) { - console.error("Error during hub disconnect:", error); - navigate("/login", { replace: true }); - } - }, [dispatch, navigate]); - - const handleSessionStart = useCallback( - async (sessionInfo: ApiSessionInfoDto, hubConnection: HubConnection) => { - if (!sessionInfo || sessionInfo.isPseudoController) { - return; - } - - try { - const primaryPosition = sessionInfo.positions.find((p) => p.isPrimary)?.position; - - if (!primaryPosition) { - throw new Error("No primary position found"); - } - - const artccId = sessionInfo.artccId; - // For ERAM positions, use the sectorId; for other positions like tower, use the callsign - const sectorId = primaryPosition.eramConfiguration?.sectorId || primaryPosition.callsign || "UNKNOWN"; - - console.log('Primary position details:', { - callsign: primaryPosition.callsign, - name: primaryPosition.name, - hasEramConfig: !!primaryPosition.eramConfiguration, - eramSectorId: primaryPosition.eramConfiguration?.sectorId, - finalSectorId: sectorId - }); - - dispatch(setArtccId(artccId)); - dispatch(setSectorId(sectorId)); - dispatch(setSession(sessionInfo)); - dispatch(setSessionIsActive(sessionInfo.isActive ?? false)); - dispatch(initThunk()); - - if (hubConnection.state === HubConnectionState.Connected) { - const joinSessionParams = { - sessionId: sessionInfo.id, - clientName: "vFDIO", - clientVersion: VERSION, - hasEramConfig: !!primaryPosition.eramConfiguration, - eramSectorId: primaryPosition.eramConfiguration?.sectorId ?? 0 - }; - console.log('Sending joinSession with params:', joinSessionParams); - await hubConnection.invoke("joinSession", joinSessionParams); - console.log(`Joined session ${sessionInfo.id} with position ${primaryPosition.callsign} (${primaryPosition.name})`); - - const artccFacilityId = sessionInfo.artccId; // Use ARTCC ID instead of facility ID - const primaryFacilityId = sessionInfo.positions.find((p) => p.isPrimary)?.facilityId; - - // Store facility ID for later use - if (primaryFacilityId) { - setFacilityId(primaryFacilityId); - } - - if (artccFacilityId) { - try { - // Subscribe to FlightPlans using ARTCC ID (e.g., ZOA instead of OAK) - const initialFlightplans = await hubConnection.invoke("subscribe", new ApiTopic("FlightPlans", artccFacilityId)); - - if (initialFlightplans && Array.isArray(initialFlightplans)) { - setFlightplans(prev => { - const newMap = new Map(prev); - initialFlightplans.forEach(fp => { - if (fp?.aircraftId) { - newMap.set(fp.aircraftId, fp); - dispatch(updateFlightplanThunk(fp)); - } - }); - return newMap; - }); - } - - // Subscribe to FlightStrips using facility ID (e.g., OAK) - if (primaryFacilityId) { - const initialFlightStrips = await hubConnection.invoke("subscribe", new ApiTopic("FlightStrips", primaryFacilityId)); - console.log("Subscribed to FlightStrips, received:", initialFlightStrips); - - if (initialFlightStrips && Array.isArray(initialFlightStrips)) { - setFlightStrips(prev => { - const newMap = new Map(prev); - initialFlightStrips.forEach(strip => { - if (strip?.aircraftId) { - newMap.set(strip.aircraftId, strip); - } - }); - return newMap; - }); - } - } - - } catch (subscribeError) { - console.warn(`Failed to subscribe: ${subscribeError}`); - } - } - dispatch(setHubConnected(true)); - } else { - throw new Error("Hub connection not in Connected state"); - } - } catch (error: any) { - console.error("Session start failed:", error); - toast.error(error.message); - await disconnectHub(); - } - }, - [dispatch, disconnectHub] - ); - - useEffect(() => { - if (!env || !vatsimToken) { - return; - } - - const hubUrl = env.clientHubUrl; - - const getValidNasToken = async () => { - return refreshToken(env.apiBaseUrl, vatsimToken).then((r) => { - console.log("Refreshed NAS token"); - return r.data; - }); - }; - - ref.current = new HubConnectionBuilder() - .withUrl(hubUrl, { - accessTokenFactory: getValidNasToken, - transport: HttpTransportType.WebSockets, - skipNegotiation: true, - }) - .withAutomaticReconnect() - .build(); - - const hubConnection = ref.current; - - hubConnection.onclose(() => { - dispatch(setArtccId("")); - dispatch(setSectorId("")); - dispatch(setHubConnected(false)); - console.log("ATC hub disconnected"); - navigate("/login", { replace: true }); - }); - - hubConnection.on("HandleSessionStarted", (sessionInfo: ApiSessionInfoDto) => { - console.log("Session started:", sessionInfo); - handleSessionStart(sessionInfo, hubConnection); - }); - - hubConnection.on("HandleSessionEnded", () => { - console.log("clearing session"); - dispatch(clearSession()); - disconnectHub(); - }); - - hubConnection.on("ReceiveFlightPlans", async (topic: ApiTopic, flightplans: ApiFlightplan[]) => { - if (flightplans && Array.isArray(flightplans)) { - setFlightplans(prev => { - const newMap = new Map(prev); - flightplans.forEach(fp => { - if (fp?.aircraftId) { - newMap.set(fp.aircraftId, fp); - dispatch(updateFlightplanThunk(fp)); - } - }); - return newMap; - }); - } - }); - - hubConnection.on("DeleteFlightplans", async (topic: ApiTopic, flightplanIds: string[]) => { - flightplanIds.forEach((flightplanId) => { - setFlightplans(prev => { - const newMap = new Map(prev); - newMap.delete(flightplanId); - return newMap; - }); - dispatch(deleteFlightplanThunk(flightplanId)); - }); - }); - - hubConnection.on("ReceiveStripItems", async (topic: any, stripItems: any[]) => { - console.log("Received ReceiveStripItems:", stripItems); - if (stripItems && Array.isArray(stripItems)) { - setFlightStrips(prev => { - const newMap = new Map(prev); - stripItems.forEach(strip => { - if (strip?.aircraftId) { - newMap.set(strip.aircraftId, strip); - } - }); - return newMap; - }); - } - }); - - hubConnection.on("HandleFsdConnectionStateChanged", (state: boolean) => { - dispatch(setFsdIsConnected(state)); - if (!state) { - dispatch(addOutageMessage(new OutageEntry("FSD_DOWN", "FSD CONNECTION DOWN"))); - } else { - dispatch(delOutageMessage("FSD_DOWN")); - } - }); - - hubConnection.on("SetSessionActive", (isActive) => { - dispatch(setSessionIsActive(isActive)); - sessionStorage.setItem("session-active", `${isActive}`); - }); - - hubConnection.keepAliveIntervalInMilliseconds = 1000; - }, [dispatch, navigate, disconnectHub, handleSessionStart, env, vatsimToken]); - - const connectHub = useCallback(async () => { - if (!env || !vatsimToken || !ref.current) { - if (ref.current?.state === HubConnectionState.Connected) { - dispatch(setHubConnected(true)); - return; - } - dispatch(setHubConnected(false)); - throw new Error(`Cannot connect - env: ${!!env}, token: ${!!vatsimToken}, ref: ${!!ref.current}`); - } - - const hubConnection = ref.current; - - if (hubConnection.state !== HubConnectionState.Connected) { - try { - await hubConnection.start(); - console.log("Connected to hub, waiting for session..."); - - try { - const sessions = await hubConnection.invoke("GetSessions"); - const primarySession = sessions?.find((s) => !s.isPseudoController); - console.log("Available sessions:", sessions); - const primaryPosition = primarySession?.positions.find((p) => p.isPrimary)?.position; - - console.log(sessions); - console.log(primarySession); - - if (primarySession && primaryPosition) { - console.log(`Found primary position: ${primaryPosition.callsign} (${primaryPosition.name})`); - console.log(hubConnection); - await handleSessionStart(primarySession, hubConnection); - console.log(`joined existing session ${primarySession.id}`); - } else { - console.log("No primary session found, waiting for HandleSessionStarted event"); - } - } catch (error) { - console.log(error); - console.log("No active session yet, waiting for HandleSessionStarted event"); - } - } catch (error) { - dispatch(setHubConnected(false)); - throw error; - } - } - }, [dispatch, handleSessionStart, env, vatsimToken]); - - const sendEramMessage = useCallback(async (eramMessage: ProcessEramMessageDto) => { - return invokeHub(() => ref.current, connectHub, async (connection) => { - const result = await connection.invoke("processEramMessage", eramMessage); - if (result) { - if (result.isSuccess) { - const feedbackMessage = result.feedback.length > 0 ? result.feedback.join("\n") : "Command accepted"; - console.log("ERAM command processed successfully:", feedbackMessage); - - if (result.response) { - dispatch(setMraMessage(result.response)); - } - } else { - const rejectMessage = result.feedback.length > 0 ? `REJECT\n${result.feedback.join("\n")}` : "REJECT\nCommand failed"; - console.log("ERAM command processing failed:", rejectMessage); - } - } - return result; - }); - }, [connectHub, dispatch]); - - const amendFlightplan = useCallback(async (fp: CreateOrAmendFlightplanDto) => { - return invokeHub(() => ref.current, connectHub, async (connection) => { - await connection.invoke("amendFlightPlan", fp); - }); - }, [connectHub]); - - const deleteFlightplan = useCallback(async (aircraftId: string) => { - // Workaround: "delete" by amending the flightplan to be blank/empty - try { - console.log(`Attempting to delete flightplan by clearing data: ${aircraftId}`); - await amendFlightplan({ - aircraftId: aircraftId, - cid: '', - status: 'Proposed', - aircraftType: '', - faaEquipmentSuffix: '', - equipment: '', - icaoEquipmentCodes: '', - icaoSurveillanceCodes: '', - speed: 0, - altitude: '', - departure: '', - destination: '', - alternate: '', - route: '', - remarks: '', - assignedBeaconCode: null, - estimatedDepartureTime: 0, - actualDepartureTime: 0, - hoursEnroute: 0, - minutesEnroute: 0, - fuelHours: 0, - fuelMinutes: 0, - pilotCid: '', - holdAnnotations: null, - wakeTurbulenceCode: '', - }); - console.log(`Cleared flightplan data for: ${aircraftId}`); - - // Only remove from local state if server operation succeeded - setFlightplans(prev => { - const newMap = new Map(prev); - newMap.delete(aircraftId); - return newMap; - }); - dispatch(deleteFlightplanThunk(aircraftId)); - } catch (error) { - console.error(`Failed to clear flightplan: ${error}`); - // Re-throw the error so the caller can handle it - throw error; - } - }, [amendFlightplan, dispatch]); - - const requestFlightStrip = useCallback(async (aircraftId: string) => { - return invokeHub(() => ref.current, connectHub, async (connection) => { - await connection.invoke("RequestFlightStrip", facilityId, aircraftId.toUpperCase()); - }); - }, [connectHub, facilityId]); - - const sendCommand = useCallback(async (command: string): Promise => { - const trimmedCommand = command.trim().toUpperCase(); - const parts = trimmedCommand.split(' '); - - try { - const elements = parts.map(token => ({ - token: token - })); - - // Always use DSide (ERAM) position type to allow ERAM commands from any position - const eramMessage: ProcessEramMessageDto = { - source: EramPositionType.DSide, - elements, - invertNumericKeypad: false - }; - - const result = await sendEramMessage(eramMessage); - - if (result) { - if (result.isSuccess) { - console.log(result); - const feedback = result.feedback?.length > 0 ? result.feedback.join("\n") : ""; - const response = result.response || ""; - - if (feedback && response) { - return `${feedback}\n${response}`; - } else if (response) { - return response; - } else if (feedback) { - return feedback; - } else { - return "COMMAND ACCEPTED"; - } - } else { - return `REJECT: ${result.feedback.join("\n") || "COMMAND FAILED"}`; - } - } else { - return "ERROR: NO RESPONSE FROM SERVER"; - } - - } catch (error) { - console.error('Command processing failed:', error); - return `ERROR: ${error}`; - } - }, [flightplans, sendEramMessage]); - - // Auto-connect when the provider is mounted and we have token + env - useEffect(() => { - if (vatsimToken && env) { - const timer = setTimeout(() => { - console.log("Auto-connecting to hub..."); - connectHub().catch((error) => { - console.error("Auto-connect failed:", error); - }); - }, 1000); // Give a moment for the component to settle - - return () => clearTimeout(timer); - } - }, [vatsimToken, env, connectHub]); - - const contextValue = useMemo(() => ({ - hubConnection: ref.current, - hubConnected, - connectHub, - disconnectHub, - sendEramMessage, - sendCommand, - amendFlightplan, - deleteFlightplan, - requestFlightStrip, - flightplans, - flightStrips, - }), [hubConnected, connectHub, disconnectHub, sendEramMessage, sendCommand, amendFlightplan, deleteFlightplan, requestFlightStrip, flightplans, flightStrips]); - - return {children}; -}; +import type { ReactNode } from "react"; +import React, { createContext, useCallback, useEffect, useRef, useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import type { HubConnection } from "@microsoft/signalr"; +import { HttpTransportType, HubConnectionBuilder } from "@microsoft/signalr"; +import type { Nullable } from "../types/utility-types"; +import { + clearSession, + envSelector, + setSession, + vatsimTokenSelector, + setSessionIsActive, + setHubConnected, + hubConnectedSelector, + logout, +} from "../redux/slices/authSlice"; +import { refreshToken } from "../api/vNasDataApi"; +import type { ApiSessionInfoDto } from "../types/apiTypes/apiSessionInfoDto"; +import type { ApiFlightplan, CreateOrAmendFlightplanDto } from "../types/apiTypes/apiFlightplan"; +import { ApiTopic } from "../types/apiTypes/apiTopic"; +import { updateFlightplanThunk, deleteFlightplanThunk, initThunk } from "../redux/thunks"; +import { addOutageMessage, delOutageMessage, setFsdIsConnected } from "../redux/slices/appSlice"; +import { setMcaAcceptMessage, setMcaRejectMessage, setMraMessage } from "../redux/slices/mcaSlice"; +import { setArtccId, setSectorId } from "../redux/slices/sectorSlice"; +import { useRootDispatch, useRootSelector } from "../redux/hooks"; +import { VERSION } from "../utils/constants"; +import { OutageEntry } from "../types/outageEntry"; +import { HubConnectionState } from "@microsoft/signalr"; +import { invokeHub } from "../utils/hubUtils"; + +const toast = { + error: (message: string) => console.error(message) +}; + +type HubContextValue = { + connectHub: () => Promise; + disconnectHub: () => Promise; + hubConnection: HubConnection | null; + sendCommand: (command: string) => Promise; + amendFlightplan: (fp: CreateOrAmendFlightplanDto) => Promise; + deleteFlightplan: (aircraftId: string) => Promise; + requestFlightStrip: (aircraftId: string) => Promise; + flightplans: Map; + flightStrips: Map; +}; + +export const HubContext = createContext(null); + +export const HubContextProvider = ({ children }: { children: ReactNode }) => { + const dispatch = useRootDispatch(); + const vatsimToken = useRootSelector(vatsimTokenSelector)!; + const ref = useRef>(null); + const env = useRootSelector(envSelector); + const navigate = useNavigate(); + const hubConnected = useRootSelector(hubConnectedSelector); + const [flightplans, setFlightplans] = useState>(new Map()); + const [flightStrips, setFlightStrips] = useState>(new Map()); + const [facilityId, setFacilityId] = useState(""); + + const disconnectHub = useCallback(async () => { + try { + await ref.current?.stop(); + dispatch(setHubConnected(false)); + dispatch(setArtccId("")); + dispatch(setSectorId("")); + + dispatch(clearSession()); + dispatch(logout()); + navigate("/login", { replace: true }); + } catch (error) { + console.error("Error during hub disconnect:", error); + navigate("/login", { replace: true }); + } + }, [dispatch, navigate]); + + const handleSessionStart = useCallback( + async (sessionInfo: ApiSessionInfoDto, hubConnection: HubConnection) => { + if (!sessionInfo || sessionInfo.isPseudoController) { + return; + } + + try { + const primaryPosition = sessionInfo.positions.find((p) => p.isPrimary)?.position; + + if (!primaryPosition) { + throw new Error("No primary position found"); + } + + const artccId = sessionInfo.artccId; + // For ERAM positions, use the sectorId; for other positions like tower, use the callsign + const sectorId = primaryPosition.eramConfiguration?.sectorId || primaryPosition.callsign || "UNKNOWN"; + + console.log('Primary position details:', { + callsign: primaryPosition.callsign, + name: primaryPosition.name, + hasEramConfig: !!primaryPosition.eramConfiguration, + eramSectorId: primaryPosition.eramConfiguration?.sectorId, + finalSectorId: sectorId + }); + + dispatch(setArtccId(artccId)); + dispatch(setSectorId(sectorId)); + dispatch(setSession(sessionInfo)); + dispatch(setSessionIsActive(sessionInfo.isActive ?? false)); + dispatch(initThunk()); + + if (hubConnection.state === HubConnectionState.Connected) { + const joinSessionParams = { + sessionId: sessionInfo.id, + clientName: "vFDIO", + clientVersion: VERSION, + hasEramConfig: !!primaryPosition.eramConfiguration, + eramSectorId: primaryPosition.eramConfiguration?.sectorId ?? 0 + }; + console.log('Sending joinSession with params:', joinSessionParams); + await hubConnection.invoke("joinSession", joinSessionParams); + console.log(`Joined session ${sessionInfo.id} with position ${primaryPosition.callsign} (${primaryPosition.name})`); + + const artccFacilityId = sessionInfo.artccId; // Use ARTCC ID instead of facility ID + const primaryFacilityId = sessionInfo.positions.find((p) => p.isPrimary)?.facilityId; + + // Store facility ID for later use + if (primaryFacilityId) { + setFacilityId(primaryFacilityId); + } + + if (artccFacilityId) { + try { + // Subscribe to FlightPlans using ARTCC ID (e.g., ZOA instead of OAK) + const initialFlightplans = await hubConnection.invoke("subscribe", new ApiTopic("FlightPlans", artccFacilityId)); + + if (initialFlightplans && Array.isArray(initialFlightplans)) { + setFlightplans(prev => { + const newMap = new Map(prev); + initialFlightplans.forEach(fp => { + if (fp?.aircraftId) { + newMap.set(fp.aircraftId, fp); + dispatch(updateFlightplanThunk(fp)); + } + }); + return newMap; + }); + } + + // Subscribe to FlightStrips using facility ID (e.g., OAK) + if (primaryFacilityId) { + const initialFlightStrips = await hubConnection.invoke("subscribe", new ApiTopic("FlightStrips", primaryFacilityId)); + console.log("Subscribed to FlightStrips, received:", initialFlightStrips); + + if (initialFlightStrips && Array.isArray(initialFlightStrips)) { + setFlightStrips(prev => { + const newMap = new Map(prev); + initialFlightStrips.forEach(strip => { + if (strip?.aircraftId) { + newMap.set(strip.aircraftId, strip); + } + }); + return newMap; + }); + } + } + + } catch (subscribeError) { + console.warn(`Failed to subscribe: ${subscribeError}`); + } + } + dispatch(setHubConnected(true)); + } else { + throw new Error("Hub connection not in Connected state"); + } + } catch (error: any) { + console.error("Session start failed:", error); + toast.error(error.message); + await disconnectHub(); + } + }, + [dispatch, disconnectHub] + ); + + useEffect(() => { + if (!env || !vatsimToken) { + return; + } + + const hubUrl = env.clientHubUrl; + + const getValidNasToken = async () => { + return refreshToken(env.apiBaseUrl, vatsimToken).then((r) => { + console.log("Refreshed NAS token"); + return r.data; + }); + }; + + ref.current = new HubConnectionBuilder() + .withUrl(hubUrl, { + accessTokenFactory: getValidNasToken, + transport: HttpTransportType.WebSockets, + skipNegotiation: true, + }) + .withAutomaticReconnect() + .build(); + + const hubConnection = ref.current; + + hubConnection.onclose(() => { + dispatch(setArtccId("")); + dispatch(setSectorId("")); + dispatch(setHubConnected(false)); + console.log("ATC hub disconnected"); + navigate("/login", { replace: true }); + }); + + hubConnection.on("HandleSessionStarted", (sessionInfo: ApiSessionInfoDto) => { + console.log("Session started:", sessionInfo); + handleSessionStart(sessionInfo, hubConnection); + }); + + hubConnection.on("HandleSessionEnded", () => { + console.log("clearing session"); + dispatch(clearSession()); + disconnectHub(); + }); + + hubConnection.on("ReceiveFlightPlans", async (topic: ApiTopic, flightplans: ApiFlightplan[]) => { + if (flightplans && Array.isArray(flightplans)) { + setFlightplans(prev => { + const newMap = new Map(prev); + flightplans.forEach(fp => { + if (fp?.aircraftId) { + newMap.set(fp.aircraftId, fp); + dispatch(updateFlightplanThunk(fp)); + } + }); + return newMap; + }); + } + }); + + hubConnection.on("DeleteFlightplans", async (topic: ApiTopic, flightplanIds: string[]) => { + flightplanIds.forEach((flightplanId) => { + setFlightplans(prev => { + const newMap = new Map(prev); + newMap.delete(flightplanId); + return newMap; + }); + dispatch(deleteFlightplanThunk(flightplanId)); + }); + }); + + hubConnection.on("ReceiveStripItems", async (topic: any, stripItems: any[]) => { + console.log("Received ReceiveStripItems:", stripItems); + if (stripItems && Array.isArray(stripItems)) { + setFlightStrips(prev => { + const newMap = new Map(prev); + stripItems.forEach(strip => { + if (strip?.aircraftId) { + newMap.set(strip.aircraftId, strip); + } + }); + return newMap; + }); + } + }); + + hubConnection.on("HandleFsdConnectionStateChanged", (state: boolean) => { + dispatch(setFsdIsConnected(state)); + if (!state) { + dispatch(addOutageMessage(new OutageEntry("FSD_DOWN", "FSD CONNECTION DOWN"))); + } else { + dispatch(delOutageMessage("FSD_DOWN")); + } + }); + + hubConnection.on("SetSessionActive", (isActive) => { + dispatch(setSessionIsActive(isActive)); + sessionStorage.setItem("session-active", `${isActive}`); + }); + + hubConnection.keepAliveIntervalInMilliseconds = 1000; + }, [dispatch, navigate, disconnectHub, handleSessionStart, env, vatsimToken]); + + const connectHub = useCallback(async () => { + if (!env || !vatsimToken || !ref.current) { + if (ref.current?.state === HubConnectionState.Connected) { + dispatch(setHubConnected(true)); + return; + } + dispatch(setHubConnected(false)); + throw new Error(`Cannot connect - env: ${!!env}, token: ${!!vatsimToken}, ref: ${!!ref.current}`); + } + + const hubConnection = ref.current; + + if (hubConnection.state !== HubConnectionState.Connected) { + try { + await hubConnection.start(); + console.log("Connected to hub, waiting for session..."); + + try { + const sessions = await hubConnection.invoke("GetSessions"); + const primarySession = sessions?.find((s) => !s.isPseudoController); + console.log("Available sessions:", sessions); + const primaryPosition = primarySession?.positions.find((p) => p.isPrimary)?.position; + + console.log(sessions); + console.log(primarySession); + + if (primarySession && primaryPosition) { + console.log(`Found primary position: ${primaryPosition.callsign} (${primaryPosition.name})`); + console.log(hubConnection); + await handleSessionStart(primarySession, hubConnection); + console.log(`joined existing session ${primarySession.id}`); + } else { + console.log("No primary session found, waiting for HandleSessionStarted event"); + } + } catch (error) { + console.log(error); + console.log("No active session yet, waiting for HandleSessionStarted event"); + } + } catch (error) { + dispatch(setHubConnected(false)); + throw error; + } + } + }, [dispatch, handleSessionStart, env, vatsimToken]); + + const sendCommand = useCallback(async (command: string): Promise => { + const trimmedCommand = command.trim().toUpperCase(); + const parts = trimmedCommand.split(' '); + + try { + const elements = parts.map(token => ({ + token: token + })); + + const eramMessage = { + source: 1, // DSide + elements, + invertNumericKeypad: false + }; + + const result = await invokeHub<{ isSuccess: boolean; feedback: string[]; response?: string }>( + () => ref.current, + connectHub, + async (connection) => { + const res = await connection.invoke("processEramMessage", eramMessage); + if (res) { + if (res.isSuccess) { + const feedbackMessage = res.feedback.length > 0 ? res.feedback.join("\n") : "Command accepted"; + console.log("ERAM command processed successfully:", feedbackMessage); + if (res.response) { + dispatch(setMraMessage(res.response)); + } + } else { + const rejectMessage = res.feedback.length > 0 ? `REJECT\n${res.feedback.join("\n")}` : "REJECT\nCommand failed"; + console.log("ERAM command processing failed:", rejectMessage); + } + } + return res; + } + ); + + if (result) { + if (result.isSuccess) { + console.log(result); + const feedback = result.feedback?.length > 0 ? result.feedback.join("\n") : ""; + const response = result.response || ""; + + if (feedback && response) { + return `${feedback}\n${response}`; + } else if (response) { + return response; + } else if (feedback) { + return feedback; + } else { + return "COMMAND ACCEPTED"; + } + } else { + return `REJECT: ${result.feedback.join("\n") || "COMMAND FAILED"}`; + } + } else { + return "ERROR: NO RESPONSE FROM SERVER"; + } + + } catch (error) { + console.error('Command processing failed:', error); + return `ERROR: ${error}`; + } + }, [connectHub, dispatch]); + + const amendFlightplan = useCallback(async (fp: CreateOrAmendFlightplanDto) => { + return invokeHub(() => ref.current, connectHub, async (connection) => { + await connection.invoke("amendFlightPlan", fp); + }); + }, [connectHub]); + + const deleteFlightplan = useCallback(async (aircraftId: string) => { + // Workaround: "delete" by amending the flightplan to be blank/empty + try { + console.log(`Attempting to delete flightplan by clearing data: ${aircraftId}`); + await amendFlightplan({ + aircraftId: aircraftId, + cid: '', + status: 'Proposed', + aircraftType: '', + faaEquipmentSuffix: '', + equipment: '', + icaoEquipmentCodes: '', + icaoSurveillanceCodes: '', + speed: 0, + altitude: '', + departure: '', + destination: '', + alternate: '', + route: '', + remarks: '', + assignedBeaconCode: null, + estimatedDepartureTime: 0, + actualDepartureTime: 0, + hoursEnroute: 0, + minutesEnroute: 0, + fuelHours: 0, + fuelMinutes: 0, + pilotCid: '', + holdAnnotations: null, + wakeTurbulenceCode: '', + }); + console.log(`Cleared flightplan data for: ${aircraftId}`); + + // Only remove from local state if server operation succeeded + setFlightplans(prev => { + const newMap = new Map(prev); + newMap.delete(aircraftId); + return newMap; + }); + dispatch(deleteFlightplanThunk(aircraftId)); + } catch (error) { + console.error(`Failed to clear flightplan: ${error}`); + throw error; + } + }, [amendFlightplan, dispatch]); + + const requestFlightStrip = useCallback(async (aircraftId: string) => { + return invokeHub(() => ref.current, connectHub, async (connection) => { + await connection.invoke("RequestFlightStrip", facilityId, aircraftId.toUpperCase()); + }); + }, [connectHub, facilityId]); + + // Auto-connect when the provider is mounted and we have token + env + useEffect(() => { + if (vatsimToken && env) { + const timer = setTimeout(() => { + console.log("Auto-connecting to hub..."); + connectHub().catch((error) => { + console.error("Auto-connect failed:", error); + }); + }, 1000); // Give a moment for the component to settle + + return () => clearTimeout(timer); + } + }, [vatsimToken, env, connectHub]); + + const contextValue = useMemo(() => ({ + hubConnection: ref.current, + hubConnected, + connectHub, + disconnectHub, + sendCommand, + amendFlightplan, + deleteFlightplan, + requestFlightStrip, + flightplans, + flightStrips, + }), [hubConnected, connectHub, disconnectHub, sendCommand, amendFlightplan, deleteFlightplan, requestFlightStrip, flightplans, flightStrips]); + + return {children}; +}; From eae8aa9784499b6d99bad7d4593065cffad72ecf Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 12 Mar 2026 08:48:00 -0700 Subject: [PATCH 27/27] Stop rendering separators and half strips --- src/App.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 399cc6a..034360a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -73,8 +73,11 @@ const AppContent = () => { useEffect(() => { if (!hubConnection) return; + const separatorTypes = ['RedSeparator', 'GreenSeparator', 'WhiteSeparator', 'HalfStripLeft']; + const handleStripPrint = (topic: any, stripItems: any[]) => { stripItems.forEach(strip => { + if (separatorTypes.includes(strip?.type)) return; if (strip?.fieldValues) { const formattedStrip = formatStripFromFieldValues(strip.fieldValues); console.log('ReceiveStripItems - Final formatted strip:', formattedStrip);