diff --git a/.env.development b/.env.development index df61907..f36e24c 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,13 @@ VITE_API_URL="http://localhost:3001" VITE_ENV="development" + +NODE_ENV="development" + +# Telegram Bot Configuration +# Get your bot token from @BotFather on Telegram +TELEGRAM_BOT_TOKEN="466589147:AAHhNtyR0SN0hI9YUxd-On0TlHDdYbHDxvY" + +# Get your chat ID by messaging your bot and visiting: +# https://api.telegram.org/bot/getUpdates +TELEGRAM_CHAT_ID="177464418" + diff --git a/.env.docker b/.env.docker index f70de6f..07e8234 100644 --- a/.env.docker +++ b/.env.docker @@ -1,2 +1,11 @@ VITE_API_URL=/api VITE_ENV=docker + +# Telegram Bot Configuration +# Get your bot token from @BotFather on Telegram +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# Get your chat ID by messaging your bot and visiting: +# https://api.telegram.org/bot/getUpdates +TELEGRAM_CHAT_ID=your_chat_id_here + diff --git a/.env.production b/.env.production index a2c155b..ba0e72e 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,10 @@ VITE_API_URL=/api VITE_ENV=production + +# Telegram Bot Configuration +# Get your bot token from @BotFather on Telegram +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# Get your chat ID by messaging your bot and visiting: +# https://api.telegram.org/bot/getUpdates +TELEGRAM_CHAT_ID=your_chat_id_here diff --git a/README.md b/README.md new file mode 100644 index 0000000..6964802 --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# Lawn Mowing Schedule Manager + +A comprehensive full-stack application for managing lawn mowing schedules with zone tracking, history logging, and Telegram notifications. + +## Features + +### 🌱 Zone Management +- Create and manage lawn zones with images +- Set mowing intervals or specific dates +- Track zone areas and status +- Visual site plan with interactive markers + +### 📊 Activity Tracking +- Record mowing and trimming sessions +- Bulk operations for multiple zones +- Detailed history with notes and weather +- Time tracking and statistics + +### 📱 Telegram Notifications +- Daily reminders for due/overdue zones +- Weekly status reports every Sunday +- Automatic scheduling with cron jobs +- Manual test triggers + +### 📈 Analytics +- Zone status dashboard +- Mowing progress tracking +- Weekly and monthly statistics +- Equipment usage tracking + +## Setup + +### Prerequisites +- Node.js 18+ +- npm or yarn + +### Installation + +1. Clone the repository +```bash +git clone +cd lawn-mowing-scheduler +``` + +2. Install dependencies +```bash +npm install +``` + +3. Configure environment variables +```bash +cp .env.example .env +``` + +Edit `.env` with your settings: +```env +# Telegram Bot Configuration (optional) +TELEGRAM_BOT_TOKEN=your_bot_token_here +TELEGRAM_CHAT_ID=your_chat_id_here + +# Database +DATABASE_URL=file:lawn_scheduler.db + +# Server +PORT=3001 +NODE_ENV=development +``` + +### Telegram Setup (Optional) + +1. Create a bot with @BotFather on Telegram: + - Send `/newbot` to @BotFather + - Follow the instructions to create your bot + - Copy the bot token + +2. Get your chat ID: + - Start a chat with your bot + - Send any message + - Visit `https://api.telegram.org/bot/getUpdates` + - Find your chat ID in the response + +3. Add the credentials to your `.env` file + +4. Test the connection using the Telegram settings in the app + +### Development + +Start the development server: +```bash +npm run dev +``` + +This will start: +- Frontend (Vite): http://localhost:5173 +- Backend (Express): http://localhost:3001 + +### Production + +Build and start the production server: +```bash +npm run build +npm start +``` + +## Docker Deployment + +### Development +```bash +docker-compose -f docker-compose.dev.yml up --build +``` + +### Production +```bash +docker-compose up --build +``` + +## API Endpoints + +### Zones +- `GET /api/zones` - Get all zones +- `POST /api/zones` - Create zone +- `PUT /api/zones/:id` - Update zone +- `DELETE /api/zones/:id` - Delete zone +- `POST /api/zones/:id/mow` - Mark zone as mowed +- `POST /api/zones/:id/trim` - Record trimming +- `POST /api/zones/bulk-mow` - Bulk mowing session + +### History +- `GET /api/history` - Get mowing history +- `GET /api/history/stats` - Get statistics + +### Equipment +- `GET /api/mowers` - Get mowers +- `POST /api/mowers` - Create mower + +### Telegram +- `POST /api/telegram/test` - Test connection +- `POST /api/telegram/check-reminders` - Manual reminder +- `POST /api/telegram/weekly-report` - Manual report + +## Notification Schedule + +- **Daily Reminders**: 8:00 AM - Zones that are due or overdue +- **Weekly Reports**: Sunday 9:00 AM - Complete status summary + +## Database Schema + +The application uses SQLite with the following main tables: +- `zones` - Lawn zones with scheduling info +- `mowing_history` - Activity log with sessions +- `mowers` - Equipment tracking + +## Technologies Used + +### Frontend +- React 18 with TypeScript +- Tailwind CSS for styling +- Vite for development and building + +### Backend +- Node.js with Express +- SQLite with libsql client +- Multer for file uploads +- node-cron for scheduling +- node-telegram-bot-api for notifications + +### Infrastructure +- Docker for containerization +- Nginx for reverse proxy +- File-based SQLite database + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## License + +MIT License - see LICENSE file for details \ No newline at end of file diff --git a/db/lawn_scheduler.db b/db/lawn_scheduler.db index 81eae8f..cfe58ae 100644 Binary files a/db/lawn_scheduler.db and b/db/lawn_scheduler.db differ diff --git a/lawn_scheduler.db b/lawn_scheduler.db new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 3c934bb..cda0876 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,22 @@ { "name": "lawn-mowing-scheduler", - "version": "0.3.1", + "version": "0.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lawn-mowing-scheduler", - "version": "0.3.1", + "version": "0.6.2", "dependencies": { "@libsql/client": "^0.4.0", "cors": "^2.8.5", + "dotenv": "^17.2.0", "express": "^4.18.2", + "framer-motion": "^12.23.12", "lucide-react": "^0.344.0", - "multer": "^2.0.1", + "multer": "2.0.1", + "node-cron": "^4.2.1", + "node-telegram-bot-api": "^0.66.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.0" @@ -363,6 +367,83 @@ "node": ">=6.9.0" } }, + "node_modules/@cypress/request": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.8.tgz", + "integrity": "sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request-promise": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@cypress/request-promise/-/request-promise-5.0.0.tgz", + "integrity": "sha512-eKdYVpa9cBEw2kTBlHeu1PP16Blwtum6QHg/u9s/MoHkZfuo1pRGka1VlUHXF5kdew82BvOJVVGk0x8X0nbp+w==", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "@cypress/request": "^3.0.0" + } + }, + "node_modules/@cypress/request-promise/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/@cypress/request/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1796,7 +1877,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1869,12 +1949,93 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array.prototype.findindex": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findindex/-/array.prototype.findindex-2.2.4.tgz", + "integrity": "sha512-LLm4mhxa9v8j0A/RPnpQAP4svXToJFh+Hp1pNYl5ZD5qpB4zdx/D4YjpVcETkhFbUKWO3iGMVLvrOnnmkAJT6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1918,12 +2079,51 @@ "postcss": "^8.1.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1936,6 +2136,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bl/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/bl/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==", + "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/bl/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==", + "license": "MIT" + }, + "node_modules/bl/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2055,6 +2307,24 @@ "node": ">= 0.8" } }, + "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==", + "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", @@ -2123,6 +2393,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2506,6 +2782,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2570,6 +2852,18 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2579,6 +2873,57 @@ "node": ">= 12" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -2619,6 +2964,40 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "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==", + "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/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2659,6 +3038,18 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dotenv": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "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", @@ -2679,6 +3070,16 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2706,6 +3107,83 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2751,6 +3229,35 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -3072,6 +3579,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -3133,11 +3646,25 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -3170,8 +3697,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -3223,6 +3749,15 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3303,6 +3838,21 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3319,6 +3869,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", @@ -3369,6 +3928,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3400,6 +3986,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3456,6 +4071,32 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3524,6 +4165,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3542,6 +4199,43 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3551,6 +4245,33 @@ "node": ">=4" } }, + "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==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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", @@ -3605,6 +4326,20 @@ "node": ">= 0.8" } }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3664,6 +4399,20 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3673,6 +4422,57 @@ "node": ">= 0.10" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3685,6 +4485,34 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -3700,6 +4528,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "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", @@ -3709,6 +4570,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3718,6 +4594,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3730,6 +4624,30 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3739,12 +4657,182 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -3792,6 +4880,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -3810,11 +4904,16 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -3822,6 +4921,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3834,6 +4939,21 @@ "node": ">=6" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3932,7 +5052,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -4102,6 +5221,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4169,6 +5303,15 @@ "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -4213,6 +5356,35 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/node-telegram-bot-api": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.66.0.tgz", + "integrity": "sha512-s4Hrg5q+VPl4/tJVG++pImxF6eb8tNJNj4KnDqAOKL6zGU34lo9RXmyAN158njwGN+v8hdNf8s9fWIYW9hPb5A==", + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.1", + "@cypress/request-promise": "^5.0.0", + "array.prototype.findindex": "^2.0.2", + "bl": "^1.2.3", + "debug": "^3.2.7", + "eventemitter3": "^3.0.0", + "file-type": "^3.9.0", + "mime": "^1.6.0", + "pump": "^2.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/node-telegram-bot-api/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -4273,6 +5445,16 @@ "node": ">=0.10.0" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4302,6 +5484,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4314,6 +5525,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4331,6 +5551,23 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4440,6 +5677,12 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4476,6 +5719,15 @@ "node": ">= 6" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -4628,6 +5880,12 @@ "node": ">= 0.8.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==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4641,6 +5899,18 @@ "node": ">= 0.10" } }, + "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/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -4648,11 +5918,20 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -4672,6 +5951,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4815,6 +6100,178 @@ "node": ">=8.10.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/request/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "peer": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "peer": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4825,6 +6282,12 @@ "node": ">=0.10.0" } }, + "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/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4929,6 +6392,25 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4949,6 +6431,39 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5035,6 +6550,52 @@ "node": ">= 0.8.0" } }, + "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==", + "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/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5200,6 +6761,31 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -5209,6 +6795,28 @@ "node": ">= 0.8" } }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5285,6 +6893,62 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -5444,6 +7108,24 @@ "node": ">=0.8" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5484,6 +7166,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5522,9 +7216,26 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5550,6 +7261,80 @@ "node": ">= 0.6" } }, + "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==", + "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/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -5592,6 +7377,24 @@ } } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -5605,6 +7408,15 @@ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "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/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5648,11 +7460,20 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5667,6 +7488,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5676,6 +7506,20 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vite": { "version": "5.4.8", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", @@ -5775,6 +7619,91 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5905,6 +7834,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", diff --git a/package.json b/package.json index 1743ab9..aa1f09d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lawn-mowing-scheduler", "private": true, - "version": "0.6.2", + "version": "0.7.0", "type": "module", "scripts": { "dev": "concurrently \"npm run server\" \"npm run client\"", @@ -16,9 +16,13 @@ "dependencies": { "@libsql/client": "^0.4.0", "cors": "^2.8.5", + "dotenv": "^17.2.0", "express": "^4.18.2", + "framer-motion": "^12.23.12", "lucide-react": "^0.344.0", - "multer": "^2.0.1", + "multer": "2.0.1", + "node-cron": "^4.2.1", + "node-telegram-bot-api": "^0.66.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.0" diff --git a/server/index.js b/server/index.js index d68b4a0..0ac683a 100644 --- a/server/index.js +++ b/server/index.js @@ -5,6 +5,13 @@ import multer from 'multer'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; +import cron from 'node-cron'; +import { checkMowingReminders, sendWeeklyReport, testTelegramConnection } from './telegram.js'; +import dotenv from 'dotenv'; + +dotenv.config({ + path: process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development', +}); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -106,6 +113,31 @@ await db.execute(` ) `); +// Create season settings table +await db.execute(` + CREATE TABLE IF NOT EXISTS season_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + startMonth INTEGER NOT NULL DEFAULT 5, + startDay INTEGER NOT NULL DEFAULT 1, + endMonth INTEGER NOT NULL DEFAULT 9, + endDay INTEGER NOT NULL DEFAULT 30, + isActive BOOLEAN DEFAULT 1, + createdAt TEXT DEFAULT CURRENT_TIMESTAMP, + updatedAt TEXT DEFAULT CURRENT_TIMESTAMP + ) +`); + +// Insert default season settings if table is empty +const seasonCountResult = await db.execute('SELECT COUNT(*) as count FROM season_settings'); +const seasonCount = seasonCountResult.rows[0].count; + +if (seasonCount === 0) { + await db.execute({ + sql: 'INSERT INTO season_settings (startMonth, startDay, endMonth, endDay, isActive) VALUES (?, ?, ?, ?, ?)', + args: [5, 1, 9, 30, 1] // May 1 to September 30, active + }); +} + // Add new columns to existing zones table if they don't exist try { await db.execute(`ALTER TABLE zones ADD COLUMN nextMowDate TEXT`); @@ -218,8 +250,54 @@ if (count === 0) { } } +// Helper function to check if we're currently in mowing season +async function isInMowingSeason() { + try { + const result = await db.execute('SELECT * FROM season_settings WHERE id = 1'); + if (result.rows.length === 0) return true; // Default to always in season if no settings + + const settings = result.rows[0]; + if (!settings.isActive) return true; // If season is disabled, always in season + + const now = new Date(); + const currentMonth = now.getMonth() + 1; + const currentDay = now.getDate(); + + const startDate = new Date(now.getFullYear(), settings.startMonth - 1, settings.startDay); + const endDate = new Date(now.getFullYear(), settings.endMonth - 1, settings.endDay); + const currentDate = new Date(now.getFullYear(), currentMonth - 1, currentDay); + + if (startDate <= endDate) { + // Same year season (e.g., May to September) + return currentDate >= startDate && currentDate <= endDate; + } else { + // Cross-year season (e.g., November to March) + return currentDate >= startDate || currentDate <= endDate; + } + } catch (error) { + console.error('Error checking mowing season:', error); + return true; // Default to in season on error + } +} + + // Helper function to calculate zone status -function calculateZoneStatus(zone) { +async function calculateZoneStatus(zone) { + // First check if we're in mowing season + const inSeason = await isInMowingSeason(); + if (!inSeason) { + return { + ...zone, + daysSinceLastMow: null, + daysUntilNext: null, + status: 'off-season', + isOverdue: false, + isDueToday: false, + isNew: false, + isOffSeason: true + }; + } + const today = new Date(); // For new zones that haven't been mowed yet @@ -231,7 +309,8 @@ function calculateZoneStatus(zone) { status: 'new', isOverdue: false, isDueToday: false, - isNew: true + isNew: true, + isOffSeason: false }; } @@ -240,32 +319,37 @@ function calculateZoneStatus(zone) { let daysUntilNext; let status = 'ok'; - - if (zone.scheduleType === 'specific' && zone.nextMowDate) { + + // Always check for nextMowDate first (this could be set by trimming or specific scheduling) + if (zone.nextMowDate) { // Specific date scheduling const nextMowDate = new Date(zone.nextMowDate); daysUntilNext = Math.floor((nextMowDate - today) / (1000 * 60 * 60 * 24)); - } else { + } else if (zone.scheduleType === 'interval' && zone.intervalDays) { // Interval-based scheduling daysUntilNext = zone.intervalDays - daysSinceLastMow; + } else { + // No valid scheduling information + daysUntilNext = null; } - - if (daysUntilNext < 0) { + + if (daysUntilNext !== null && daysUntilNext < 0) { status = 'overdue'; - } else if (daysUntilNext <= 0) { + } else if (daysUntilNext !== null && daysUntilNext <= 0) { status = 'due'; - } else if (daysUntilNext <= 1) { + } else if (daysUntilNext !== null && daysUntilNext <= 1) { status = 'due'; } - + return { ...zone, daysSinceLastMow, daysUntilNext, status, - isOverdue: daysUntilNext < 0, - isDueToday: daysUntilNext <= 0 && daysUntilNext >= -1, - isNew: false + isOverdue: daysUntilNext !== null && daysUntilNext < 0, + isDueToday: daysUntilNext !== null && daysUntilNext <= 0 && daysUntilNext >= -1, + isNew: false, + isOffSeason: false }; } @@ -346,7 +430,7 @@ app.get('/api/zones', async (req, res) => { area: row.area || 0, createdAt: row.createdAt })); - const zonesWithStatus = zones.map(calculateZoneStatus); + const zonesWithStatus = await Promise.all(zones.map(calculateZoneStatus)); res.json(zonesWithStatus); } catch (error) { res.status(500).json({ error: error.message }); @@ -672,8 +756,8 @@ app.put('/api/zones/:id', upload.single('image'), async (req, res) => { area: updatedResult.rows[0].area || 0, createdAt: updatedResult.rows[0].createdAt }; - - const zoneWithStatus = calculateZoneStatus(updatedZone); + + const zoneWithStatus = await calculateZoneStatus(zone); res.json(zoneWithStatus); } catch (error) { res.status(500).json({ error: error.message }); @@ -766,15 +850,15 @@ app.post('/api/zones/:id/mow', async (req, res) => { area: updatedResult.rows[0].area || 0, createdAt: updatedResult.rows[0].createdAt }; - - const zoneWithStatus = calculateZoneStatus(updatedZone); + + const zoneWithStatus = await calculateZoneStatus(zone); res.json(zoneWithStatus); } catch (error) { res.status(500).json({ error: error.message }); } }); -// Trimming endpoint (doesn't update zone schedule) +// Trimming endpoint (update zone schedule by the week) app.post('/api/zones/:id/trim', async (req, res) => { try { const { notes, duration, weather, mowerId } = req.body; @@ -789,15 +873,48 @@ app.post('/api/zones/:id/trim', async (req, res) => { if (zoneResult.rows.length === 0) { return res.status(404).json({ error: 'Zone not found' }); } - + + const zone = zoneResult.rows[0]; + + // Update the zone's next mowing date by adding one week + // This applies the rule that trimming delays mowing by a week + let updatedNextMowDate = null; + + if (zone.scheduleType === 'interval' && zone.intervalDays) { + // For interval scheduling, calculate new next mow date + const baseDate = zone.lastMowedDate ? new Date(zone.lastMowedDate) : new Date(); + const originalNextMow = new Date(baseDate.getTime() + (zone.intervalDays * 24 * 60 * 60 * 1000)); + updatedNextMowDate = new Date(originalNextMow.getTime() + (7 * 24 * 60 * 60 * 1000)).toISOString(); + + // Update the zone's nextMowDate + await db.execute({ + sql: 'UPDATE zones SET nextMowDate = ? WHERE id = ?', + args: [updatedNextMowDate, req.params.id] + }); + } else if (zone.scheduleType === 'specific' && zone.nextMowDate) { + // For specific date scheduling, push the date forward by one week + const currentNextMow = new Date(zone.nextMowDate); + updatedNextMowDate = new Date(currentNextMow.getTime() + (7 * 24 * 60 * 60 * 1000)).toISOString(); + + // Update the zone's nextMowDate + await db.execute({ + sql: 'UPDATE zones SET nextMowDate = ? WHERE id = ?', + args: [updatedNextMowDate, req.params.id] + }); + } + // Add to mowing history with trimming activity type // Note: We don't update the zone's lastMowedDate or nextMowDate for trimming await db.execute({ sql: 'INSERT INTO mowing_history (zoneId, mowerId, mowedDate, notes, duration, weather, activityType) VALUES (?, ?, ?, ?, ?, ?, ?)', args: [req.params.id, mowerId || null, today, notes || null, duration || null, weather || null, 'trimming'] }); - - res.json({ success: true, message: 'Trimming recorded successfully' }); + + res.json({ + success: true, + message: 'Trimming recorded successfully. Next mowing date has been delayed by one week.', + updatedNextMowDate + }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -859,6 +976,32 @@ app.post('/api/zones/bulk-mow', async (req, res) => { } await db.execute({ sql, args }); + } else if (activityType === 'trimming') { + // For trimming, delay the next mowing date by one week + let updatedNextMowDate = null; + + if (zone.scheduleType === 'interval' && zone.intervalDays) { + // For interval scheduling, calculate new next mow date + const baseDate = zone.lastMowedDate ? new Date(zone.lastMowedDate) : new Date(); + const originalNextMow = new Date(baseDate.getTime() + (zone.intervalDays * 24 * 60 * 60 * 1000)); + updatedNextMowDate = new Date(originalNextMow.getTime() + (7 * 24 * 60 * 60 * 1000)).toISOString(); + + // Update the zone's nextMowDate + await db.execute({ + sql: 'UPDATE zones SET nextMowDate = ? WHERE id = ?', + args: [updatedNextMowDate, zone.id] + }); + } else if (zone.scheduleType === 'specific' && zone.nextMowDate) { + // For specific date scheduling, push the date forward by one week + const currentNextMow = new Date(zone.nextMowDate); + updatedNextMowDate = new Date(currentNextMow.getTime() + (7 * 24 * 60 * 60 * 1000)).toISOString(); + + // Update the zone's nextMowDate + await db.execute({ + sql: 'UPDATE zones SET nextMowDate = ? WHERE id = ?', + args: [updatedNextMowDate, zone.id] + }); + } } // Add to mowing history with session ID @@ -869,8 +1012,8 @@ app.post('/api/zones/bulk-mow', async (req, res) => { } res.json({ - success: true, - message: `Successfully recorded ${activityType} session for ${zones.length} zones`, + success: true, + message: `Successfully recorded ${activityType} session for ${zones.length} zones${activityType === 'trimming' ? '. Next mowing dates have been delayed by one week.' : ''}`, sessionId, zonesUpdated: zones.length }); @@ -891,7 +1034,147 @@ app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); +// Telegram test endpoint +app.post('/api/telegram/test', async (req, res) => { + try { + const result = await testTelegramConnection(); + res.json(result); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Manual trigger for mowing reminders +app.post('/api/telegram/check-reminders', async (req, res) => { + try { + await checkMowingReminders(); + res.json({ success: true, message: 'Mowing reminders checked and sent if needed' }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Manual trigger for weekly report +app.post('/api/telegram/weekly-report', async (req, res) => { + try { + await sendWeeklyReport(); + res.json({ success: true, message: 'Weekly report sent successfully' }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Season settings routes +app.get('/api/season', async (req, res) => { + try { + const result = await db.execute('SELECT * FROM season_settings WHERE id = 1'); + if (result.rows.length === 0) { + // Return default settings if none exist + res.json({ + startMonth: 5, + startDay: 1, + endMonth: 9, + endDay: 30, + isActive: true + }); + } else { + const settings = result.rows[0]; + res.json({ + id: settings.id, + startMonth: settings.startMonth, + startDay: settings.startDay, + endMonth: settings.endMonth, + endDay: settings.endDay, + isActive: settings.isActive, + createdAt: settings.createdAt, + updatedAt: settings.updatedAt + }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.put('/api/season', async (req, res) => { + try { + const { startMonth, startDay, endMonth, endDay, isActive } = req.body; + + // Check if settings exist + const existingResult = await db.execute('SELECT * FROM season_settings WHERE id = 1'); + + if (existingResult.rows.length === 0) { + // Create new settings + const result = await db.execute({ + sql: 'INSERT INTO season_settings (startMonth, startDay, endMonth, endDay, isActive) VALUES (?, ?, ?, ?, ?)', + args: [startMonth, startDay, endMonth, endDay, isActive] + }); + + const newSettings = await db.execute({ + sql: 'SELECT * FROM season_settings WHERE id = ?', + args: [result.lastInsertRowid] + }); + + res.json({ + id: newSettings.rows[0].id, + startMonth: newSettings.rows[0].startMonth, + startDay: newSettings.rows[0].startDay, + endMonth: newSettings.rows[0].endMonth, + endDay: newSettings.rows[0].endDay, + isActive: newSettings.rows[0].isActive, + createdAt: newSettings.rows[0].createdAt, + updatedAt: newSettings.rows[0].updatedAt + }); + } else { + // Update existing settings + await db.execute({ + sql: 'UPDATE season_settings SET startMonth = ?, startDay = ?, endMonth = ?, endDay = ?, isActive = ?, updatedAt = CURRENT_TIMESTAMP WHERE id = 1', + args: [startMonth, startDay, endMonth, endDay, isActive] + }); + + const updatedSettings = await db.execute('SELECT * FROM season_settings WHERE id = 1'); + + res.json({ + id: updatedSettings.rows[0].id, + startMonth: updatedSettings.rows[0].startMonth, + startDay: updatedSettings.rows[0].startDay, + endMonth: updatedSettings.rows[0].endMonth, + endDay: updatedSettings.rows[0].endDay, + isActive: updatedSettings.rows[0].isActive, + createdAt: updatedSettings.rows[0].createdAt, + updatedAt: updatedSettings.rows[0].updatedAt + }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + app.listen(PORT, '0.0.0.0', () => { console.log(`Server running on http://0.0.0.0:${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); + + // Setup cron jobs for Telegram notifications + if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID) { + console.log('Setting up Telegram notifications...'); + + // Daily mowing reminders at 8:00 AM + cron.schedule('0 8 * * *', () => { + console.log('Running daily mowing reminder check...'); + checkMowingReminders(); + }, { + timezone: 'Europe/Moscow' // Adjust timezone as needed + }); + + // Weekly report every Sunday at 9:00 AM + cron.schedule('0 9 * * 0', () => { + console.log('Sending weekly lawn care report...'); + sendWeeklyReport(); + }, { + timezone: 'Europe/Moscow' // Adjust timezone as needed + }); + + console.log('Telegram notifications scheduled successfully'); + } else { + console.log('Telegram notifications not configured (missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID)'); + } }); \ No newline at end of file diff --git a/server/telegram.js b/server/telegram.js new file mode 100644 index 0000000..8297670 --- /dev/null +++ b/server/telegram.js @@ -0,0 +1,272 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { createClient } from '@libsql/client'; + +// Initialize Telegram bot +const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; +const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID; + +let bot = null; + +if (TELEGRAM_BOT_TOKEN) { + bot = new TelegramBot(TELEGRAM_BOT_TOKEN, { polling: false }); +} + +// Initialize database connection +const db = createClient({ + url: 'file:lawn_scheduler.db' +}); + +// Helper function to calculate zone status +function calculateZoneStatus(zone) { + const today = new Date(); + + if (!zone.lastMowedDate) { + return { + ...zone, + daysSinceLastMow: null, + daysUntilNext: null, + status: 'new', + isOverdue: false, + isDueToday: false, + isNew: true + }; + } + + const lastMowed = new Date(zone.lastMowedDate); + const daysSinceLastMow = Math.floor((today - lastMowed) / (1000 * 60 * 60 * 24)); + + let daysUntilNext; + let status = 'ok'; + + if (zone.scheduleType === 'specific' && zone.nextMowDate) { + const nextMowDate = new Date(zone.nextMowDate); + daysUntilNext = Math.floor((nextMowDate - today) / (1000 * 60 * 60 * 24)); + } else { + daysUntilNext = zone.intervalDays - daysSinceLastMow; + } + + if (daysUntilNext < 0) { + status = 'overdue'; + } else if (daysUntilNext <= 0) { + status = 'due'; + } else if (daysUntilNext <= 1) { + status = 'due'; + } + + return { + ...zone, + daysSinceLastMow, + daysUntilNext, + status, + isOverdue: daysUntilNext < 0, + isDueToday: daysUntilNext <= 0 && daysUntilNext >= -1, + isNew: false + }; +} + +// Send Telegram message +export async function sendTelegramMessage(message) { + if (!bot || !TELEGRAM_CHAT_ID) { + console.log('Telegram not configured. Message would be:', message); + return false; + } + + try { + await bot.sendMessage(TELEGRAM_CHAT_ID, message, { parse_mode: 'Markdown' }); + console.log('Telegram message sent successfully'); + return true; + } catch (error) { + console.error('Failed to send Telegram message:', error); + return false; + } +} + +// Check for zones that need mowing and send notifications +export async function checkMowingReminders() { + try { + // First check if we're in mowing season + const seasonResult = await db.execute('SELECT * FROM season_settings WHERE id = 1'); + if (seasonResult.rows.length > 0) { + const settings = seasonResult.rows[0]; + if (settings.isActive) { + const now = new Date(); + const currentMonth = now.getMonth() + 1; + const currentDay = now.getDate(); + + const startDate = new Date(now.getFullYear(), settings.startMonth - 1, settings.startDay); + const endDate = new Date(now.getFullYear(), settings.endMonth - 1, settings.endDay); + const currentDate = new Date(now.getFullYear(), currentMonth - 1, currentDay); + + let inSeason; + if (startDate <= endDate) { + inSeason = currentDate >= startDate && currentDate <= endDate; + } else { + inSeason = currentDate >= startDate || currentDate <= endDate; + } + + if (!inSeason) { + console.log('Not in mowing season, skipping reminders'); + return; + } + } + } + + const result = await db.execute('SELECT * FROM zones ORDER BY name'); + const zones = result.rows.map(row => ({ + id: row.id, + name: row.name, + lastMowedDate: row.lastMowedDate, + intervalDays: row.intervalDays, + nextMowDate: row.nextMowDate, + scheduleType: row.scheduleType || 'interval', + area: row.area || 0, + })); + + const zonesWithStatus = zones.map(calculateZoneStatus); + + // Find zones that are due or overdue + const dueZones = zonesWithStatus.filter(zone => zone.isDueToday); + const overdueZones = zonesWithStatus.filter(zone => zone.isOverdue); + const newZones = zonesWithStatus.filter(zone => zone.isNew); + + if (dueZones.length > 0 || overdueZones.length > 0 || newZones.length > 0) { + let message = '🌱 *Lawn Mowing Reminder*\n\n'; + + if (overdueZones.length > 0) { + message += '🚨 *OVERDUE ZONES:*\n'; + overdueZones.forEach(zone => { + const daysOverdue = Math.abs(zone.daysUntilNext); + message += `• ${zone.name} - ${daysOverdue} day${daysOverdue > 1 ? 's' : ''} overdue\n`; + }); + message += '\n'; + } + + if (dueZones.length > 0) { + message += '⏰ *DUE TODAY:*\n'; + dueZones.forEach(zone => { + message += `• ${zone.name}\n`; + }); + message += '\n'; + } + + if (newZones.length > 0) { + message += '🆕 *NEW ZONES (Not yet mowed):*\n'; + newZones.forEach(zone => { + message += `• ${zone.name}\n`; + }); + message += '\n'; + } + + message += `Total zones needing attention: ${overdueZones.length + dueZones.length + newZones.length}`; + + await sendTelegramMessage(message); + } + } catch (error) { + console.error('Error checking mowing reminders:', error); + } +} + +// Generate and send weekly report +export async function sendWeeklyReport() { + try { + const result = await db.execute('SELECT * FROM zones ORDER BY name'); + const zones = result.rows.map(row => ({ + id: row.id, + name: row.name, + lastMowedDate: row.lastMowedDate, + intervalDays: row.intervalDays, + nextMowDate: row.nextMowDate, + scheduleType: row.scheduleType || 'interval', + area: row.area || 0, + })); + + const zonesWithStatus = zones.map(calculateZoneStatus); + + // Get weekly statistics + const weekAgo = new Date(); + weekAgo.setDate(weekAgo.getDate() - 7); + + const weeklyHistoryResult = await db.execute({ + sql: ` + SELECT + COUNT(*) as sessions, + SUM(duration) as totalMinutes, + SUM(z.area) as totalArea, + COUNT(DISTINCT mh.zoneId) as uniqueZones + FROM mowing_history mh + JOIN zones z ON mh.zoneId = z.id + WHERE mh.mowedDate >= ? + `, + args: [weekAgo.toISOString()] + }); + + const weeklyStats = weeklyHistoryResult.rows[0]; + + // Count zones by status + const overdueCount = zonesWithStatus.filter(zone => zone.isOverdue).length; + const dueCount = zonesWithStatus.filter(zone => zone.isDueToday).length; + const newCount = zonesWithStatus.filter(zone => zone.isNew).length; + const okCount = zonesWithStatus.filter(zone => zone.status === 'ok').length; + const totalArea = zones.reduce((sum, zone) => sum + zone.area, 0); + const mowedArea = zonesWithStatus + .filter(zone => zone.status === 'ok') + .reduce((sum, zone) => sum + zone.area, 0); + const mowedPercentage = totalArea > 0 ? (mowedArea / totalArea) * 100 : 0; + + let message = '📊 *Weekly Lawn Care Report*\n'; + message += `📅 ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}\n\n`; + + // Overall status + message += '🏡 *ZONE STATUS:*\n'; + message += `✅ Up to date: ${okCount} zones\n`; + message += `🆕 New zones: ${newCount} zones\n`; + message += `⏰ Due today: ${dueCount} zones\n`; + message += `🚨 Overdue: ${overdueCount} zones\n`; + message += `📐 Total area: ${totalArea.toLocaleString()} sq ft\n`; + message += `🌱 Mowed area: ${mowedPercentage.toFixed(1)}%\n\n`; + + // Weekly activity + message += '📈 *THIS WEEK\'S ACTIVITY:*\n'; + message += `🔄 Mowing sessions: ${weeklyStats.sessions || 0}\n`; + message += `⏱️ Total time: ${weeklyStats.totalMinutes ? Math.floor(weeklyStats.totalMinutes / 60) : 0}h ${weeklyStats.totalMinutes ? weeklyStats.totalMinutes % 60 : 0}m\n`; + message += `📐 Area mowed: ${(weeklyStats.totalArea || 0).toLocaleString()} sq ft\n`; + message += `🎯 Zones maintained: ${weeklyStats.uniqueZones || 0}\n\n`; + + // Zones needing attention + const needsAttention = [...zonesWithStatus.filter(zone => zone.isOverdue || zone.isDueToday || zone.isNew)]; + if (needsAttention.length > 0) { + message += '⚠️ *NEEDS ATTENTION:*\n'; + needsAttention.forEach(zone => { + let status = ''; + if (zone.isOverdue) { + status = `🚨 ${Math.abs(zone.daysUntilNext)} days overdue`; + } else if (zone.isDueToday) { + status = '⏰ Due today'; + } else if (zone.isNew) { + status = '🆕 Not yet mowed'; + } + message += `• ${zone.name} - ${status}\n`; + }); + } else { + message += '🎉 *All zones are up to date!*\n'; + } + + await sendTelegramMessage(message); + } catch (error) { + console.error('Error generating weekly report:', error); + } +} + +// Test Telegram connection +export async function testTelegramConnection() { + if (!bot || !TELEGRAM_CHAT_ID) { + return { success: false, message: 'Telegram not configured' }; + } + + try { + await sendTelegramMessage('🤖 *Lawn Care Manager*\n\nTelegram notifications are working correctly!'); + return { success: true, message: 'Test message sent successfully' }; + } catch (error) { + return { success: false, message: error.message }; + } +} \ No newline at end of file diff --git a/src/components/BulkMowingModal.tsx b/src/components/BulkMowingModal.tsx index 9e401fb..75c9090 100644 --- a/src/components/BulkMowingModal.tsx +++ b/src/components/BulkMowingModal.tsx @@ -152,7 +152,7 @@ const BulkMowingModal: React.FC = ({

{activityType === 'trimming' - ? 'Выберите несколько зон, обрезанных за один сеанс (не сбрасывает расписание скашивания).' + ? 'Выберите несколько зон, обрезанных за один сеанс (сдвиг скашивания на одну неделю в каждой зоне).' : 'Выберите несколько зон, скошенных за один сеанс' }

@@ -203,7 +203,7 @@ const BulkMowingModal: React.FC = ({ {formData.selectedZoneIds.length > 0 && (

- Selected: {selectedZones.map(z => z.name).join(', ')} + Выбрано: {selectedZones.map(z => z.name).join(', ')} (Общая площадь: {totalSelectedArea.toLocaleString()} м2) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 9fa8148..56fe0be 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Plus, Filter, Calendar, Scissors, AlertTriangle, Square, CheckCircle, Clock, Map, History } from './Icons'; +import { Plus, Filter, Calendar, Scissors, AlertTriangle, Square, CheckCircle, Clock, Map, History, Zap, Send, Settings, X } from './Icons'; import { Zone, MowingFormData, BulkMowingFormData } from '../types/zone'; import { api } from '../services/api'; import ZoneCard from './ZoneCard'; @@ -8,6 +8,8 @@ import SitePlan from './SitePlan'; import HistoryView from './HistoryView'; import MowingModal from './MowingModal'; import BulkMowingModal from './BulkMowingModal'; +import TelegramSettings from './TelegramSettings'; +import SeasonSettingsComponent from './SeasonSettings'; type FilterType = 'all' | 'due' | 'overdue' | 'new'; type ViewType = 'dashboard' | 'sitePlan' | 'history'; @@ -26,9 +28,12 @@ const Dashboard: React.FC = () => { const [showBulkTrimmingModal, setShowBulkTrimmingModal] = useState(false); const [mowingZone, setMowingZone] = useState(null); const [trimmingZone, setTrimmingZone] = useState(null); + const [showTelegramSettings, setShowTelegramSettings] = useState(false); + const [showSeasonSettings, setShowSeasonSettings] = useState(false); const [loading, setLoading] = useState(true); const [mowingLoading, setMowingLoading] = useState(false); const [trimmingLoading, setTrimmingLoading] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(false); useEffect(() => { loadZones(); @@ -94,13 +99,14 @@ const Dashboard: React.FC = () => { const handleTrimmingSubmit = async (data: MowingFormData) => { if (!trimmingZone) return; - + setTrimmingLoading(true); try { await api.markAsTrimmed(trimmingZone.id, data); setShowTrimmingModal(false); setTrimmingZone(null); - // Note: Don't reload zones since trimming doesn't affect mowing schedule + // Перезагрузка зон, поскольку подравнивание теперь влияет на график скашивания (задержки на 1 неделю) + loadZones(); } catch (error) { console.error('Failed to mark as trimmed:', error); } finally { @@ -125,7 +131,8 @@ const Dashboard: React.FC = () => { try { await api.bulkMarkAsMowed(data); setShowBulkTrimmingModal(false); - // Note: Don't reload zones since trimming doesn't affect mowing schedule + // Перезагрузка зон, поскольку подравнивание теперь влияет на график скашивания (задержки на 1 неделю) + loadZones(); } catch (error) { console.error('Failed to record bulk trimming session:', error); } finally { @@ -170,12 +177,12 @@ const Dashboard: React.FC = () => { const newCount = zones.filter(zone => zone.isNew).length; const okCount = zones.filter(zone => zone.status === 'ok').length; const totalArea = zones.reduce((sum, zone) => sum + zone.area, 0); - + // Calculate mowed vs remaining area based on status const mowedArea = zones - .filter(zone => zone.status === 'ok') - .reduce((sum, zone) => sum + zone.area, 0); - + .filter(zone => zone.status === 'ok') + .reduce((sum, zone) => sum + zone.area, 0); + const mowedPercentage = totalArea > 0 ? (mowedArea / totalArea) * 100 : 0; // Check if there are zones that need mowing for bulk action @@ -185,299 +192,426 @@ const Dashboard: React.FC = () => { if (loading) { return ( -

-
-
+
+
+
); } return ( -
-
- {/* Header */} -
-
-
-

+
+
+ {/* Mobile sidebar overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} +
+
+
- Менеджер по уходу за газоном -

-

Следите за своим графиком стрижки газона

+

Уход за газоном

+
+
-
- {/* View Toggle */} -
+ + +
+ + {/* Main content */} +
+ {/* Top bar */} +
+
+
+ +
+

+ {view === 'dashboard' && 'Dashboard'} + {view === 'sitePlan' && 'Site Plan'} + {view === 'history' && 'History'} +

+

+ {view === 'dashboard' && 'Keep track of your lawn mowing schedule'} + {view === 'sitePlan' && 'Visualize your property zones'} + {view === 'history' && 'Track your lawn care activities'} +

+
+
+
+
+ + {/* Main content area */} +
+
+ {view === 'history' ? ( + + ) : view === 'sitePlan' ? ( + + ) : ( + <> + {/* Stats Cards */} +
+
+
+
+

Зон

+

{zones.length}

+
+ +
+
+ +
+
+
+

Площадь

+

{totalArea.toLocaleString()}

+

sq ft

+
+ +
+
+ +
+
+
+

Актуально

+

{okCount}

+

zones maintained

+
+ +
+
+{/* +
+
+
+

New Zones

+

{newCount}

+

not yet mowed

+
+ +
+
+*/} +
+
+
+

Внимание!

+

{dueCount + overdueCount + newCount}

+

zones to mow

+
+ +
+
+ +
+
+
+

Срок-сегодня

+

{dueCount}

+
+ +
+
+ +
+
+
+

Срок прошел

+

{overdueCount}

+
+ +
+
+
+ + {/* Progress Bar */} + {totalArea > 0 && ( +
+
+

Ход скашивания

+ + {mowedArea.toLocaleString()} / {totalArea.toLocaleString()} м2 + +
+
+
+
+
+ + + {mowedPercentage.toFixed(1)}% Завершено ({okCount} зон) + + + + {(100 - mowedPercentage).toFixed(1)}% Осталось ({dueCount + overdueCount + newCount} зон) + +
+
+ )} + + {/* Bulk Mowing Notice */} + {zonesNeedingMowing.length > 1 && ( +
+
+
+ +
+

+ Необходимо скосить или подравнять несколько зон +

+

+ Используйте массовые операции для нескольких зон, за один сеанс, с пропорциональным распределением времени +

+
+
+
+ + +
+
+
+ )} + + {/* Filter Buttons */} +
+
+ + Filter: +
+ {[ + { key: 'all' as FilterType, label: 'All Zones', count: zones.length }, + { key: 'new' as FilterType, label: 'New Zones', count: newCount }, + { key: 'due' as FilterType, label: 'Due Today', count: dueCount }, + { key: 'overdue' as FilterType, label: 'Overdue', count: overdueCount }, + ].map(({ key, label, count }) => ( + + ))} +
+
+
+ + {/* Zones Grid */} + {filteredZones.length === 0 ? ( +
+ +

No zones found

+

+ {filter === 'all' ? 'Get started by adding your first lawn zone.' : `No zones match the "${filter}" filter.`} +

+
+ ) : ( +
+ {filteredZones.map(zone => ( +
+ { + setEditingZone(zone); + setShowForm(true); + }} + onDelete={handleDeleteZone} + /> +
+ ))} +
+ )} + + )} +
- {view === 'history' ? ( - - ) : view === 'sitePlan' ? ( - - ) : ( - <> - {/* Stats Cards */} -
-
-
-
-

Кол-во зон

-

{zones.length}

-
- -
-
- -
-
-
-

Площадь, вся

-

{totalArea.toLocaleString()}

-

м2

-
- -
-
- -
-
-
-

Актуально

-

{okCount}

-

зон обслужено

-
- -
-
- -{/* -
-
-
-

Новые зоны

-

{newCount}

-

еще не косились

-
- -
-
-*/} - -
-
-
-

Внимание!

-

{dueCount + overdueCount + newCount}

-

зон для покоса

-
- -
-
- -
-
-
-

Срок - сегодня

-

{dueCount}

-
- -
-
- -
-
-
-

Срок прошел

-

{overdueCount}

-
- -
-
-
- - {/* Progress Bar */} - {totalArea > 0 && ( -
-
-

Ход скашивания

- - {mowedArea.toLocaleString()} / {totalArea.toLocaleString()} м2 - -
-
-
-
-
- - - {mowedPercentage.toFixed(1)}% Завершено ({okCount} зон) - - - - {(100 - mowedPercentage).toFixed(1)}% Осталось ({dueCount + overdueCount} зон) - -
-
- )} - - {/* Bulk Mowing Notice */} - {zonesNeedingMowing.length > 1 && ( -
-
-
- -
-

- Необходимо скосить или подравнять несколько зон -

-

- Используйте массовые операции для нескольких зон, за один сеанс, с пропорциональным распределением времени -

-
-
-
- - -
-
-
- )} - - {/* Filter Buttons */} -
-
- - Фильтр: -
- {[ - { key: 'all' as FilterType, label: 'Все зоны', count: zones.length }, - { key: 'new' as FilterType, label: 'Новые зоны', count: newCount }, - { key: 'due' as FilterType, label: 'Срок - сегодня', count: dueCount }, - { key: 'overdue' as FilterType, label: 'Срок прошел', count: overdueCount }, - ].map(({ key, label, count }) => ( - - ))} -
-
-
- - {/* Zones Grid */} - {filteredZones.length === 0 ? ( -
- -

Не найдено ни одной зоны

-

- {filter === 'all' ? 'Get started by adding your first lawn zone.' : `Нет подходящих зон для "${filter}" фильтра.`} -

-
- ) : ( -
- {filteredZones.map(zone => ( -
- { - setEditingZone(zone); - setShowForm(true); - }} - onDelete={handleDeleteZone} - /> -
- ))} -
- )} - - )} - {/* Zone Form Modal */} {showForm && ( - { - setShowForm(false); - setEditingZone(null); - }} - /> + { + setShowForm(false); + setEditingZone(null); + }} + /> )} {/* Single Zone Mowing Modal */} @@ -496,16 +630,16 @@ const Dashboard: React.FC = () => { {/* Single Zone Trimming Modal */} {showTrimmingModal && trimmingZone && ( - { - setShowTrimmingModal(false); - setTrimmingZone(null); - }} - loading={trimmingLoading} - /> + { + setShowTrimmingModal(false); + setTrimmingZone(null); + }} + loading={trimmingLoading} + /> )} {/* Bulk Mowing Modal */} {showBulkMowingModal && ( @@ -520,16 +654,29 @@ const Dashboard: React.FC = () => { {/* Bulk Trimming Modal */} {showBulkTrimmingModal && ( - setShowBulkTrimmingModal(false)} - loading={trimmingLoading} - /> + setShowBulkTrimmingModal(false)} + loading={trimmingLoading} + /> + )} + + {/* Telegram Settings Modal */} + {showTelegramSettings && ( + setShowTelegramSettings(false)} + /> + )} + + {/* Season Settings Modal */} + {showSeasonSettings && ( + setShowSeasonSettings(false)} + /> )}
-
); }; diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 849c4f3..3df7234 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -186,8 +186,44 @@ export const Users: React.FC<{ className?: string }> = ({ className = "h-6 w-6" ); + export const Zap: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => ( +); + +export const Send: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => ( + + + + +); + +export const Snowflake: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => ( + + + + +); + +export const Sun: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => ( + + + + + + + + + + + +); + +export const Settings: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => ( + + + + ); \ No newline at end of file diff --git a/src/components/MowingModal.tsx b/src/components/MowingModal.tsx index faa345d..4c10a14 100644 --- a/src/components/MowingModal.tsx +++ b/src/components/MowingModal.tsx @@ -93,7 +93,7 @@ const MowingModal: React.FC = ({ {activityType === 'trimming' && (

- Подравнивание не приводит к изменению графика скашивания + Подравнивание сдвинет следующий покос на одну неделю

)}
diff --git a/src/components/SeasonSettings.tsx b/src/components/SeasonSettings.tsx new file mode 100644 index 0000000..7a1b889 --- /dev/null +++ b/src/components/SeasonSettings.tsx @@ -0,0 +1,275 @@ +import React, { useState, useEffect } from 'react'; +import { X, Sun, Snowflake, Settings } from './Icons'; +import { SeasonSettings } from '../types/zone'; +import { api } from '../services/api'; + +interface SeasonSettingsProps { + onClose: () => void; +} + +const SeasonSettingsComponent: React.FC = ({ onClose }) => { + const [settings, setSettings] = useState({ + startMonth: 5, + startDay: 1, + endMonth: 9, + endDay: 30, + isActive: true, + }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + loadSettings(); + }, []); + + const loadSettings = async () => { + try { + const data = await api.getSeasonSettings(); + setSettings(data); + } catch (error) { + console.error('Failed to load season settings:', error); + // Use default settings if none exist + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + setError(null); + + // Validation + if (settings.startMonth < 1 || settings.startMonth > 12 || + settings.endMonth < 1 || settings.endMonth > 12) { + setError('Months must be between 1 and 12'); + setSaving(false); + return; + } + + if (settings.startDay < 1 || settings.startDay > 31 || + settings.endDay < 1 || settings.endDay > 31) { + setError('Days must be between 1 and 31'); + setSaving(false); + return; + } + + try { + await api.updateSeasonSettings(settings); + onClose(); + } catch (error) { + setError(error instanceof Error ? error.message : 'Failed to save settings'); + } finally { + setSaving(false); + } + }; + + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + const formatDate = (month: number, day: number) => { + return `${monthNames[month - 1]} ${day}`; + }; + + const isCurrentlyInSeason = () => { + const now = new Date(); + const currentMonth = now.getMonth() + 1; + const currentDay = now.getDate(); + + const startDate = new Date(now.getFullYear(), settings.startMonth - 1, settings.startDay); + const endDate = new Date(now.getFullYear(), settings.endMonth - 1, settings.endDay); + const currentDate = new Date(now.getFullYear(), currentMonth - 1, currentDay); + + if (startDate <= endDate) { + // Same year season + return currentDate >= startDate && currentDate <= endDate; + } else { + // Cross-year season (e.g., Nov to Mar) + return currentDate >= startDate || currentDate <= endDate; + } + }; + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + return ( +
+
+
+

+ + Mowing Season Settings +

+ +
+ +
+ {error && ( +
+ {error} +
+ )} + + {/* Season Status */} +
+
+ {settings.isActive && isCurrentlyInSeason() ? ( + + ) : ( + + )} +
+

+ {settings.isActive && isCurrentlyInSeason() ? 'In Season' : 'Off Season'} +

+

+ {settings.isActive && isCurrentlyInSeason() + ? 'Mowing schedules are active' + : 'All zones are considered maintained' + } +

+
+
+
+ + {/* Enable/Disable Season */} +
+ +
+ + {settings.isActive && ( + <> + {/* Season Start */} +
+ +
+
+ + +
+
+ + setSettings(prev => ({ ...prev, startDay: parseInt(e.target.value) || 1 }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent" + /> +
+
+

+ Season starts: {formatDate(settings.startMonth, settings.startDay)} +

+
+ + {/* Season End */} +
+ +
+
+ + +
+
+ + setSettings(prev => ({ ...prev, endDay: parseInt(e.target.value) || 1 }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent" + /> +
+
+

+ Season ends: {formatDate(settings.endMonth, settings.endDay)} +

+
+ + {/* Season Info */} +
+

How it works:

+
    +
  • • During season: Normal mowing schedules apply
  • +
  • • Off season: All zones show as "maintained"
  • +
  • • Mowing/trimming can still be recorded year-round
  • +
  • • Notifications only sent during active season
  • +
+
+ + )} +
+ +
+ + +
+
+
+ ); +}; + +export default SeasonSettingsComponent; \ No newline at end of file diff --git a/src/components/TelegramSettings.tsx b/src/components/TelegramSettings.tsx new file mode 100644 index 0000000..1809519 --- /dev/null +++ b/src/components/TelegramSettings.tsx @@ -0,0 +1,236 @@ +import React, { useState } from 'react'; +import { X, Send, CheckCircle, AlertTriangle } from './Icons'; + +interface TelegramSettingsProps { + onClose: () => void; +} + +const TelegramSettings: React.FC = ({ onClose }) => { + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); + + const handleTestConnection = async () => { + setTesting(true); + setTestResult(null); + + try { + const response = await fetch('/api/telegram/test', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const result = await response.json(); + setTestResult(result); + } catch (error) { + setTestResult({ + success: false, + message: 'Failed to test connection: ' + (error instanceof Error ? error.message : 'Unknown error') + }); + } finally { + setTesting(false); + } + }; + + const handleSendTestReminder = async () => { + setTesting(true); + + try { + const response = await fetch('/api/telegram/check-reminders', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const result = await response.json(); + setTestResult(result); + } catch (error) { + setTestResult({ + success: false, + message: 'Failed to send reminder: ' + (error instanceof Error ? error.message : 'Unknown error') + }); + } finally { + setTesting(false); + } + }; + + const handleSendWeeklyReport = async () => { + setTesting(true); + + try { + const response = await fetch('/api/telegram/weekly-report', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const result = await response.json(); + setTestResult(result); + } catch (error) { + setTestResult({ + success: false, + message: 'Failed to send report: ' + (error instanceof Error ? error.message : 'Unknown error') + }); + } finally { + setTesting(false); + } + }; + + return ( +
+
+
+

+ + Telegram Notifications +

+ +
+ +
+ {/* Setup Instructions */} +
+

Setup Instructions

+
    +
  1. Create a new bot by messaging @BotFather on Telegram
  2. +
  3. Send /newbot and follow the instructions
  4. +
  5. Copy the bot token and add it to your .env file as TELEGRAM_BOT_TOKEN
  6. +
  7. Start a chat with your bot and send any message
  8. +
  9. Visit https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates to get your chat ID
  10. +
  11. Add the chat ID to your .env file as TELEGRAM_CHAT_ID
  12. +
  13. Restart the server to apply the changes
  14. +
+
+ + {/* Notification Schedule */} +
+

Notification Schedule

+
+
+
+ Daily Reminders: 8:00 AM - Zones that are due or overdue +
+
+
+ Weekly Report: Sunday 9:00 AM - Complete lawn status summary +
+
+
+ + {/* Test Buttons */} +
+

Test Notifications

+ +
+ + + + + +
+
+ + {/* Test Result */} + {testResult && ( +
+
+ {testResult.success ? ( + + ) : ( + + )} + + {testResult.success ? 'Success!' : 'Error'} + +
+

{testResult.message}

+
+ )} + + {/* Example Messages */} +
+

Example Messages

+ +
+
+

Daily Reminder:

+
+ 🌱 Lawn Mowing Reminder

+ 🚨 OVERDUE ZONES:
+ • Front Yard - 2 days overdue

+ ⏰ DUE TODAY:
+ • Back Garden

+ Total zones needing attention: 2 +
+
+ +
+

Weekly Report:

+
+ 📊 Weekly Lawn Care Report
+ 📅 Sunday, January 21, 2024

+ 🏡 ZONE STATUS:
+ ✅ Up to date: 2 zones
+ 🆕 New zones: 0 zones
+ ⏰ Due today: 1 zones
+ 🚨 Overdue: 0 zones
+ 📐 Total area: 580 sq ft
+ 🌱 Mowed area: 85.2%

+ 📈 THIS WEEK'S ACTIVITY:
+ 🔄 Mowing sessions: 3
+ ⏱️ Total time: 2h 15m
+ 📐 Area mowed: 1,160 sq ft
+ 🎯 Zones maintained: 3 +
+
+
+
+
+ +
+ +
+
+
+ ); +}; + +export default TelegramSettings; \ No newline at end of file diff --git a/src/components/ZoneCard.tsx b/src/components/ZoneCard.tsx index cb668c8..8f92d0c 100644 --- a/src/components/ZoneCard.tsx +++ b/src/components/ZoneCard.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Scissors, Edit, Trash2, Calendar, Camera, Square, Clock, AlertTriangle, Zap } from './Icons'; import { Zone } from '../types/zone'; +import {ImagePreview} from "./utils/ImagePreview.tsx"; interface ZoneCardProps { zone: Zone; @@ -28,6 +29,8 @@ const ZoneCard: React.FC = ({ return 'border-orange-500 bg-orange-50'; case 'new': return 'border-blue-500 bg-blue-50'; + case 'off-season': + return 'border-gray-500 bg-gray-50'; default: return 'border-green-500 bg-green-50'; } @@ -36,6 +39,8 @@ const ZoneCard: React.FC = ({ const getStatusText = (zone: Zone) => { if (zone.isNew) { return 'Еще не косилась'; + } else if (zone.isOffSeason) { + return 'Не сезон'; } else if (zone.isOverdue) { return `${Math.abs(zone.daysUntilNext!)} дней посроченно`; } else if (zone.isDueToday) { @@ -53,6 +58,8 @@ const ZoneCard: React.FC = ({ return 'text-orange-700'; case 'new': return 'text-blue-700'; + case 'off-season': + return 'text-gray-700'; default: return 'text-green-700'; } @@ -101,12 +108,11 @@ const ZoneCard: React.FC = ({ {/* Zone Image */} {zone.imagePath ? (
- {zone.name} -
) : (
@@ -148,9 +154,9 @@ const ZoneCard: React.FC = ({