diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index 292c70c..0000000
--- a/.babelrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "presets": [
- "@babel/preset-env",
- "@babel/preset-react",
- "@babel/preset-typescript"
- ],
- "plugins": ["react-refresh/babel"]
-}
diff --git a/.dockerignore b/.dockerignore
index 06067e9..0acd217 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,6 +1,110 @@
-db/*.sqlite3
-*.sqlite3
-*.sqlite3-shm
-*.sqlite3-wal
-certs/
-.DS_Store
\ No newline at end of file
+# ============================================================================
+# Optimized .dockerignore for multi-stage build
+# ============================================================================
+
+# Node modules (installed in container)
+**/node_modules/
+**/.npm
+
+# Development files
+*.log
+*.tmp
+*.swp
+*.swo
+*~
+**/.env.local
+**/.env.development
+**/.env.test
+
+# Git and version control
+.git/
+.gitignore
+.gitattributes
+.github/
+
+# Development databases
+**/db/*.sqlite3*
+**/*.sqlite3*
+**/db/*.db
+
+# IDE and editor files
+.vscode/
+.idea/
+*.sublime-*
+**/.editorconfig
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Test files
+test/
+spec/
+**/*.test.js
+**/*.spec.js
+**/jest.config.*
+**/vitest.config.*
+
+# Documentation and assets
+README.md
+LICENSE
+docs/
+screenshots/
+**/*.md
+
+# Build artifacts (built in container)
+dist/
+build/
+.next/
+.nuxt/
+coverage/
+.nyc_output/
+
+# Temporary and cache files
+tmp/
+temp/
+.cache/
+**/.cache
+**/npm-debug.log*
+**/yarn-debug.log*
+**/yarn-error.log*
+
+# Development only
+frontend.log
+server.log
+cookies.txt
+**/.eslintcache
+
+# Certificates (generated in container)
+**/certs/
+
+# Docker related (avoid recursion)
+Dockerfile*
+docker-compose*.yml
+.dockerignore
+
+# Development scripts not needed in production
+backend-express/scripts/
+backend-express/migrations/
+backend-express/seeders/
+
+# Additional exclusions for minimal image
+backend-express/test/
+backend-express/tests/
+**/node_modules/.cache/
+**/.git/
+**/.gitignore
+**/yarn.lock
+**/*.log
+**/*.tmp
+**/coverage/
+
+# Backup files
+**/*.bak
+**/*.backup
+**/*.orig
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 816b011..9ba04af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ node_modules
public/js/bundle.js
.aider*
+
+backend/coverage/
\ No newline at end of file
diff --git a/.rubocop.yml b/.rubocop.yml
deleted file mode 100644
index 18f1d6a..0000000
--- a/.rubocop.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-Metrics/ClassLength:
- Max: 500
-Metrics/BlockLength:
- Max: 50
-Metrics/MethodLength:
- Max: 50
-Style/Documentation:
- Enabled: false
diff --git a/Dockerfile b/Dockerfile
index eb02f4f..a3fb0cf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,108 +1,156 @@
-# Use a base image that supports both Node.js and Ruby
-FROM ruby:3.2.2-slim
+# ============================================================================
+# Ultra-optimized multi-stage build for minimal rootless Docker image
+# ============================================================================
-# Install Node.js and necessary packages
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends \
- build-essential \
- libsqlite3-dev \
- openssl \
- libffi-dev \
- libpq-dev \
- curl \
- gnupg2 \
- ca-certificates && \
- # Install Node.js 20
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
- apt-get install -y nodejs && \
- rm -rf /var/lib/apt/lists/* && \
- apt-get clean
+# Stage 1: Frontend Build Environment (optimized)
+FROM node:20-alpine AS frontend-builder
-WORKDIR /usr/src/app
+WORKDIR /app
-# Install Ruby dependencies first
-COPY Gemfile* ./
-RUN bundle config set --local deployment 'true' && \
- bundle config set --local without 'development test' && \
- bundle install --jobs 4 --retry 3
-
-# Install Node.js dependencies
+# Copy frontend package files
COPY package*.json ./
-COPY webpack.config.js ./
-COPY babel.config.js ./
-COPY tsconfig.json ./
-COPY postcss.config.js ./
-COPY tailwind.config.js ./
-RUN npm ci
+COPY webpack.config.js babel.config.js tsconfig.json postcss.config.js tailwind.config.js ./
-# Remove any existing development databases
-RUN rm -f db/development*
+# Install frontend dependencies (including dev deps for build)
+RUN npm install --ignore-scripts --no-audit --no-fund && \
+ npm cache clean --force && \
+ rm -rf ~/.npm
-# Copy application files
-COPY app/ app/
-COPY config/ config/
-COPY config.ru ./
-COPY Rakefile ./
-COPY app.rb ./
-COPY db/migrate/ db/migrate/
-COPY db/schema.rb db/schema.rb
+# Copy frontend source code
COPY frontend/ frontend/
COPY public/ public/
-COPY src/ src/
-# Create non-root user for security
-RUN useradd -m -U app && \
- chown -R app:app /usr/src/app
+# Build frontend assets with optimizations
+RUN NODE_ENV=production npm run build && \
+ # Remove source maps and dev artifacts
+ find dist -name "*.map" -delete && \
+ find dist -name "*.dev.*" -delete && \
+ # Compress built assets
+ find dist -type f \( -name "*.js" -o -name "*.css" -o -name "*.html" \) -exec gzip -9 -k {} \;
+# Stage 2: Backend Dependencies (ultra-minimal)
+FROM node:20-alpine AS backend-deps
+
+WORKDIR /app
+
+# Install build dependencies temporarily for native modules
+RUN apk add --no-cache --virtual .build-deps \
+ python3 \
+ make \
+ g++ \
+ sqlite-dev
+
+# Install only runtime dependencies for backend
+COPY backend/package*.json ./
+RUN npm install --production --no-audit --no-fund && \
+ npm cache clean --force && \
+ rm -rf ~/.npm /tmp/* && \
+ # Remove build dependencies after install
+ apk del .build-deps && \
+ # Remove unnecessary files from node_modules
+ find node_modules -name "*.md" -delete && \
+ find node_modules -name "*.txt" -delete && \
+ find node_modules -name "LICENSE*" -delete && \
+ find node_modules -name "CHANGELOG*" -delete && \
+ find node_modules -name "README*" -delete && \
+ find node_modules -name ".github" -type d -exec rm -rf {} + 2>/dev/null || true && \
+ find node_modules -name "test" -type d -exec rm -rf {} + 2>/dev/null || true && \
+ find node_modules -name "tests" -type d -exec rm -rf {} + 2>/dev/null || true && \
+ find node_modules -name "docs" -type d -exec rm -rf {} + 2>/dev/null || true && \
+ find node_modules -name "examples" -type d -exec rm -rf {} + 2>/dev/null || true
+
+# Stage 3: Test Stage (run tests before production)
+FROM node:20-alpine AS test
+
+WORKDIR /app
+
+# Install build dependencies for testing
+RUN apk add --no-cache --virtual .test-deps \
+ python3 \
+ make \
+ g++ \
+ sqlite-dev
+
+# Copy backend package files and install all dependencies (including dev)
+COPY backend/package*.json ./backend/
+RUN cd backend && npm install --no-audit --no-fund
+
+# Copy backend source code
+COPY backend/ ./backend/
+
+# Run tests
+RUN cd backend && npm test
+
+# Stage 4: Final Production Image (minimal base)
+FROM node:20-alpine AS production
+
+# Create non-root user first (before installing packages)
+RUN addgroup -g 1001 -S app && \
+ adduser -S app -u 1001 -G app
+
+# Install minimal runtime dependencies with size optimization
+RUN apk add --no-cache --virtual .runtime-deps \
+ sqlite \
+ openssl \
+ curl \
+ dumb-init && \
+ # Clean up package cache immediately
+ rm -rf /var/cache/apk/* /tmp/* && \
+ # Remove unnecessary files
+ rm -rf /usr/share/man /usr/share/doc /usr/share/info
+
+# Set working directory
+WORKDIR /app
+
+# Copy backend dependencies from deps stage (optimized)
+COPY --from=backend-deps --chown=app:app /app/node_modules ./backend/node_modules
+
+# Copy backend application code (exclude unnecessary files)
+COPY --chown=app:app backend/app.js ./backend/
+COPY --chown=app:app backend/package*.json ./backend/
+COPY --chown=app:app backend/config/ ./backend/config/
+COPY --chown=app:app backend/models/ ./backend/models/
+COPY --chown=app:app backend/routes/ ./backend/routes/
+COPY --chown=app:app backend/middleware/ ./backend/middleware/
+COPY --chown=app:app backend/services/ ./backend/services/
+
+# Copy minimal built frontend assets from builder stage
+COPY --from=frontend-builder --chown=app:app /app/dist ./backend/dist
+COPY --from=frontend-builder --chown=app:app /app/public/locales ./backend/dist/locales
+
+# Create ultra-minimal startup script (before switching to non-root user)
+RUN printf '#!/bin/sh\nset -e\ncd backend\nmkdir -p db certs\nDB_FILE="db/production.sqlite3"\n[ "$NODE_ENV" = "development" ] && DB_FILE="db/development.sqlite3"\nif [ ! -f "$DB_FILE" ]; then\n node -e "require(\\"./models\\").sequelize.sync({force:true}).then(()=>{console.log(\\"✅ DB ready\\");process.exit(0)}).catch(e=>{console.error(\\"❌\\",e.message);process.exit(1)})"\nelse\n node -e "require(\\"./models\\").sequelize.authenticate().then(()=>{console.log(\\"✅ DB OK\\");process.exit(0)}).catch(e=>{console.error(\\"❌\\",e.message);process.exit(1)})"\nfi\nif [ -n "$TUDUDI_USER_EMAIL" ]&&[ -n "$TUDUDI_USER_PASSWORD" ]; then\n node -e "const{User}=require(\\"./models\\");const bcrypt=require(\\"bcrypt\\");(async()=>{try{const[u,c]=await User.findOrCreate({where:{email:process.env.TUDUDI_USER_EMAIL},defaults:{email:process.env.TUDUDI_USER_EMAIL,password_digest:await bcrypt.hash(process.env.TUDUDI_USER_PASSWORD,10)}});console.log(c?\\"✅ User created\\":\\"ℹ️ User exists\\");process.exit(0)}catch(e){console.error(\\"❌\\",e.message);process.exit(1)}})();"||exit 1\nfi\n[ "$TUDUDI_INTERNAL_SSL_ENABLED" = "true" ]&&[ ! -f "certs/server.crt" ]&&openssl req -x509 -newkey rsa:2048 -keyout certs/server.key -out certs/server.crt -days 365 -nodes -subj "/CN=localhost" 2>/dev/null||true\nexec node app.js\n' > start.sh && chmod +x start.sh
+
+# Create necessary directories and final cleanup
+RUN mkdir -p ./backend/db ./backend/certs && \
+ chown -R app:app ./backend/db ./backend/certs ./start.sh && \
+ # Final size optimization - remove Node.js build tools and cache
+ apk del --no-cache .runtime-deps sqlite openssl curl && \
+ apk add --no-cache sqlite-libs openssl curl dumb-init && \
+ rm -rf /usr/local/lib/node_modules/npm/docs /usr/local/lib/node_modules/npm/man && \
+ rm -rf /root/.npm /tmp/* /var/tmp/* /var/cache/apk/*
+
+# Switch to non-root user
USER app
-# Expose ports for both frontend (8080) and backend (9292)
-EXPOSE 8080 9292
+# Expose port
+EXPOSE 3002
-# Set production environment variables
-ENV RACK_ENV=production \
- NODE_ENV=production \
+# Set optimized production environment variables
+ENV NODE_ENV=production \
+ PORT=3002 \
TUDUDI_INTERNAL_SSL_ENABLED=false \
- TUDUDI_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:9292,http://127.0.0.1:8080,http://127.0.0.1:9292,http://0.0.0.0:8080,http://0.0.0.0:9292" \
- LANG=C.UTF-8 \
- TZ=UTC
+ TUDUDI_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:3002,http://127.0.0.1:8080,http://127.0.0.1:3002" \
+ TUDUDI_SESSION_SECRET="" \
+ TUDUDI_USER_EMAIL="" \
+ TUDUDI_USER_PASSWORD="" \
+ DISABLE_TELEGRAM=false \
+ DISABLE_SCHEDULER=false
-# Generate SSL certificates if needed
-RUN mkdir -p certs && \
- if [ "$TUDUDI_INTERNAL_SSL_ENABLED" = "true" ]; then \
- openssl req -x509 -newkey rsa:4096 \
- -keyout certs/server.key -out certs/server.crt \
- -days 365 -nodes \
- -subj '/CN=localhost' \
- -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"; \
- fi
+# Minimal healthcheck
+HEALTHCHECK --interval=60s --timeout=3s --start-period=10s --retries=2 \
+ CMD curl -sf http://localhost:3002/api/health || exit 1
-# Add healthcheck for backend
-HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
- CMD curl -f http://localhost:9292/api/health || exit 1
-
-# Build production frontend assets
-RUN npm run build
-
-# Copy translation files to dist folder for production serving
-RUN cp -r public/locales dist/
-
-# Create startup script
-RUN echo '#!/bin/bash\n\
-set -e\n\
-\n\
-# Run database migrations\n\
-bundle exec rake db:migrate\n\
-\n\
-# Create user if it does not exist\n\
-if [ -n "$TUDUDI_USER_EMAIL" ] && [ -n "$TUDUDI_USER_PASSWORD" ]; then\n\
- echo "Creating user if it does not exist..."\n\
- echo "user = User.find_by(email: \"$TUDUDI_USER_EMAIL\") || User.create(email: \"$TUDUDI_USER_EMAIL\", password: \"$TUDUDI_USER_PASSWORD\"); puts \"User: #{user.email}\"" | bundle exec rake console\n\
-fi\n\
-\n\
-# Start backend with both API and static file serving\n\
-bundle exec puma -C app/config/puma.rb\n\
-' > start.sh && chmod +x start.sh
-
-# Run both services
-CMD ["./start.sh"]
\ No newline at end of file
+# Use dumb-init for proper signal handling
+ENTRYPOINT ["dumb-init", "--"]
+CMD ["/app/start.sh"]
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
deleted file mode 100644
index 2f2d0d7..0000000
--- a/Gemfile
+++ /dev/null
@@ -1,28 +0,0 @@
-source 'https://rubygems.org'
-
-gem 'puma'
-gem 'rack-protection', '~> 3.1.0'
-gem 'rake', '~> 13.0'
-gem 'rufus-scheduler', '~> 3.8.2'
-
-# DB
-gem 'sinatra-activerecord'
-gem 'sinatra-cross_origin'
-gem 'sinatra-namespace'
-gem 'sqlite3'
-
-# Authentication
-gem 'bcrypt', '~> 3.1'
-
-# Other
-gem 'byebug', '~> 11.1'
-gem 'nokogiri', '~> 1.15'
-gem 'rerun'
-
-# Development
-gem 'faker'
-gem 'rubocop'
-
-# Testing
-gem 'minitest', group: :test
-gem 'rack-test', group: :test
diff --git a/Gemfile.lock b/Gemfile.lock
deleted file mode 100644
index e12c402..0000000
--- a/Gemfile.lock
+++ /dev/null
@@ -1,146 +0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- activemodel (7.1.1)
- activesupport (= 7.1.1)
- activerecord (7.1.1)
- activemodel (= 7.1.1)
- activesupport (= 7.1.1)
- timeout (>= 0.4.0)
- activesupport (7.1.1)
- base64
- bigdecimal
- concurrent-ruby (~> 1.0, >= 1.0.2)
- connection_pool (>= 2.2.5)
- drb
- i18n (>= 1.6, < 2)
- minitest (>= 5.1)
- mutex_m
- tzinfo (~> 2.0)
- ast (2.4.2)
- base64 (0.2.0)
- bcrypt (3.1.19)
- bigdecimal (3.1.4)
- byebug (11.1.3)
- concurrent-ruby (1.2.2)
- connection_pool (2.4.1)
- drb (2.2.0)
- ruby2_keywords
- et-orbi (1.2.11)
- tzinfo
- faker (3.2.2)
- i18n (>= 1.8.11, < 2)
- ffi (1.16.3)
- fugit (1.11.1)
- et-orbi (~> 1, >= 1.2.11)
- raabro (~> 1.4)
- i18n (1.14.1)
- concurrent-ruby (~> 1.0)
- json (2.6.3)
- language_server-protocol (3.17.0.3)
- listen (3.8.0)
- rb-fsevent (~> 0.10, >= 0.10.3)
- rb-inotify (~> 0.9, >= 0.9.10)
- minitest (5.20.0)
- multi_json (1.15.0)
- mustermann (3.0.0)
- ruby2_keywords (~> 0.0.1)
- mutex_m (0.2.0)
- nio4r (2.5.9)
- nokogiri (1.18.8-aarch64-linux-gnu)
- racc (~> 1.4)
- nokogiri (1.18.8-arm64-darwin)
- racc (~> 1.4)
- nokogiri (1.18.8-x86_64-linux-gnu)
- racc (~> 1.4)
- parallel (1.23.0)
- parser (3.2.2.4)
- ast (~> 2.4.1)
- racc
- puma (6.4.0)
- nio4r (~> 2.0)
- raabro (1.4.0)
- racc (1.7.3)
- rack (2.2.8)
- rack-protection (3.1.0)
- rack (~> 2.2, >= 2.2.4)
- rack-test (2.1.0)
- rack (>= 1.3)
- rainbow (3.1.1)
- rake (13.2.1)
- rb-fsevent (0.11.2)
- rb-inotify (0.10.1)
- ffi (~> 1.0)
- regexp_parser (2.8.2)
- rerun (0.14.0)
- listen (~> 3.0)
- rexml (3.2.6)
- rubocop (1.57.2)
- json (~> 2.3)
- language_server-protocol (>= 3.17.0)
- parallel (~> 1.10)
- parser (>= 3.2.2.4)
- rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 1.8, < 3.0)
- rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.28.1, < 2.0)
- ruby-progressbar (~> 1.7)
- unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.30.0)
- parser (>= 3.2.1.0)
- ruby-progressbar (1.13.0)
- ruby2_keywords (0.0.5)
- rufus-scheduler (3.8.2)
- fugit (~> 1.1, >= 1.1.6)
- sinatra (3.1.0)
- mustermann (~> 3.0)
- rack (~> 2.2, >= 2.2.4)
- rack-protection (= 3.1.0)
- tilt (~> 2.0)
- sinatra-activerecord (2.0.27)
- activerecord (>= 4.1)
- sinatra (>= 1.0)
- sinatra-contrib (3.1.0)
- multi_json
- mustermann (~> 3.0)
- rack-protection (= 3.1.0)
- sinatra (= 3.1.0)
- tilt (~> 2.0)
- sinatra-cross_origin (0.4.0)
- sinatra-namespace (1.0)
- sinatra-contrib
- sqlite3 (1.6.8-aarch64-linux)
- sqlite3 (1.6.8-arm64-darwin)
- sqlite3 (1.6.8-x86_64-linux)
- tilt (2.3.0)
- timeout (0.4.1)
- tzinfo (2.0.6)
- concurrent-ruby (~> 1.0)
- unicode-display_width (2.5.0)
-
-PLATFORMS
- aarch64-linux
- arm64-darwin-22
- arm64-darwin-24
- x86_64-linux
-
-DEPENDENCIES
- bcrypt (~> 3.1)
- byebug (~> 11.1)
- faker
- minitest
- nokogiri (~> 1.15)
- puma
- rack-protection (~> 3.1.0)
- rack-test
- rake (~> 13.0)
- rerun
- rubocop
- rufus-scheduler (~> 3.8.2)
- sinatra-activerecord
- sinatra-cross_origin
- sinatra-namespace
- sqlite3
-
-BUNDLED WITH
- 2.4.21
diff --git a/README.md b/README.md
index 6252060..f5c581c 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@
This app allows users to manage their tasks, projects, areas, notes, and tags in an organized way. Users can create tasks, projects, areas (to group projects), notes, and tags. Each task can be associated with a project, and both tasks and notes can be tagged for better organization. Projects can belong to areas and can also have multiple notes and tags. This structure helps users categorize and track their work efficiently, whether they’re managing individual tasks, larger projects, or keeping detailed notes.
+
## ✨ Features
- **Task Management**: Create, update, and delete tasks. Mark tasks as completed and view them by different filters (Today, Upcoming, Someday). Order them by Name, Due Date, Date Created, or Priority.
@@ -35,9 +36,9 @@ Check out our [GitHub Project](https://github.com/users/chrisvel/projects/2) for
## 🛠️ Getting Started
-**One simple command**, that's all it takes to run tududi with _docker_.
+### Quick Start with Docker
-### 🐋 Docker
+**One simple command**, that's all it takes to run tududi with Docker.
First pull the latest image:
@@ -58,7 +59,7 @@ The following environment variables are used to configure tududi:
- `TUDUDI_INTERNAL_SSL_ENABLED` - Set to 'true' if using HTTPS internally (default: false)
- `TUDUDI_ALLOWED_ORIGINS` - Controls CORS access for different deployment scenarios:
- Not set: Only allows localhost origins
- - Specific domains: `https://tududi.com,http://localhost:9292`
+ - Specific domains: `https://tududi.com,http://localhost:3002`
- Allow all (development only): Set to empty string `""`
#### Common Configuration Examples:
@@ -89,23 +90,31 @@ docker run \
-e TUDUDI_USER_PASSWORD=mysecurepassword \
-e TUDUDI_SESSION_SECRET=$(openssl rand -hex 64) \
-e TUDUDI_INTERNAL_SSL_ENABLED=false \
- -e TUDUDI_ALLOWED_ORIGINS=https://tududi,http://tududi:9292 \
- -v ~/tududi_db:/usr/src/app/tududi_db \
- -p 9292:9292 \
+ -e TUDUDI_ALLOWED_ORIGINS=https://tududi,http://tududi:3002 \
+ -v ~/tududi_db:/usr/src/app/backend/db \
+ -p 3002:3002 \
-d chrisvel/tududi:latest
```
-Navigate to [https://localhost:9292](https://localhost:9292) and login with your credentials.
+Navigate to [http://localhost:3002](http://localhost:3002) and login with your credentials.
+
+### 🔑 Authentication
+
+The application uses session-based authentication with secure cookies. For development:
+- Frontend runs on port 8080 with webpack dev server
+- Backend runs on port 3001 and handles authentication
+- CORS is configured to allow cross-origin requests during development
+- In production (Docker), both frontend and backend run on the same port (3002)
## 🚧 Development
### Prerequisites
Before you begin, ensure you have the following installed:
-- Ruby (version 3.2.2 or higher)
-- Sinatra
+- Node.js (version 20 or higher)
+- Express.js
- SQLite3
-- Puma
+- npm
- ReactJS
### 🏗 Installation
@@ -120,59 +129,165 @@ To install `tududi`, follow these steps:
```bash
cd tududi
```
-3. Install the required gems:
+3. Install the required dependencies:
```bash
- bundle install
+ # Install frontend dependencies
+ npm install
+
+ # Install backend dependencies
+ cd backend
+ npm install
+ cd ..
```
-### 🔒 SSL Setup
+### 🔒 SSL Setup (Optional)
+
+For HTTPS support, create SSL certificates:
1. Create and enter the directory:
```bash
- mkdir certs
- cd certs
+ mkdir backend/certs
+ cd backend/certs
```
2. Create the key and cert:
```bash
openssl genrsa -out server.key 2048
openssl req -new -x509 -key server.key -out server.crt -days 365
+ cd ../..
```
### 📂 Database Setup
-Execute the migrations:
+The database will be automatically initialized when you start the Express backend. For manual database operations:
-```bash
-rake db:migrate
+```bash
+cd backend
+
+# Initialize database (creates tables, drops existing data)
+npm run db:init
+
+# Sync database (creates tables if they don't exist)
+npm run db:sync
+
+# Migrate database (alters existing tables to match models)
+npm run db:migrate
+
+# Reset database (drops and recreates all tables)
+npm run db:reset
+
+# Check database status and connection
+npm run db:status
+
+cd ..
```
-### 👤 Create Your User
+### 🔄 Database Migrations
-1. Open the console:
- ```bash
- rake console
- ```
-2. Add the user:
- ```ruby
- User.create(email: "myemail@somewhere.com", password: "awes0meHax0Rp4ssword")
- ```
+For schema changes, use Sequelize migrations (similar to Rails/Ruby migrations):
+
+```bash
+cd backend
+
+# Create a new migration
+npm run migration:create add-description-to-tasks
+
+# Run pending migrations
+npm run migration:run
+
+# Check migration status
+npm run migration:status
+
+# Rollback last migration
+npm run migration:undo
+
+# Rollback all migrations
+npm run migration:undo:all
+
+cd ..
+```
+
+#### Creating a New Migration Example:
+```bash
+# 1. Create the migration file
+npm run migration:create add-priority-to-projects
+
+# 2. Edit the generated file in migrations/ folder:
+# - Add your schema changes in the 'up' function
+# - Add rollback logic in the 'down' function
+
+# 3. Run the migration
+npm run migration:run
+```
+
+### 👤 User Setup
+
+#### For Development
+
+Set environment variables to automatically create the initial user:
+
+```bash
+export TUDUDI_USER_EMAIL=dev@example.com
+export TUDUDI_USER_PASSWORD=password123
+export TUDUDI_SESSION_SECRET=$(openssl rand -hex 64)
+```
+
+Or create a user manually:
+```bash
+cd backend
+npm run user:create dev@example.com password123
+cd ..
+```
+
+#### Default Development Credentials
+
+If no environment variables are set, you can use the default development credentials:
+- Email: `dev@example.com`
+- Password: `password123`
### 🚀 Usage
-To start the application, run:
+To start the application for development:
-```bash
-puma -C app/config/puma.rb
-```
+1. **Start the Express backend** (in one terminal):
+ ```bash
+ cd backend
+ npm run dev # Development mode with auto-reload
+ # Or: npm start # Production mode
+ ```
+ The backend will run on `http://localhost:3001`
+
+2. **Start the frontend development server** (in another terminal):
+ ```bash
+ npm run dev
+ ```
+ The frontend will run on `http://localhost:8080`
+
+3. **Access the application**: Open your browser to `http://localhost:8080`
+
+### Port Configuration
+
+- **Development Frontend**: `http://localhost:8080` (webpack dev server)
+- **Development Backend**: `http://localhost:3001` (Express API server)
+- **Docker/Production**: `http://localhost:3002` (combined frontend + backend)
+
+The webpack dev server automatically proxies API calls and locales to the backend server.
### 🔍 Testing
-To run tests, execute:
+To run tests:
```bash
-bundle exec ruby -Itest test/test_app.rb
+# Backend tests
+cd backend
+npm test
+
+# Frontend tests
+cd ..
+npm test
```
+Note: Test suites are currently being migrated from the Ruby/Sinatra implementation.
+
## 🤝 Contributing
Contributions to `tududi` are welcome. To contribute:
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index 7be69c7..0000000
--- a/Rakefile
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'irb'
-
-require 'sinatra/activerecord'
-require 'sinatra/activerecord/rake'
-
-require './app'
-
-desc 'Start an interactive console'
-task :console do
- ARGV.clear
- IRB.start
-end
\ No newline at end of file
diff --git a/app.rb b/app.rb
deleted file mode 100644
index 891c180..0000000
--- a/app.rb
+++ /dev/null
@@ -1,208 +0,0 @@
-require 'sinatra'
-require 'sinatra/activerecord'
-require 'securerandom'
-require 'byebug'
-
-# Models
-require './app/models/user'
-require './app/models/area'
-require './app/models/project'
-require './app/models/task'
-require './app/models/tag'
-require './app/models/note'
-require './app/models/inbox_item'
-
-# Services
-require './app/services/task_summary_service'
-require './app/services/url_title_extractor_service'
-require './config/initializers/scheduler'
-require './config/initializers/telegram_initializer'
-
-# Helpers
-require './app/helpers/authentication_helper'
-require './app/routes/authentication_routes'
-require './app/routes/tasks_routes'
-require './app/routes/projects_routes'
-require './app/routes/areas_routes'
-require './app/routes/notes_routes'
-require './app/routes/tags_routes'
-require './app/routes/users_routes'
-require './app/routes/inbox_routes'
-require './app/routes/telegram_poller'
-require './app/routes/telegram_routes'
-require './app/routes/url_routes'
-
-require 'sinatra/cross_origin'
-
-helpers AuthenticationHelper
-
-use Rack::MethodOverride
-
-set :database_file, './app/config/database.yml'
-set :views, proc { File.join(root, 'app/views') }
-set :public_folder, production? ? 'dist' : 'public'
-
-configure do
- enable :cross_origin
- enable :sessions
-
- # Session configuration
- secure_flag = production? && ENV['TUDUDI_INTERNAL_SSL_ENABLED'] == 'true'
- set :sessions, httponly: true,
- secure: secure_flag,
- expire_after: 2_592_000,
- same_site: secure_flag ? :none : :lax
- set :session_secret, ENV.fetch('TUDUDI_SESSION_SECRET') { SecureRandom.hex(64) }
-
- # CORS configuration - use environment variable in production, fallback to localhost for development
- allowed_origins = if ENV['TUDUDI_ALLOWED_ORIGINS']
- ENV['TUDUDI_ALLOWED_ORIGINS'].split(',').map(&:strip)
- else
- ['http://localhost:8080', 'http://localhost:9292', 'http://127.0.0.1:8080', 'http://127.0.0.1:9292']
- end
- set :allow_origin, allowed_origins
- set :allow_methods, %i[get post patch delete options]
- set :allow_credentials, true
- set :max_age, '1728000'
- set :expose_headers, ['Content-Type']
- set :allow_headers, %w[Authorization Content-Type Accept X-Requested-With]
-
- # Ensure ActiveRecord connection is established
- ActiveRecord::Base.establish_connection
-
- # Auto-create user if not exists
- if ENV['TUDUDI_USER_EMAIL'] && ENV['TUDUDI_USER_PASSWORD'] && ActiveRecord::Base.connection.table_exists?('users')
- user = User.find_or_initialize_by(email: ENV['TUDUDI_USER_EMAIL'])
- if user.new_record?
- user.password = ENV['TUDUDI_USER_PASSWORD']
- user.save
- end
- end
-
- # Initialize the Telegram polling after database is ready
- initialize_telegram_polling
-end
-
-# Rack Protection configuration - completely disable for development
-if development?
- # Disable Rack::Protection completely in development to avoid CSRF issues
- set :protection, false
-else
- # Use the same allowed origins for Rack::Protection in production
- use Rack::Protection,
- except: %i[remote_token session_hijacking remote_referrer],
- origin_whitelist: settings.allow_origin
-end
-
-before do
- # Handle CORS preflight requests
- if request.request_method == 'OPTIONS'
- response.headers['Access-Control-Allow-Methods'] = settings.allow_methods.map(&:to_s).join(', ')
- response.headers['Access-Control-Allow-Headers'] = settings.allow_headers.join(', ')
- response.headers['Access-Control-Max-Age'] = settings.max_age
- halt 200
- end
-
- # Set CORS headers for all requests
- if request.env['HTTP_ORIGIN'] && settings.allow_origin.include?(request.env['HTTP_ORIGIN'])
- response.headers['Access-Control-Allow-Origin'] = request.env['HTTP_ORIGIN']
- response.headers['Access-Control-Allow-Credentials'] = 'true'
- response.headers['Access-Control-Expose-Headers'] = settings.expose_headers.join(', ')
- end
-
- # Authentication check - only for API routes
- if request.path_info.start_with?('/api/') && !['/api/login', '/api/health'].include?(request.path_info)
- require_login
- end
-end
-
-helpers do
- def current_path
- request.path_info
- end
-
- def partial(page, options = {})
- erb page, options.merge!(layout: false)
- end
-
- def nav_link_active?(path, query_params = {}, project_id = nil)
- current_uri = request.path_info
- current_query = request.query_string
- current_params = Rack::Utils.parse_nested_query(current_query)
- is_project_page = current_uri.include?('/project/') && path.include?('/project/')
-
- if is_project_page
- current_uri == path && (!project_id || current_uri.end_with?("/#{project_id}"))
- elsif !query_params.empty?
- current_uri == path && query_params.all? { |k, v| current_params[k] == v }
- else
- current_uri == path && current_params.empty?
- end
- end
-
- def nav_link(path, query_params = {}, project_id = nil)
- is_active = nav_link_active?(path, query_params, project_id)
-
- classes = 'nav-link py-1 px-3'
- classes += ' active-link' if is_active
-
- classes
- end
-
- def update_query_params(key, value)
- uri = URI(request.url)
- params = Rack::Utils.parse_nested_query(uri.query)
- params[key] = value
- Rack::Utils.build_query(params)
- end
-
- def url_without_tag
- uri = URI(request.url)
- params = Rack::Utils.parse_nested_query(uri.query)
- params.delete('tag') # Remove the 'tag' parameter
- uri.query = Rack::Utils.build_query(params)
- uri.to_s
- end
-end
-
-get '/' do
- if settings.production?
- # In production, serve the built index.html directly
- send_file File.join(settings.public_folder, 'index.html')
- else
- # In development, use ERB template
- erb :index
- end
-end
-
-# Catch-all route for SPA routing in production
-get '*' do
- # Skip API routes and static assets
- unless request.path_info.start_with?('/api/') || request.path_info.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)
- if settings.production?
- # In production, serve the built index.html for all SPA routes
- send_file File.join(settings.public_folder, 'index.html')
- else
- # In development, use ERB template
- erb :index
- end
- end
-end
-
-# Health check endpoint for Docker
-get '/api/health' do
- content_type :json
- { status: 'ok', timestamp: Time.now.iso8601 }.to_json
-end
-
-# Catch-all route for non-API routes to serve the SPA
-get '*' do
- pass if request.path_info.start_with?('/api/')
- erb :index
-end
-
-not_found do
- content_type :json
- status 404
- { error: 'Not Found', message: 'The requested resource could not be found.' }.to_json
-end
diff --git a/app/config/database.yml b/app/config/database.yml
deleted file mode 100644
index 3eea98c..0000000
--- a/app/config/database.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# config/database.yml
-default: &default
- adapter: sqlite3
- pool: 15
- timeout: 5000
-
-development:
- <<: *default
- database: db/development.sqlite3
-
-test:
- <<: *default
- database: db/test.sqlite3
-
-production:
- <<: *default
- database: tududi_db/production.sqlite3
diff --git a/app/config/puma.rb b/app/config/puma.rb
deleted file mode 100644
index 5362ee1..0000000
--- a/app/config/puma.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-if ENV['TUDUDI_INTERNAL_SSL_ENABLED'] == 'true'
- ssl_bind '0.0.0.0', '9292', {
- key: 'certs/server.key',
- cert: 'certs/server.crt',
- verify_mode: 'none'
- }
-else
- bind 'tcp://0.0.0.0:9292'
-end
diff --git a/app/helpers/authentication_helper.rb b/app/helpers/authentication_helper.rb
deleted file mode 100644
index dfc2c9b..0000000
--- a/app/helpers/authentication_helper.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module AuthenticationHelper
- def logged_in?
- !!session[:user_id]
- end
-
- def current_user
- @current_user ||= User.find(session[:user_id]) if session[:user_id]
- end
-
- def require_login
- return if ['/api/login', '/api/logout', '/api/current_user'].include?(request.path_info)
-
- return if logged_in?
-
- if request.xhr? || request.path_info.start_with?('/api/')
- halt 401, { error: 'You must be logged in' }.to_json
- else
- redirect '/login'
- end
- end
-end
diff --git a/app/models/area.rb b/app/models/area.rb
deleted file mode 100644
index 84b8a0f..0000000
--- a/app/models/area.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Area < ActiveRecord::Base
- belongs_to :user
- has_many :projects, dependent: :destroy
-
- validates :name, presence: true, uniqueness: { scope: :user_id }
-end
diff --git a/app/models/inbox_item.rb b/app/models/inbox_item.rb
deleted file mode 100644
index 89e7318..0000000
--- a/app/models/inbox_item.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-class InboxItem < ActiveRecord::Base
- belongs_to :user
-
- enum status: { added: 'added', processed: 'processed', deleted: 'deleted' }
- enum source: { tududi: 'tududi', telegram: 'telegram' }
-
- scope :active, -> { where(status: 'added') }
- scope :processed, -> { where(status: 'processed') }
- scope :by_source, ->(source) { where(source: source) }
-
- validates :content, presence: true
- validates :status, inclusion: { in: statuses.keys }
- validates :source, inclusion: { in: sources.keys }
-
- def mark_as_processed!
- update(status: 'processed')
- end
-
- def mark_as_deleted!
- update(status: 'deleted')
- end
-end
diff --git a/app/models/note.rb b/app/models/note.rb
deleted file mode 100644
index 5338d7f..0000000
--- a/app/models/note.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class Note < ActiveRecord::Base
- belongs_to :user
- belongs_to :project, optional: true
- has_and_belongs_to_many :tags
-
- validates :content, presence: true
- validates :title, presence: true, uniqueness: { scope: :user_id }
-end
diff --git a/app/models/project.rb b/app/models/project.rb
deleted file mode 100644
index db73f0f..0000000
--- a/app/models/project.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-class Project < ActiveRecord::Base
- belongs_to :user
- belongs_to :area, optional: true
- has_many :tasks, dependent: :destroy
- has_many :notes, dependent: :destroy
- has_and_belongs_to_many :tags
-
- enum priority: { low: 0, medium: 1, high: 2 }
-
- scope :with_incomplete_tasks, -> { joins(:tasks).where.not(tasks: { status: Task.statuses[:done] }).distinct }
- scope :with_complete_tasks, -> { joins(:tasks).where(tasks: { status: Task.statuses[:done] }).distinct }
-
- validates :name, presence: true, uniqueness: { scope: :user_id }
-
- def task_status_counts
- status_counts = tasks.group(:status).count
-
- total = status_counts.values.sum
-
- {
- total: total,
- in_progress: status_counts['in_progress'] || 0,
- done: status_counts['done'] || 0,
- not_started: status_counts['not_started'] || 0
- }
- end
-
- def progress_percentage
- counts = task_status_counts
- return 0 if counts[:total].zero?
-
- completed_tasks = counts[:total] - counts[:not_started]
- (completed_tasks.to_f / counts[:total] * 100).round
- end
-
- def due_date_at
- self[:due_date_at]&.strftime('%Y-%m-%d')
- end
-end
diff --git a/app/models/tag.rb b/app/models/tag.rb
deleted file mode 100644
index a5662ad..0000000
--- a/app/models/tag.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class Tag < ActiveRecord::Base
- belongs_to :user
- has_and_belongs_to_many :tasks
- has_and_belongs_to_many :notes
- has_and_belongs_to_many :projects
-
- validates :name, presence: true, uniqueness: { scope: :user_id }
-end
diff --git a/app/models/task.rb b/app/models/task.rb
deleted file mode 100644
index f89b74e..0000000
--- a/app/models/task.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-class Task < ActiveRecord::Base
- belongs_to :user
- belongs_to :project, optional: true
- has_and_belongs_to_many :tags
-
- enum priority: { low: 0, medium: 1, high: 2 }
- enum status: { not_started: 0, in_progress: 1, done: 2, archived: 3, waiting: 4 }
-
- scope :complete, -> { where(status: statuses[:done]) }
- scope :incomplete, -> { where.not(status: statuses[:done]) }
- scope :due_today, -> { incomplete.where('DATE(due_date) <= ?', Date.today) }
- scope :upcoming, -> { incomplete.where('due_date BETWEEN ? AND ?', Date.today, Date.today + 7.days) }
- scope :someday, -> { incomplete.where(due_date: nil) }
- scope :next_actions, -> { incomplete.where(due_date: nil, project_id: nil) }
- scope :waiting_for, -> { incomplete.where(status: statuses[:waiting]) }
- scope :inbox, -> { incomplete.where('due_date IS NULL OR project_id IS NULL') }
-
- scope :ordered_by_due_date, lambda { |direction = 'asc'|
- order(Arel.sql("CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date #{direction}"))
- }
-
- scope :with_tag, lambda { |tag_name|
- joins(:tags).where(tags: { name: tag_name })
- }
-
- scope :by_status, ->(status) { where(status: statuses[status]) }
- scope :by_priority, ->(priority) { where(priority: priorities[priority]) }
-
- scope :order_by_priority, -> { order(priority: :desc) }
-
- validates :name, presence: true, uniqueness: { scope: :user_id }
-
- def self.filter_by_params(params, user)
- tasks = user.tasks.includes(:project, :tags)
-
- tasks = case params[:type]
- when 'today'
- tasks
- when 'upcoming'
- tasks.upcoming
- when 'next'
- tasks.next_actions
- when 'inbox'
- tasks.inbox
- when 'someday'
- tasks.someday
- when 'waiting'
- tasks.waiting_for
- else
- params[:status] == 'done' ? tasks.complete : tasks.incomplete
- end
-
- tasks = tasks.with_tag(params[:tag]) if params[:tag]
-
- tasks = tasks.apply_ordering(params[:order_by]) if params[:order_by]
-
- tasks.left_joins(:tags).distinct
- end
-
- scope :apply_ordering, lambda { |order_by|
- order_column, order_direction = order_by.split(':')
- order_direction ||= 'asc'
- order_direction = order_direction.downcase == 'desc' ? :desc : :asc
-
- allowed_columns = %w[created_at updated_at name priority status due_date]
- raise ArgumentError, 'Invalid order column specified.' unless allowed_columns.include?(order_column)
-
- if order_column == 'due_date'
- ordered_by_due_date(order_direction)
- else
- order("tasks.#{order_column} #{order_direction}")
- end
- }
-
- def self.compute_metrics(user)
- total_open_tasks = user.tasks.incomplete.count
- one_month_ago = Date.today - 30
- tasks_pending_over_month = user.tasks.incomplete.where('created_at < ?', one_month_ago).count
-
- tasks_in_progress = user.tasks.incomplete.where(status: statuses[:in_progress]).order(priority: :desc)
- tasks_in_progress_count = tasks_in_progress.count
-
- # Calculate tasks due today including those due via projects
- tasks_due_today = user.tasks.incomplete.joins(:project)
- .where('tasks.due_date <= ? OR projects.due_date_at <= ?', Date.today, Date.today)
- .distinct
-
- # Gather an array of IDs to be excluded from suggested tasks
- excluded_task_ids = tasks_in_progress.pluck(:id) + tasks_due_today.pluck(:id)
-
- # Gather tasks in projects expiring starting today, order by task priority
- tasks_in_expiring_projects = user.tasks.incomplete
- .joins(:project)
- .where('projects.due_date_at >= ?', Date.today)
- .where(projects: { active: true }) # Only active projects
- .where.not(id: excluded_task_ids)
- .order(Arel.sql('projects.due_date_at ASC, tasks.priority DESC'))
- .limit(5)
-
- # Gather tasks not assigned to projects expiring today, ordered by task priority
- tasks_without_projects = user.tasks.incomplete
- .where(status: statuses[:not_started], project_id: nil)
- .or(user.tasks.where(project_id: nil, status: statuses[:not_started]))
- .where.not(id: excluded_task_ids)
- .order(priority: :desc)
- .limit(5)
-
- # Combine both list of suggested tasks
- suggested_tasks = sort_suggested_tasks(tasks_in_expiring_projects + tasks_without_projects)
- {
- total_open_tasks: total_open_tasks,
- tasks_pending_over_month: tasks_pending_over_month,
- tasks_in_progress: tasks_in_progress,
- tasks_in_progress_count: tasks_in_progress_count,
- tasks_due_today: tasks_due_today,
- suggested_tasks: suggested_tasks
- }
- end
-
- def self.sort_suggested_tasks(tasks)
- tasks.sort_by do |task|
- # Parse or default the task due date
- task_due_date = if task.due_date.is_a?(String)
- Date.parse(task.due_date)
- else
- task.due_date || Date.new(9999, 12, 31)
- end
-
- # Parse or default the project due date
- project_due_date = if task.project&.due_date_at.is_a?(String)
- Date.parse(task&.project&.due_date_at)
- else
- task.project&.due_date_at || Date.new(9999, 12, 31)
- end
-
- # Priority in descending order (sorted values should be negative for sort_by)
- priority_value = -Task.priorities.fetch(task.priority, -1)
-
- # Determine sorting flags based on various criteria
- is_high_priority_proj_with_due_date = task.priority == 'high' && task&.project&.due_date_at ? 0 : 1
- is_high_priority_with_due_date = task.priority == 'high' && task.due_date ? 0 : 1
- is_high_priority = task.priority == 'high' && !task.due_date && !task&.project&.due_date_at ? 0 : 1
-
- is_medium_priority_proj_with_due_date = task.priority == 'medium' && task&.project&.due_date_at ? 0 : 1
- is_medium_priority_with_due_date = task.priority == 'medium' && task.due_date ? 0 : 1
- is_medium_priority = task.priority == 'medium' && !task.due_date && !task&.project&.due_date_at ? 0 : 1
-
- is_low_priority_proj_with_due_date = task.priority == 'low' && task&.project&.due_date_at ? 0 : 1
- is_low_priority_with_due_date = task.priority == 'low' && task.due_date ? 0 : 1
- is_low_priority = task.priority == 'low' && !task.due_date && !task&.project&.due_date_at ? 0 : 1
-
- # Primary sorting criteria
- [
- is_high_priority_proj_with_due_date,
- is_high_priority_with_due_date,
- is_high_priority,
-
- is_medium_priority_proj_with_due_date,
- is_medium_priority_with_due_date,
- is_medium_priority,
-
- is_low_priority_proj_with_due_date,
- is_low_priority_with_due_date,
- is_low_priority,
-
- task_due_date,
- project_due_date,
- priority_value
- ]
- end
- end
-
- def as_json(options = {})
- super(options).merge(
- 'due_date' => due_date&.strftime('%Y-%m-%d')
- )
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
deleted file mode 100644
index d95a156..0000000
--- a/app/models/user.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class User < ActiveRecord::Base
- has_secure_password
-
- TASK_SUMMARY_FREQUENCIES = %w[daily weekdays weekly 1h 2h 4h 8h 12h].freeze
-
- has_many :tasks, dependent: :destroy
- has_many :projects, dependent: :destroy
- has_many :areas, dependent: :destroy
- has_many :notes, dependent: :destroy
- has_many :tags, dependent: :destroy
- has_many :inbox_items, dependent: :destroy
-
- validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true
- validates :appearance, inclusion: { in: %w[light dark] }
- validates :language, presence: true
- validates :timezone, presence: true
- validates :task_summary_frequency, inclusion: { in: TASK_SUMMARY_FREQUENCIES }, allow_nil: true
-
- # has_one_attached :avatar_image
-end
diff --git a/app/routes/areas_routes.rb b/app/routes/areas_routes.rb
deleted file mode 100644
index 14abc98..0000000
--- a/app/routes/areas_routes.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'sinatra'
-require 'json'
-
-post '/api/areas' do
- content_type :json
- begin
- request_body = request.body.read
- area_data = JSON.parse(request_body, symbolize_names: true)
-
- halt 400, { error: 'Area name is required.' }.to_json unless area_data[:name] && !area_data[:name].strip.empty?
-
- area = current_user.areas.build(name: area_data[:name], description: area_data[:description])
-
- if area.save
- status 201
- area.to_json
- else
- status 400
- { error: 'There was a problem creating the area.', details: area.errors.full_messages }.to_json
- end
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON.' }.to_json
- end
-end
-
-get '/api/areas/:id' do
- area = current_user.areas.find_by(id: params[:id])
- halt 404, { error: "Area not found or doesn't belong to the current user." }.to_json unless area
- area.to_json
-end
-
-patch '/api/areas/:id' do
- content_type :json
- begin
- area = current_user.areas.find_by(id: params[:id])
- halt 404, { error: 'Area not found.' }.to_json unless area
-
- request_body = request.body.read
- area_data = JSON.parse(request_body, symbolize_names: true)
-
- # Update Area attributes
- area.name = area_data[:name] if area_data[:name]
- area.description = area_data[:description] if area_data[:description]
-
- if area.save
- status 200
- area.to_json
- else
- status 400
- { error: 'There was a problem updating the area.', details: area.errors.full_messages }.to_json
- end
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON.' }.to_json
- end
-end
-
-delete '/api/areas/:id' do
- content_type :json
- area = current_user.areas.find_by(id: params[:id])
- halt 404, { error: 'Area not found.' }.to_json unless area
-
- if area.destroy
- status 204
- else
- status 400
- { error: 'There was a problem deleting the area.' }.to_json
- end
-end
-
-get '/api/areas' do
- content_type :json
- areas = current_user.areas
- areas.to_json
-end
diff --git a/app/routes/authentication_routes.rb b/app/routes/authentication_routes.rb
deleted file mode 100644
index cc20eaa..0000000
--- a/app/routes/authentication_routes.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'json'
-
-class Sinatra::Application
- get '/api/current_user' do
- content_type :json
-
- if logged_in?
- { user: { email: current_user.email, id: current_user.id, language: current_user.language,
- appearance: current_user.appearance, timezone: current_user.timezone } }.to_json
- else
- { user: nil }.to_json
- end
- end
-
- post '/api/login' do
- content_type :json
- request_payload = begin
- JSON.parse(request.body.read)
- rescue StandardError
- nil
- end
-
- if request_payload
- email = request_payload['email']
- password = request_payload['password']
- else
- halt 400, { error: 'Invalid login parameters.' }.to_json
- end
-
- user = User.find_by(email: email)
- if user&.authenticate(password)
- session[:user_id] = user.id
- status 200
- { user: { email: user.email, id: user.id, language: user.language, appearance: user.appearance,
- timezone: user.timezone } }.to_json
- else
- halt 401, { errors: ['Invalid credentials'] }.to_json
- end
- end
-
- get '/api/logout' do
- session.clear
- status 200
- { message: 'Logged out successfully' }.to_json
- end
- # session.clear
- # redirect '/login'
- # end
-end
diff --git a/app/routes/inbox_routes.rb b/app/routes/inbox_routes.rb
deleted file mode 100644
index f955dfa..0000000
--- a/app/routes/inbox_routes.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-module Sinatra
- class Application
- get '/api/inbox' do
- content_type :json
-
- items = current_user.inbox_items.where(status: 'added').order(created_at: :desc)
- items.to_json
- end
-
- post '/api/inbox' do
- content_type :json
-
- request_body = request.body.read
- item_data = begin
- JSON.parse(request_body)
- rescue JSON::ParserError => e
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- item = current_user.inbox_items.build(
- content: item_data['content'],
- source: item_data['source'] || 'tududi'
- )
-
- if item.save
- status 201
- item.to_json
- else
- errors = item.errors.full_messages
- halt 400, { error: 'There was a problem creating the inbox item.', details: errors }.to_json
- end
- end
-
- patch '/api/inbox/:id' do
- content_type :json
-
- item = current_user.inbox_items.find_by(id: params[:id])
- halt 404, { error: 'Inbox item not found.' }.to_json unless item
-
- request_body = request.body.read
- item_data = begin
- JSON.parse(request_body)
- rescue JSON::ParserError => e
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- if item.update(content: item_data['content'])
- item.to_json
- else
- errors = item.errors.full_messages
- halt 400, { error: 'There was a problem updating the inbox item.', details: errors }.to_json
- end
- end
-
- patch '/api/inbox/:id/process' do
- content_type :json
-
- item = current_user.inbox_items.find_by(id: params[:id])
- halt 404, { error: 'Inbox item not found.' }.to_json unless item
-
- if item.mark_as_processed!
- item.to_json
- else
- halt 400, { error: 'There was a problem processing the inbox item.' }.to_json
- end
- end
-
- # Mark an inbox item as deleted
- delete '/api/inbox/:id' do
- content_type :json
-
- item = current_user.inbox_items.find_by(id: params[:id])
- halt 404, { error: 'Inbox item not found.' }.to_json unless item
-
- if item.mark_as_deleted!
- { message: 'Inbox item successfully deleted' }.to_json
- else
- halt 400, { error: 'There was a problem deleting the inbox item.' }.to_json
- end
- end
-
- # Get a specific inbox item by ID
- get '/api/inbox/:id' do
- content_type :json
-
- item = current_user.inbox_items.find_by(id: params[:id])
- halt 404, { error: 'Inbox item not found.' }.to_json unless item
-
- item.to_json
- end
- end
-end
diff --git a/app/routes/notes_routes.rb b/app/routes/notes_routes.rb
deleted file mode 100644
index 23f92fa..0000000
--- a/app/routes/notes_routes.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-class Sinatra::Application
- def update_note_tags(note, tags_array)
- return if tags_array.blank?
-
- begin
- tag_names = tags_array.uniq
- tags = tag_names.map do |name|
- current_user.tags.find_or_create_by(name: name)
- end
- note.tags = tags
- rescue StandardError => e
- puts "Failed to update tags: #{e.message}"
- end
- end
-
- get '/api/notes' do
- order_by = params[:order_by] || 'title:asc'
- order_column, order_direction = order_by.split(':')
-
- @notes = current_user.notes.includes(:tags)
- @notes = @notes.joins(:tags).where(tags: { name: params[:tag] }) if params[:tag]
- @notes = @notes.order("notes.#{order_column} #{order_direction}")
-
- query_params = Rack::Utils.parse_nested_query(request.query_string)
- query_params.delete('tag')
- @base_query = query_params.to_query
- @base_url = '/notes?'
- @base_url += "#{@base_query}&" unless @base_query.empty?
-
- @notes.to_json(include: :tags)
- end
-
- get '/api/note/:id' do
- content_type :json
- note = current_user.notes.includes(:tags).find_by(id: params[:id])
-
- halt 404, { error: 'Note not found.' }.to_json unless note
-
- note.to_json(include: :tags)
- end
-
- post '/api/note' do
- content_type :json
-
- request_body = request.body.read
- note_data = JSON.parse(request_body, symbolize_names: true)
-
- note_attributes = {
- title: note_data[:title],
- content: note_data[:content],
- user_id: current_user.id
- }
-
- if note_data[:project_id].to_s.empty?
- note = current_user.notes.build(note_attributes)
- else
- project = current_user.projects.find_by(id: note_data[:project_id])
- halt 400, { error: 'Invalid project.' }.to_json unless project
- note = project.notes.build(note_attributes)
- end
-
- if note.save
- # Handle tags array whether it's an array of strings or an array of objects with name property
- tag_names = if note_data[:tags].is_a?(Array) && note_data[:tags].all? { |t| t.is_a?(String) }
- note_data[:tags]
- elsif note_data[:tags].is_a?(Array) && note_data[:tags].all? { |t| t.is_a?(Hash) && t[:name] }
- note_data[:tags].map { |t| t[:name] }
- else
- []
- end
-
- update_note_tags(note, tag_names)
- status 201
- note.to_json(include: :tags)
- else
- status 400
- { error: 'There was a problem creating the note.', details: note.errors.full_messages }.to_json
- end
- end
-
- patch '/api/note/:id' do
- content_type :json
- note = current_user.notes.find_by(id: params[:id])
- halt 404, { error: 'Note not found.' }.to_json unless note
-
- request_body = request.body.read
- request_data = JSON.parse(request_body)
-
- note_attributes = {
- title: request_data['title'],
- content: request_data['content']
- }
-
- if request_data['project_id'] && !request_data['project_id'].to_s.empty?
- project = current_user.projects.find_by(id: request_data['project_id'])
- halt 400, { error: 'Invalid project.' }.to_json unless project
- note.project = project
- else
- note.project = nil
- end
-
- if note.update(note_attributes)
- # Handle tags array whether it's an array of strings or an array of objects with name property
- tag_names = if request_data['tags'].is_a?(Array) && request_data['tags'].all? { |t| t.is_a?(String) }
- request_data['tags']
- elsif request_data['tags'].is_a?(Array) && request_data['tags'].all? do |t|
- t.is_a?(Hash) && t['name']
- end
- request_data['tags'].map { |t| t['name'] }
- else
- []
- end
-
- update_note_tags(note, tag_names)
- note.to_json(include: :tags)
- else
- status 400
- { error: 'There was a problem updating the note.', details: note.errors.full_messages }.to_json
- end
- end
-
- delete '/api/note/:id' do
- content_type :json
- note = current_user.notes.find_by(id: params[:id])
- halt 404, { error: 'Note not found.' }.to_json unless note
-
- if note.destroy
- { message: 'Note deleted successfully.' }.to_json
- else
- status 400
- { error: 'There was a problem deleting the note.' }.to_json
- end
- end
-end
diff --git a/app/routes/projects_routes.rb b/app/routes/projects_routes.rb
deleted file mode 100644
index 3a53f76..0000000
--- a/app/routes/projects_routes.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-require 'sinatra/namespace'
-
-class Sinatra::Application
- register Sinatra::Namespace
-
- def update_project_tags(project, tags_data)
- return if tags_data.nil?
-
- tag_names = tags_data.map { |tag| tag['name'] }.compact.reject(&:empty?).uniq
-
- existing_tags = Tag.where(user: current_user, name: tag_names)
- new_tags = tag_names - existing_tags.pluck(:name)
- created_tags = new_tags.map { |name| Tag.create(name: name, user: current_user) }
-
- project.tags = (existing_tags + created_tags).uniq
- end
-
- namespace '/api' do
- before do
- content_type :json
- end
-
- get '/projects' do
- active_param = params[:active]
- is_active = active_param == 'true' unless active_param.nil? || active_param == 'all'
-
- pin_to_sidebar_param = params[:pin_to_sidebar]
- is_pinned = pin_to_sidebar_param == 'true' unless pin_to_sidebar_param.nil?
-
- area_id_param = params[:area_id]
-
- projects = current_user.projects
- .includes(:tags)
- .left_joins(:tasks, :area)
- .distinct
- .order('projects.name ASC')
-
- projects = projects.where(active: is_active) unless is_active.nil?
- projects = projects.where(pin_to_sidebar: is_pinned) unless is_pinned.nil?
- projects = projects.where(area_id: area_id_param) unless area_id_param.blank?
- task_status_counts = projects.each_with_object({}) do |project, counts|
- counts[project.id] = project.task_status_counts
- end
-
- grouped_projects = projects.group_by(&:area)
-
- {
- projects: projects.as_json(include: { tasks: {}, area: { only: :name }, tags: { only: %i[id name] } }),
- task_status_counts: task_status_counts,
- grouped_projects: grouped_projects.as_json(include: { area: { only: :name } })
- }.to_json
- end
-
- get '/project/:id' do
- project = current_user.projects.includes(:tasks, :tags).find_by(id: params[:id])
-
- halt 404, { error: 'Project not found' }.to_json unless project
-
- project.as_json(include: { tasks: {}, area: { only: %i[id name] }, tags: { only: %i[id name] } }).to_json
- end
-
- post '/project' do
- request_body = request.body.read
- project_data = begin
- JSON.parse(request_body)
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- project_data['priority'] = Project.priorities[project_data['priority']] if project_data['priority'].is_a?(String)
-
- project = current_user.projects.new(
- name: project_data['name'],
- description: project_data['description'] || '',
- area_id: project_data['area_id'],
- active: true,
- pin_to_sidebar: false,
- priority: project_data['priority'],
- due_date_at: project_data['due_date_at']
- )
-
- if project.save
- update_project_tags(project, project_data['tags'])
- status 201
- project.as_json(include: { tags: { only: %i[id name] } }).to_json
- else
- status 400
- { error: 'There was a problem creating the project.', details: project.errors.full_messages }.to_json
- end
- end
-
- patch '/project/:id' do
- project = current_user.projects.find_by(id: params[:id])
-
- halt 404, { error: 'Project not found.' }.to_json unless project
-
- request_body = request.body.read
- project_data = begin
- JSON.parse(request_body)
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- project.assign_attributes(
- name: project_data['name'],
- description: project_data['description'],
- area_id: project_data['area_id'],
- active: project_data['active'],
- pin_to_sidebar: project_data['pin_to_sidebar'],
- priority: project_data ['priority'],
- due_date_at: project_data['due_date_at']
- )
-
- if project.save
- update_project_tags(project, project_data['tags'])
- project.as_json(include: { tags: { only: %i[id name] } }).to_json
- else
- status 400
- { error: 'There was a problem updating the project.', details: project.errors.full_messages }.to_json
- end
- end
-
- delete '/project/:id' do
- project = current_user.projects.find_by(id: params[:id])
-
- halt 404, { error: 'Project not found' }.to_json unless project
-
- if project.destroy
- { message: 'Project successfully deleted' }.to_json
- else
- status 400
- { error: 'There was a problem deleting the project.' }.to_json
- end
- end
- end
-end
diff --git a/app/routes/tags_routes.rb b/app/routes/tags_routes.rb
deleted file mode 100644
index 0ff656f..0000000
--- a/app/routes/tags_routes.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-class Sinatra::Application
- get '/api/tags' do
- content_type :json
-
- tags = current_user.tags.order('name ASC')
-
- tags.as_json(only: %i[id name]).to_json
- end
-
- get '/api/tag/:id' do
- content_type :json
-
- tag = current_user.tags.find_by(id: params[:id])
-
- halt 404, { error: 'Tag not found' }.to_json unless tag
-
- tag.as_json(only: %i[id name]).to_json
- end
-
- post '/api/tag' do
- content_type :json
-
- request_body = JSON.parse(request.body.read)
- tag = current_user.tags.new(name: request_body['name'])
-
- if tag.save
- status 201
- tag.as_json(only: %i[id name]).to_json
- else
- status 400
- { error: 'There was a problem creating the tag.' }.to_json
- end
- end
-
- patch '/api/tag/:id' do
- content_type :json
-
- tag = current_user.tags.find_by(id: params[:id])
-
- halt 404, { error: 'Tag not found' }.to_json unless tag
-
- request_body = JSON.parse(request.body.read)
- tag.name = request_body['name']
-
- if tag.save
- tag.as_json(only: %i[id name]).to_json
- else
- status 400
- { error: 'There was a problem updating the tag.' }.to_json
- end
- end
-
- delete '/api/tag/:id' do
- content_type :json
-
- tag = current_user.tags.find_by(id: params[:id])
-
- halt 404, { error: 'Tag not found' }.to_json unless tag
-
- if tag.destroy
- { message: 'Tag successfully deleted' }.to_json
- else
- status 400
- { error: 'There was a problem deleting the tag.' }.to_json
- end
- end
-end
diff --git a/app/routes/tasks_routes.rb b/app/routes/tasks_routes.rb
deleted file mode 100644
index 3e1d06c..0000000
--- a/app/routes/tasks_routes.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-module Sinatra
- class Application
- def update_task_tags(task, tags_data)
- return if tags_data.nil?
-
- tag_names = tags_data.map { |tag| tag['name'] }.compact.reject(&:empty?).uniq
-
- existing_tags = Tag.where(user: current_user, name: tag_names)
- new_tags = tag_names - existing_tags.pluck(:name)
- created_tags = new_tags.map { |name| Tag.create(name: name, user: current_user) }
-
- task.tags = (existing_tags + created_tags).uniq
- end
-
- get '/api/tasks' do
- content_type :json
-
- begin
- tasks = Task.filter_by_params(params, current_user)
- rescue ArgumentError => e
- halt 400, { error: e.message }.to_json
- end
-
- metrics = Task.compute_metrics(current_user)
-
- # Prepare the response
- response = {
- tasks: tasks.as_json(include: { tags: { only: %i[id name] }, project: { only: :name } }),
- metrics: {
- total_open_tasks: metrics[:total_open_tasks],
- tasks_pending_over_month: metrics[:tasks_pending_over_month],
- tasks_in_progress_count: metrics[:tasks_in_progress_count],
- tasks_in_progress: metrics[:tasks_in_progress].as_json(include: { tags: { only: %i[id name] },
- project: { only: :name } }),
- tasks_due_today: metrics[:tasks_due_today].as_json(include: { tags: { only: %i[id name] },
- project: { only: :name } }),
- suggested_tasks: metrics[:suggested_tasks].as_json(include: { tags: { only: %i[id name] },
- project: { only: :name } })
- }
- }
-
- response.to_json
- end
-
- post '/api/task' do
- content_type :json
-
- request_body = request.body.read
- task_data = begin
- JSON.parse(request_body)
- rescue JSON::ParserError => e
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- task_attributes = {
- name: task_data['name'],
- priority: task_data['priority'],
- due_date: task_data['due_date'].presence,
- status: task_data['status'] || Task.statuses[:not_started],
- note: task_data['note'],
- user_id: current_user.id
- }
-
- value = task_data['project_id']
- task = if value.nil? || value.to_s.strip.empty?
- current_user.tasks.build(task_attributes)
- else
- project = current_user.projects.find_by(id: value)
- halt 400, { error: 'Invalid project.' }.to_json unless project
- project.tasks.build(task_attributes)
- end
-
- if task.save
- update_task_tags(task, task_data['tags'])
- status 201
- task.to_json(include: { tags: { only: :name }, project: { only: :name } })
- else
- errors = task.errors.full_messages
- halt 400, { error: 'There was a problem creating the task.', details: errors }.to_json
- end
- end
-
- patch '/api/task/:id' do
- content_type :json
- puts "Request to update task with ID: #{params[:id]}"
- puts "Current user: #{current_user&.id}"
-
- request_body = request.body.read
- task_data = begin
- JSON.parse(request_body)
- rescue JSON::ParserError => e
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- task = current_user.tasks.find_by(id: params[:id])
-
- halt 404, { error: 'Task not found.' }.to_json unless task
-
- task_attributes = {
- name: task_data['name'], # Get the name from the JSON body
- priority: task_data['priority'],
- status: task_data['status'] || Task.statuses[:not_started],
- note: task_data['note'],
- due_date: task_data['due_date'].presence
- }
-
- if task_data['project_id'] && !task_data['project_id'].to_s.strip.empty?
- project = current_user.projects.find_by(id: task_data['project_id'])
- halt 400, { error: 'Invalid project.' }.to_json unless project
- task.project = project
- else
- task.project = nil
- end
-
- if task.update(task_attributes)
- update_task_tags(task, task_data['tags'])
- task.to_json(include: { tags: { only: :name }, project: { only: :name } })
- else
- errors = task.errors.full_messages
- halt 400, { error: 'There was a problem updating the task.', details: errors }.to_json
- end
- end
-
- patch '/api/task/:id/toggle_completion' do
- content_type :json
-
- task = current_user.tasks.find_by(id: params[:id])
- halt 404, { error: 'Task not found.' }.to_json unless task
-
- new_status = if task.done?
- task.note.present? ? :in_progress : :not_started
- else
- :done
- end
- task.status = new_status
-
- if task.save
- task.to_json
- else
- status 422
- { error: 'Unable to update task' }.to_json
- end
- end
-
- delete '/api/task/:id' do
- content_type :json
-
- task = current_user.tasks.find_by(id: params[:id])
- halt 404, { error: 'Task not found.' }.to_json unless task
-
- if task.destroy
- status 200
- { message: 'Task successfully deleted' }.to_json
- else
- halt 400, { error: 'There was a problem deleting the task.' }.to_json
- end
- end
- end
-end
diff --git a/app/routes/telegram_poller.rb b/app/routes/telegram_poller.rb
deleted file mode 100644
index decaf1a..0000000
--- a/app/routes/telegram_poller.rb
+++ /dev/null
@@ -1,231 +0,0 @@
-require 'net/http'
-require 'uri'
-require 'json'
-require 'thread'
-
-# A class to handle polling for Telegram updates
-class TelegramPoller
- @@instance = nil
- @@mutex = Mutex.new
-
- attr_reader :running, :thread, :poll_interval, :last_update_id, :users_to_poll
-
- def initialize
- @running = false
- @thread = nil
- @poll_interval = 5 # seconds
- @last_update_id = 0
- @users_to_poll = []
-
- # Keep a record of which users have active polling
- @user_status = {}
- end
-
- def self.instance
- @@mutex.synchronize do
- @@instance ||= new
- end
- @@instance
- end
-
- # Start polling for a specific user
- def add_user(user)
- return false unless user && user.telegram_bot_token
-
- @users_to_poll << user unless @users_to_poll.any? { |u| u.id == user.id }
-
- # Start the polling thread if not already running
- start_polling if @users_to_poll.any? && !@running
-
- true
- end
-
- # Remove a user from polling
- def remove_user(user_id)
- @users_to_poll.reject! { |u| u.id == user_id }
-
- # Stop polling if no users left
- stop_polling if @users_to_poll.empty? && @running
-
- true
- end
-
- # Start the polling thread
- def start_polling
- return if @running
-
- @running = true
- @thread = Thread.new do
- while @running
- begin
- poll_updates
- rescue => e
- puts "Error polling Telegram: #{e.message}"
- puts e.backtrace.join("\n")
- end
-
- sleep @poll_interval
- end
- end
- end
-
- # Stop the polling thread
- def stop_polling
- return unless @running
-
- @running = false
- @thread.join if @thread
- @thread = nil
- end
-
- # Poll for updates from Telegram
- def poll_updates
- @users_to_poll.each do |user|
- token = user.telegram_bot_token
- next unless token
-
- begin
- # Get updates from Telegram
- uri = URI.parse("https://api.telegram.org/bot#{token}/getUpdates")
-
- params = {
- offset: @user_status[user.id]&.dig(:last_update_id).to_i + 1,
- timeout: 1 # Short timeout for quick polling
- }
-
- uri.query = URI.encode_www_form(params)
-
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
- http.read_timeout = 5
-
- request = Net::HTTP::Get.new(uri.request_uri)
- response = http.request(request)
-
- if response.code == '200'
- data = JSON.parse(response.body)
-
- if data['ok'] && data['result'].is_a?(Array)
- process_updates(user, data['result'])
- end
- else
- puts "Error polling Telegram for user #{user.id}: #{response.code} #{response.message}"
- end
- rescue => e
- puts "Error getting updates for user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Process updates received from Telegram
- def process_updates(user, updates)
- return if updates.empty?
-
- # Track the highest update_id to avoid processing the same update twice
- highest_update_id = updates.map { |u| u['update_id'].to_i }.max || 0
-
- # Save the last update ID for this user
- @user_status[user.id] ||= {}
- @user_status[user.id][:last_update_id] = highest_update_id if highest_update_id > (@user_status[user.id][:last_update_id] || 0)
-
- updates.each do |update|
- begin
- # Process message updates
- if update['message'] && update['message']['text']
- process_message(user, update)
- end
- rescue => e
- puts "Error processing update #{update['update_id']}: #{e.message}"
- end
- end
- end
-
- # Process a single message
- def process_message(user, update)
- message = update['message']
- text = message['text']
- chat_id = message['chat']['id'].to_s
- message_id = message['message_id']
-
- puts "Processing message from user #{user.id}: #{text}"
-
- # Save the chat_id if not already saved
- if user.telegram_chat_id.nil? || user.telegram_chat_id.empty?
- puts "Updating user's telegram_chat_id to #{chat_id}"
- user.update(telegram_chat_id: chat_id)
- end
-
- # Create an inbox item
- inbox_item = user.inbox_items.build(
- content: text,
- source: 'telegram'
- )
-
- if inbox_item.save
- puts "Created inbox item #{inbox_item.id} from Telegram message"
-
- # Send confirmation
- begin
- send_telegram_message(
- user.telegram_bot_token,
- chat_id,
- "✅ Added to Tududi inbox: \"#{text}\"",
- message_id
- )
- rescue => e
- puts "Error sending confirmation: #{e.message}"
- end
- else
- puts "Failed to create inbox item: #{inbox_item.errors.full_messages.join(', ')}"
-
- # Send error message
- begin
- send_telegram_message(
- user.telegram_bot_token,
- chat_id,
- "❌ Failed to add to inbox: #{inbox_item.errors.full_messages.join(', ')}",
- message_id
- )
- rescue => e
- puts "Error sending error message: #{e.message}"
- end
- end
- end
-
- # Send a message to Telegram
- def send_telegram_message(token, chat_id, text, reply_to_message_id = nil)
- uri = URI.parse("https://api.telegram.org/bot#{token}/sendMessage")
-
- # Prepare message parameters
- message_params = {
- chat_id: chat_id,
- text: text,
- parse_mode: "MarkdownV2"
- }
-
- # Add reply_to_message_id if provided
- message_params[:reply_to_message_id] = reply_to_message_id if reply_to_message_id
-
- # Send the request to Telegram API
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
- request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
- request.body = message_params.to_json
-
- response = http.request(request)
- return JSON.parse(response.body)
- end
-
- # Get status of the poller
- def status
- {
- running: @running,
- users_count: @users_to_poll.size,
- poll_interval: @poll_interval,
- user_status: @user_status
- }
- end
-end
-
-# Initialize the poller when this file is loaded
-TelegramPoller.instance
\ No newline at end of file
diff --git a/app/routes/telegram_routes.rb b/app/routes/telegram_routes.rb
deleted file mode 100644
index 791e446..0000000
--- a/app/routes/telegram_routes.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-require 'net/http'
-require 'uri'
-require 'json'
-require_relative 'telegram_poller'
-
-module Sinatra
- class Application
- # Start polling for a user
- post '/api/telegram/start-polling' do
- content_type :json
-
- # Get the current user's Telegram token
- user = current_user
- halt 400, { error: 'Telegram bot token not set.' }.to_json unless user.telegram_bot_token
-
- # Add the user to the polling list
- if TelegramPoller.instance.add_user(user)
- {
- success: true,
- message: 'Telegram polling started',
- status: TelegramPoller.instance.status
- }.to_json
- else
- halt 500, { error: 'Failed to start Telegram polling.' }.to_json
- end
- end
-
- # Stop polling for a user
- post '/api/telegram/stop-polling' do
- content_type :json
-
- user = current_user
-
- # Remove the user from the polling list
- if TelegramPoller.instance.remove_user(user.id)
- {
- success: true,
- message: 'Telegram polling stopped',
- status: TelegramPoller.instance.status
- }.to_json
- else
- halt 500, { error: 'Failed to stop Telegram polling.' }.to_json
- end
- end
-
- # Get polling status
- get '/api/telegram/polling-status' do
- content_type :json
-
- {
- success: true,
- status: TelegramPoller.instance.status,
- is_polling: TelegramPoller.instance.users_to_poll.any? { |u| u.id == current_user.id }
- }.to_json
- end
-
- # Setup the Telegram bot for a user (save token and start polling)
- post '/api/telegram/setup' do
- content_type :json
- request_body = request.body.read
-
- begin
- setup_data = JSON.parse(request_body)
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- token = setup_data['token']
- halt 400, { error: 'Telegram bot token is required.' }.to_json unless token && !token.empty?
-
- # Validate the token by making a getMe request to Telegram
- begin
- uri = URI.parse("https://api.telegram.org/bot#{token}/getMe")
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
-
- response = http.get(uri.request_uri)
- json_response = JSON.parse(response.body)
-
- if json_response['ok']
- # Token is valid, save it to the user
- bot_username = json_response['result']['username']
- current_user.update(telegram_bot_token: token)
-
- # Start polling for this user
- TelegramPoller.instance.add_user(current_user)
-
- # Return success with bot info
- {
- success: true,
- message: 'Telegram bot configured successfully and polling started!',
- bot: {
- username: bot_username,
- polling_status: TelegramPoller.instance.status,
- chat_url: "https://t.me/#{bot_username}"
- }
- }.to_json
- else
- halt 400, { error: 'Invalid Telegram bot token.', details: json_response['description'] }.to_json
- end
- rescue => e
- halt 500, { error: 'Error validating Telegram bot token.', details: e.message }.to_json
- end
- end
-
- # Test endpoint to simulate a Telegram message (for development)
- post '/api/telegram/test/:user_id' do
- content_type :json
- request_body = request.body.read
-
- begin
- message_data = JSON.parse(request_body)
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- user_id = params[:user_id]
- user = User.find_by(id: user_id)
- halt 404, { error: 'User not found.' }.to_json unless user
- halt 400, { error: 'User has no Telegram bot token configured.' }.to_json unless user.telegram_bot_token
-
- text = message_data['text'] || 'Test message from development environment'
-
- # Create an inbox item directly
- inbox_item = user.inbox_items.build(
- content: text,
- source: 'telegram'
- )
-
- if inbox_item.save
- # Send confirmation to Telegram if the user has a chat_id
- if user.telegram_chat_id
- begin
- # Use the TelegramPoller's send_message method
- response = TelegramPoller.instance.send_telegram_message(
- user.telegram_bot_token,
- user.telegram_chat_id,
- "✅ Added to Tududi inbox: \"#{text}\""
- )
- puts "Test message confirmation sent: #{response}"
- rescue => e
- puts "Error sending test confirmation: #{e.message}"
- end
- end
-
- {
- success: true,
- message: 'Test Telegram message processed successfully!',
- inbox_item_id: inbox_item.id
- }.to_json
- else
- {
- success: false,
- message: 'Failed to create inbox item from test message',
- errors: inbox_item.errors.full_messages
- }.to_json
- end
- end
- end
-end
\ No newline at end of file
diff --git a/app/routes/url_routes.rb b/app/routes/url_routes.rb
deleted file mode 100644
index aa71605..0000000
--- a/app/routes/url_routes.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require_relative '../services/url_title_extractor_service'
-
-module Sinatra
- class Application
- get '/api/url/title' do
- content_type :json
-
- url = params[:url]
- halt 400, { error: 'URL parameter is required' }.to_json unless url
-
- title = UrlTitleExtractorService.extract_title(url)
-
- if title
- { url: url, title: title }.to_json
- else
- { url: url, title: nil, error: 'Could not extract title' }.to_json
- end
- end
-
- post '/api/url/extract-from-text' do
- content_type :json
-
- request_body = request.body.read
-
- begin
- data = JSON.parse(request_body)
- text = data['text']
-
- halt 400, { error: 'Text parameter is required' }.to_json unless text
-
- result = UrlTitleExtractorService.extract_title_from_text(text)
-
- if result
- result.to_json
- else
- { found: false }.to_json
- end
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format' }.to_json
- end
- end
- end
-end
diff --git a/app/routes/users_routes.rb b/app/routes/users_routes.rb
deleted file mode 100644
index 9d0bc9f..0000000
--- a/app/routes/users_routes.rb
+++ /dev/null
@@ -1,168 +0,0 @@
-module Sinatra
- class Application
- get '/api/profile' do
- content_type :json
- user = current_user
-
- if user
- user.to_json(only: %i[id email appearance language timezone avatar_image telegram_bot_token telegram_chat_id task_summary_enabled task_summary_frequency])
- else
- halt 404, { error: 'Profile not found.' }.to_json
- end
- end
-
- patch '/api/profile' do
- content_type :json
-
- begin
- request_payload = JSON.parse(request.body.read)
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- user = current_user
-
- halt 404, { error: 'Profile not found.' }.to_json if user.nil?
-
- allowed_params = {}
- allowed_params[:appearance] = request_payload['appearance'] if request_payload.key?('appearance')
- allowed_params[:language] = request_payload['language'] if request_payload.key?('language')
- allowed_params[:timezone] = request_payload['timezone'] if request_payload.key?('timezone')
- allowed_params[:avatar_image] = request_payload['avatar_image'] if request_payload.key?('avatar_image')
- allowed_params[:telegram_bot_token] = request_payload['telegram_bot_token'] if request_payload.key?('telegram_bot_token')
-
- if user.update(allowed_params)
- user.to_json(only: %i[id email appearance language timezone avatar_image telegram_bot_token telegram_chat_id])
- else
- status 400
- { error: 'Failed to update profile.', details: user.errors.full_messages }.to_json
- end
- end
-
- post '/api/profile/task-summary/toggle' do
- content_type :json
-
- user = current_user
- halt 404, { error: 'User not found.' }.to_json unless user
-
- # Toggle the task_summary_enabled flag
- enabled = !user.task_summary_enabled
-
- if user.update(task_summary_enabled: enabled)
- # If enabling, send a test summary to confirm it works
- if enabled && user.telegram_bot_token && user.telegram_chat_id
- begin
- success = TaskSummaryService.send_summary_to_user(user.id)
-
- if success
- {
- success: true,
- enabled: enabled,
- message: 'Task summary notifications have been enabled and a test message was sent to your Telegram.'
- }.to_json
- else
- user.update(task_summary_enabled: false)
- halt 400, {
- error: 'Failed to send test message to Telegram. Please check your Telegram bot configuration.'
- }.to_json
- end
- rescue => e
- user.update(task_summary_enabled: false)
- halt 400, {
- error: 'Error sending test message to Telegram.',
- details: e.message
- }.to_json
- end
- else
- {
- success: true,
- enabled: enabled,
- message: enabled ? 'Task summary notifications have been enabled.' : 'Task summary notifications have been disabled.'
- }.to_json
- end
- else
- halt 400, {
- error: 'Failed to update task summary settings.',
- details: user.errors.full_messages
- }.to_json
- end
- end
-
- post '/api/profile/task-summary/frequency' do
- content_type :json
-
- begin
- request_payload = JSON.parse(request.body.read)
- rescue JSON::ParserError
- halt 400, { error: 'Invalid JSON format.' }.to_json
- end
-
- frequency = request_payload['frequency']
- halt 400, { error: 'Frequency is required.' }.to_json unless frequency
-
- # Validate frequency value
- valid_frequencies = User::TASK_SUMMARY_FREQUENCIES
- halt 400, { error: 'Invalid frequency value.' }.to_json unless valid_frequencies.include?(frequency)
-
- user = current_user
- halt 404, { error: 'User not found.' }.to_json unless user
-
- if user.update(task_summary_frequency: frequency)
- {
- success: true,
- frequency: frequency,
- message: "Task summary frequency has been set to #{frequency}."
- }.to_json
- else
- halt 400, {
- error: 'Failed to update task summary frequency.',
- details: user.errors.full_messages
- }.to_json
- end
- end
-
- post '/api/profile/task-summary/send-now' do
- content_type :json
-
- user = current_user
- halt 404, { error: 'User not found.' }.to_json unless user
-
- if user.telegram_bot_token && user.telegram_chat_id
- begin
- success = TaskSummaryService.send_summary_to_user(user.id)
-
- if success
- {
- success: true,
- message: 'Task summary was sent to your Telegram.'
- }.to_json
- else
- halt 400, { error: 'Failed to send message to Telegram.' }.to_json
- end
- rescue => e
- halt 400, {
- error: 'Error sending message to Telegram.',
- details: e.message
- }.to_json
- end
- else
- halt 400, { error: 'Telegram bot is not properly configured.' }.to_json
- end
- end
-
- get '/api/profile/task-summary/status' do
- content_type :json
-
- user = current_user
- halt 404, { error: 'User not found.' }.to_json unless user
-
- {
- success: true,
- enabled: user.task_summary_enabled,
- frequency: user.task_summary_frequency,
- last_run: user.task_summary_last_run,
- next_run: user.task_summary_next_run
- }.to_json
- end
- end
-end
diff --git a/app/services/task_summary_service.rb b/app/services/task_summary_service.rb
deleted file mode 100644
index 994ee1a..0000000
--- a/app/services/task_summary_service.rb
+++ /dev/null
@@ -1,288 +0,0 @@
-# app/services/task_summary_service.rb
-require 'yaml'
-
-class TaskSummaryService
- # Helper method to escape special characters for MarkdownV2
- def self.escape_markdown(text)
- # Characters that need to be escaped in MarkdownV2: _*[]()~`>#+-=|{}.!
- text.to_s.gsub(/([_*\[\]()~`>#+\-=|{}.!])/, '\\\\\1')
- end
-
- def self.generate_summary_for_user(user_id)
- user = User.find_by(id: user_id)
- return nil unless user
-
- # Get today's tasks, in progress tasks, etc.
- tasks = user.tasks
-
- today = Date.today
- due_today = tasks.where('DATE(due_date) = ?', today).where.not(status: 'done')
- in_progress = tasks.where(status: 'in_progress')
- completed_today = tasks.where(status: 'done').where('DATE(updated_at) = ?', today)
-
- # Generate summary message
- message = "📋 *Today's Task Summary*\n\n"
-
- # Add a header divider
- message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
-
- # Start Today's Plan section
- message += "✏️ *Today's Plan*\n\n"
-
- # Add due today tasks to Today's Plan
- # Add due today tasks to Today's Plan
- if due_today.any?
- message += "🚀 *Tasks Due Today:*\n"
- due_today.order(:name).each_with_index do |task, index|
- priority_emoji =
- case task.priority
- when 'high' then '🔴'
- when 'medium' then '🟠'
- when 'low' then '🟢'
- else '⚪'
- end
-
- # Escape special characters in task name and project name
- task_name = escape_markdown(task.name)
- project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
-
- message += "#{index + 1}\\. #{priority_emoji} #{task_name}#{project_info}\n"
- end
- message += "\n"
- end
- # Add in progress tasks to Today's Plan
- if in_progress.any?
- message += "⚙️ *In Progress Tasks:*\n"
- in_progress.order(:name).each_with_index do |task, index|
- priority_emoji =
- case task.priority
- when 'high' then '🔴'
- when 'medium' then '🟠'
- when 'low' then '🟢'
- else '⚪'
- end
-
- # Escape special characters in task name and project name
- task_name = escape_markdown(task.name)
- project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
-
- message += "#{index + 1}\\. #{priority_emoji} #{task_name}#{project_info}\n"
- end
- message += "\n"
- end
- # Add suggested tasks (not done, not in due today or in progress)
- suggested_task_ids = due_today.pluck(:id) + in_progress.pluck(:id)
-
- # Get tasks in expiring projects - same logic as Task.compute_metrics
- tasks_in_expiring_projects = tasks
- .where.not(status: 'done')
- .where.not(id: suggested_task_ids)
- .joins(:project)
- .where('projects.due_date_at >= ?', today)
- .where(projects: { active: true }) # Only active projects
- .order(Arel.sql('projects.due_date_at ASC, tasks.priority DESC'))
-
- # Get tasks not assigned to projects - same logic as Task.compute_metrics
- tasks_without_projects = tasks
- .where.not(status: 'done')
- .where.not(id: suggested_task_ids)
- .where(project_id: nil, status: 'not_started')
- .order(priority: :desc)
-
- # Combine both sets of tasks
- combined_tasks = (tasks_in_expiring_projects + tasks_without_projects)
-
- # Sort using same logic as Task.sort_suggested_tasks
- suggested_tasks = combined_tasks.sort_by do |task|
- # Parse or default the task due date
- task_due_date = if task.due_date.is_a?(String)
- Date.parse(task.due_date)
- else
- task.due_date || Date.new(9999, 12, 31)
- end
-
- # Parse or default the project due date
- project_due_date = if task.project&.due_date_at.is_a?(String)
- Date.parse(task&.project&.due_date_at)
- else
- task.project&.due_date_at || Date.new(9999, 12, 31)
- end
-
- # Priority in descending order (sorted values should be negative for sort_by)
- priority_value = -Task.priorities.fetch(task.priority, -1)
-
- # Determine sorting flags based on various criteria
- is_high_priority_proj_with_due_date = task.priority == 'high' && task.project&.due_date_at ? 0 : 1
- is_high_priority_with_due_date = task.priority == 'high' && task.due_date ? 0 : 1
- is_high_priority = task.priority == 'high' && !task.due_date && !task.project&.due_date_at ? 0 : 1
-
- is_medium_priority_proj_with_due_date = task.priority == 'medium' && task.project&.due_date_at ? 0 : 1
- is_medium_priority_with_due_date = task.priority == 'medium' && task.due_date ? 0 : 1
- is_medium_priority = task.priority == 'medium' && !task.due_date && !task.project&.due_date_at ? 0 : 1
-
- is_low_priority_proj_with_due_date = task.priority == 'low' && task.project&.due_date_at ? 0 : 1
- is_low_priority_with_due_date = task.priority == 'low' && task.due_date ? 0 : 1
- is_low_priority = task.priority == 'low' && !task.due_date && !task.project&.due_date_at ? 0 : 1
-
- # Primary sorting criteria - same as Task.sort_suggested_tasks
- [
- is_high_priority_proj_with_due_date,
- is_high_priority_with_due_date,
- is_high_priority,
-
- is_medium_priority_proj_with_due_date,
- is_medium_priority_with_due_date,
- is_medium_priority,
-
- is_low_priority_proj_with_due_date,
- is_low_priority_with_due_date,
- is_low_priority,
-
- task_due_date,
- project_due_date,
- priority_value
- ]
- end.first(5)
-
- if suggested_tasks.any?
- message += "💡 *Suggested Tasks \\(Top 3\\):*\n"
- # Only display the top 3 suggested tasks
- suggested_tasks.first(5).each_with_index do |task, index|
- priority_emoji =
- case task.priority
- when 'high' then '🔴'
- when 'medium' then '🟠'
- when 'low' then '🟢'
- else '⚪'
- end
-
- # Escape special characters in task name and project name
- task_name = escape_markdown(task.name)
- project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
- due_date = task.due_date ? " \\(Due: #{escape_markdown(task.due_date.strftime('%b %d'))}\\)" : ''
-
- message += "#{index + 1}\\. #{priority_emoji} #{task_name}#{project_info}#{due_date}\n"
- end
- message += "\n"
- end
-
- # Add a section divider
- message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
-
- # Add completed tasks for today if any
- if completed_today.any?
- message += "✅ *Completed Today:*\n"
- completed_today.order(updated_at: :desc).each_with_index do |task, index|
- # Escape special characters in task name and project name
- task_name = escape_markdown(task.name)
- project_info = task.project ? " \\[#{escape_markdown(task.project.name)}\\]" : ''
-
- message += "#{index + 1}\\. #{task_name}#{project_info}\n"
- end
- message += "\n"
- end
-
- # Add inbox count if available
- inbox_items_count = user.inbox_items.where(status: 'added').count
- if inbox_items_count > 0
- message += "*Inbox:*\n"
- message += "• You have #{inbox_items_count} item\\(s\\) in your inbox to process\\.\n\n"
- end
-
- # Add a section divider
- message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
- # Add a motivational note from the YAML file
- begin
- quotes_file = Rails.root.join('config', 'quotes.yml')
- quotes_data = YAML.load_file(quotes_file)['quotes']
-
- message += "💪 *Today's Motivation:*\n"
- quote = quotes_data.sample
- # Escape special characters in the quote
- message += escape_markdown(quote)
- rescue StandardError => e
- # Fallback to default quotes if there's an issue loading from YAML
- default_quotes = [
- 'Focus on progress, not perfection.',
- 'One task at a time leads to great accomplishments.',
- "Today's effort is tomorrow's success.",
- 'Small steps every day lead to big results.'
- ]
-
- message += "💪 *Today's Motivation:*\n"
- quote = default_quotes.sample
- # Escape special characters in the quote
- message += escape_markdown(quote)
- end
-
- message
- end
-
- def self.send_summary_to_user(user_id)
- user = User.find_by(id: user_id)
- return false unless user && user.telegram_bot_token && user.telegram_chat_id
-
- summary = generate_summary_for_user(user_id)
- return false unless summary
-
- # Send the message via Telegram
- begin
- TelegramPoller.instance.send_telegram_message(
- user.telegram_bot_token,
- user.telegram_chat_id,
- summary
- )
-
- # Update the last run time and calculate the next run time
- now = Time.now
- next_run = calculate_next_run_time(user, now)
-
- # Update the user's tracking fields
- user.update(
- task_summary_last_run: now,
- task_summary_next_run: next_run
- )
-
- true
- rescue StandardError => e
- puts "Error sending task summary to user #{user_id}: #{e.message}"
- false
- end
- end
-
- # Calculate when the next task summary should run based on frequency
- def self.calculate_next_run_time(user, from_time = Time.now)
- case user.task_summary_frequency
- when 'daily'
- # Next day at 7 AM
- from_time.tomorrow.change(hour: 7, min: 0, sec: 0)
- when 'weekdays'
- # If it's Friday, next is Monday, otherwise next day (if it's a weekday)
- days_until_next_weekday =
- if from_time.wday == 5 # Friday
- 3 # Next Monday
- elsif from_time.wday == 6 # Saturday
- 2 # Next Monday
- else
- 1 # Next day
- end
- from_time.advance(days: days_until_next_weekday).change(hour: 7, min: 0, sec: 0)
- when 'weekly'
- # Next week same day, or next Monday if we're being specific
- from_time.advance(days: 7).change(hour: 7, min: 0, sec: 0)
- when '1h'
- from_time + 1.hour
- when '2h'
- from_time + 2.hours
- when '4h'
- from_time + 4.hours
- when '8h'
- from_time + 8.hours
- when '12h'
- from_time + 12.hours
- else
- # Default to daily at 7 AM
- from_time.tomorrow.change(hour: 7, min: 0, sec: 0)
- end
- end
-end
diff --git a/app/services/url_title_extractor_service.rb b/app/services/url_title_extractor_service.rb
deleted file mode 100644
index 244a902..0000000
--- a/app/services/url_title_extractor_service.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-require 'net/http'
-require 'uri'
-require 'nokogiri'
-
-class UrlTitleExtractorService
- MAX_BYTES = 50_000
- TIMEOUT = 5
- USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
-
- def self.url?(text)
- url_regex = %r{^(https?://)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?$}i
- text.strip.match?(url_regex)
- end
-
- def self.extract_title(url)
- url = "http://#{url}" unless url.start_with?('http://') || url.start_with?('https://')
-
- begin
- uri = URI.parse(url)
- http = Net::HTTP.new(uri.host, uri.port)
-
- http.open_timeout = TIMEOUT
- http.read_timeout = TIMEOUT
-
- if uri.scheme == 'https'
- http.use_ssl = true
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
- end
-
- request = Net::HTTP::Get.new(uri.request_uri)
- request['User-Agent'] = USER_AGENT
- request['Accept'] = 'text/html'
- request['Range'] = "bytes=0-#{MAX_BYTES}"
-
- response = http.request(request)
-
- if response.is_a?(Net::HTTPRedirection)
- redirect_url = response['location']
- return extract_title(redirect_url)
- end
-
- if response.code.to_i.between?(200, 299) && response.body
- html = Nokogiri::HTML(response.body)
-
- title = html.at_css('title')&.text&.strip
- return title if title && !title.empty?
-
- og_title = html.at_css('meta[property="og:title"]')&.attributes&.[]('content')&.value&.strip
- return og_title if og_title && !og_title.empty?
-
- twitter_title = html.at_css('meta[name="twitter:title"]')&.attributes&.[]('content')&.value&.strip
- return twitter_title if twitter_title && !twitter_title.empty?
- end
-
- nil
- rescue StandardError => e
- puts "Error extracting title from URL: #{e.message}"
- nil
- end
- end
-
- def self.extract_title_from_text(text)
- text.split(/\s+/).each do |word|
- if url?(word)
- title = extract_title(word)
- return { url: word, title: title } if title
- end
- end
- nil
- end
-end
diff --git a/app/views/index.erb b/app/views/index.erb
deleted file mode 100644
index 93f8214..0000000
--- a/app/views/index.erb
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
- Tududi
-
-
-
-
-
diff --git a/app/views/layout.erb b/app/views/layout.erb
deleted file mode 100644
index 33a4622..0000000
--- a/app/views/layout.erb
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
- Tududi
-
-
- <%= yield %>
-
-
diff --git a/backend/.env.test b/backend/.env.test
new file mode 100644
index 0000000..b1bf7e1
--- /dev/null
+++ b/backend/.env.test
@@ -0,0 +1,4 @@
+# Test environment configuration
+NODE_ENV=test
+TUDUDI_SESSION_SECRET=test-secret-key-for-testing
+TUDUDI_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
\ No newline at end of file
diff --git a/backend/.sequelizerc b/backend/.sequelizerc
new file mode 100644
index 0000000..683bb52
--- /dev/null
+++ b/backend/.sequelizerc
@@ -0,0 +1,8 @@
+const path = require('path');
+
+module.exports = {
+ 'config': path.resolve('config', 'database.js'),
+ 'models-path': path.resolve('models'),
+ 'seeders-path': path.resolve('seeders'),
+ 'migrations-path': path.resolve('migrations')
+};
\ No newline at end of file
diff --git a/backend/app.js b/backend/app.js
new file mode 100644
index 0000000..5657f4f
--- /dev/null
+++ b/backend/app.js
@@ -0,0 +1,167 @@
+require('dotenv').config();
+const express = require('express');
+const path = require('path');
+const cors = require('cors');
+const helmet = require('helmet');
+const compression = require('compression');
+const morgan = require('morgan');
+const session = require('express-session');
+const SequelizeStore = require('connect-session-sequelize')(session.Store);
+const { sequelize } = require('./models');
+const { initializeTelegramPolling } = require('./services/telegramInitializer');
+const TaskScheduler = require('./services/taskScheduler');
+
+const app = express();
+
+// Session store
+const sessionStore = new SequelizeStore({
+ db: sequelize,
+});
+
+// Middlewares
+app.use(helmet());
+app.use(compression());
+app.use(morgan('combined'));
+
+// CORS configuration
+const allowedOrigins = process.env.TUDUDI_ALLOWED_ORIGINS
+ ? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map(origin => origin.trim())
+ : ['http://localhost:8080', 'http://localhost:9292', 'http://127.0.0.1:8080', 'http://127.0.0.1:9292'];
+
+app.use(cors({
+ origin: allowedOrigins,
+ credentials: true,
+ methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
+ allowedHeaders: ['Authorization', 'Content-Type', 'Accept', 'X-Requested-With'],
+ exposedHeaders: ['Content-Type'],
+ maxAge: 1728000
+}));
+
+// Body parsing
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Session configuration
+const secureFlag = process.env.NODE_ENV === 'production' && process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
+app.use(session({
+ secret: process.env.TUDUDI_SESSION_SECRET || require('crypto').randomBytes(64).toString('hex'),
+ store: sessionStore,
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ httpOnly: true,
+ secure: secureFlag,
+ maxAge: 2592000000, // 30 days
+ sameSite: secureFlag ? 'none' : 'lax'
+ }
+}));
+
+// Static files
+if (process.env.NODE_ENV === 'production') {
+ app.use(express.static(path.join(__dirname, 'dist')));
+} else {
+ app.use(express.static('public'));
+}
+
+// Serve locales
+if (process.env.NODE_ENV === 'production') {
+ app.use('/locales', express.static(path.join(__dirname, 'dist/locales')));
+} else {
+ app.use('/locales', express.static(path.join(__dirname, '../public/locales')));
+}
+
+// Authentication middleware
+const { requireAuth } = require('./middleware/auth');
+
+// Routes
+app.use('/api', require('./routes/auth'));
+app.use('/api', requireAuth, require('./routes/tasks'));
+app.use('/api', requireAuth, require('./routes/projects'));
+app.use('/api', requireAuth, require('./routes/areas'));
+app.use('/api', requireAuth, require('./routes/notes'));
+app.use('/api', requireAuth, require('./routes/tags'));
+app.use('/api', requireAuth, require('./routes/users'));
+app.use('/api', requireAuth, require('./routes/inbox'));
+app.use('/api', requireAuth, require('./routes/url'));
+app.use('/api', requireAuth, require('./routes/telegram'));
+app.use('/api', requireAuth, require('./routes/quotes'));
+
+// Health check
+app.get('/api/health', (req, res) => {
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
+});
+
+// SPA fallback
+app.get('*', (req, res) => {
+ if (!req.path.startsWith('/api/') && !req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
+ if (process.env.NODE_ENV === 'production') {
+ res.sendFile(path.join(__dirname, 'dist', 'index.html'));
+ } else {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+ }
+ } else {
+ res.status(404).json({ error: 'Not Found', message: 'The requested resource could not be found.' });
+ }
+});
+
+// Error handling
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).json({ error: 'Internal Server Error', message: err.message });
+});
+
+const PORT = process.env.PORT || 3002;
+
+// Initialize database and start server
+async function startServer() {
+ try {
+ // Create session store table
+ await sessionStore.sync();
+
+ // Sync database
+ await sequelize.sync();
+
+ // Auto-create user if not exists
+ if (process.env.TUDUDI_USER_EMAIL && process.env.TUDUDI_USER_PASSWORD) {
+ const { User } = require('./models');
+ const bcrypt = require('bcrypt');
+
+ const [user, created] = await User.findOrCreate({
+ where: { email: process.env.TUDUDI_USER_EMAIL },
+ defaults: {
+ email: process.env.TUDUDI_USER_EMAIL,
+ password: await bcrypt.hash(process.env.TUDUDI_USER_PASSWORD, 10)
+ }
+ });
+
+ if (created) {
+ console.log('Default user created:', user.email);
+ }
+ }
+
+ // Initialize Telegram polling after database is ready
+ await initializeTelegramPolling();
+
+ // Initialize task scheduler
+ const scheduler = TaskScheduler.getInstance();
+ await scheduler.initialize();
+
+ const server = app.listen(PORT, '0.0.0.0', () => {
+ console.log(`Server running on port ${PORT}`);
+ console.log(`Server listening on http://localhost:${PORT}`);
+ });
+
+ server.on('error', (err) => {
+ console.error('Server error:', err);
+ });
+ } catch (error) {
+ console.error('Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+if (require.main === module) {
+ startServer();
+}
+
+module.exports = app;
\ No newline at end of file
diff --git a/backend/config/database.js b/backend/config/database.js
new file mode 100644
index 0000000..05afcff
--- /dev/null
+++ b/backend/config/database.js
@@ -0,0 +1,42 @@
+require('dotenv').config();
+const path = require('path');
+
+const dbPath = process.env.DATABASE_URL
+ ? process.env.DATABASE_URL.replace('sqlite:///', '')
+ : path.join(__dirname, '..', 'db');
+
+module.exports = {
+ development: {
+ dialect: 'sqlite',
+ storage: path.join(dbPath, 'development.sqlite3'),
+ logging: console.log,
+ define: {
+ timestamps: true,
+ underscored: true,
+ createdAt: 'created_at',
+ updatedAt: 'updated_at'
+ }
+ },
+ test: {
+ dialect: 'sqlite',
+ storage: path.join(dbPath, 'test.sqlite3'),
+ logging: false,
+ define: {
+ timestamps: true,
+ underscored: true,
+ createdAt: 'created_at',
+ updatedAt: 'updated_at'
+ }
+ },
+ production: {
+ dialect: 'sqlite',
+ storage: path.join(dbPath, 'production.sqlite3'),
+ logging: false,
+ define: {
+ timestamps: true,
+ underscored: true,
+ createdAt: 'created_at',
+ updatedAt: 'updated_at'
+ }
+ }
+};
\ No newline at end of file
diff --git a/config/quotes.yml b/backend/config/quotes.yml
similarity index 100%
rename from config/quotes.yml
rename to backend/config/quotes.yml
diff --git a/backend/jest.config.js b/backend/jest.config.js
new file mode 100644
index 0000000..839bcc6
--- /dev/null
+++ b/backend/jest.config.js
@@ -0,0 +1,25 @@
+module.exports = {
+ testEnvironment: 'node',
+ setupFilesAfterEnv: ['/tests/helpers/setup.js'],
+ testMatch: [
+ '/tests/**/*.test.js',
+ '/tests/**/*.spec.js'
+ ],
+ maxWorkers: 1,
+ collectCoverageFrom: [
+ 'routes/**/*.js',
+ 'models/**/*.js',
+ 'middleware/**/*.js',
+ 'services/**/*.js',
+ '!models/index.js',
+ '!**/*.test.js',
+ '!**/*.spec.js'
+ ],
+ coverageDirectory: 'coverage',
+ coverageReporters: ['text', 'lcov', 'html'],
+ verbose: true,
+ forceExit: true,
+ clearMocks: true,
+ resetMocks: true,
+ restoreMocks: true
+};
\ No newline at end of file
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
new file mode 100644
index 0000000..0b99144
--- /dev/null
+++ b/backend/middleware/auth.js
@@ -0,0 +1,30 @@
+const { User } = require('../models');
+
+const requireAuth = async (req, res, next) => {
+ try {
+ // Skip authentication for health check, login routes, and current_user
+ if (req.path === '/api/health' || req.path === '/api/login' || req.path === '/api/current_user') {
+ return next();
+ }
+
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ req.session.destroy();
+ return res.status(401).json({ error: 'User not found' });
+ }
+
+ req.currentUser = user;
+ next();
+ } catch (error) {
+ console.error('Authentication error:', error);
+ res.status(500).json({ error: 'Authentication error' });
+ }
+};
+
+module.exports = {
+ requireAuth
+};
\ No newline at end of file
diff --git a/backend/migrations/20250615000001-create-users.js b/backend/migrations/20250615000001-create-users.js
new file mode 100644
index 0000000..79cee6f
--- /dev/null
+++ b/backend/migrations/20250615000001-create-users.js
@@ -0,0 +1,61 @@
+'use strict';
+
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ await queryInterface.createTable('users', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ email: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ unique: true
+ },
+ password: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ telegram_bot_token: {
+ type: Sequelize.STRING,
+ allowNull: true
+ },
+ telegram_chat_id: {
+ type: Sequelize.STRING,
+ allowNull: true
+ },
+ task_summary_enabled: {
+ type: Sequelize.BOOLEAN,
+ defaultValue: false
+ },
+ task_summary_frequency: {
+ type: Sequelize.STRING,
+ defaultValue: 'daily'
+ },
+ task_summary_last_run: {
+ type: Sequelize.DATE,
+ allowNull: true
+ },
+ task_summary_next_run: {
+ type: Sequelize.DATE,
+ allowNull: true
+ },
+ created_at: {
+ allowNull: false,
+ type: Sequelize.DATE,
+ defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
+ },
+ updated_at: {
+ allowNull: false,
+ type: Sequelize.DATE,
+ defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
+ }
+ });
+ },
+
+ async down(queryInterface, Sequelize) {
+ await queryInterface.dropTable('users');
+ }
+};
\ No newline at end of file
diff --git a/backend/models/area.js b/backend/models/area.js
new file mode 100644
index 0000000..61bf11c
--- /dev/null
+++ b/backend/models/area.js
@@ -0,0 +1,36 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Area = sequelize.define('Area', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ description: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ user_id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ }
+ }, {
+ tableName: 'areas',
+ indexes: [
+ {
+ fields: ['user_id']
+ }
+ ]
+ });
+
+ return Area;
+};
\ No newline at end of file
diff --git a/backend/models/inbox_item.js b/backend/models/inbox_item.js
new file mode 100644
index 0000000..6bf854a
--- /dev/null
+++ b/backend/models/inbox_item.js
@@ -0,0 +1,42 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const InboxItem = sequelize.define('InboxItem', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ content: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ status: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'added'
+ },
+ source: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'tududi'
+ },
+ user_id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ }
+ }, {
+ tableName: 'inbox_items',
+ indexes: [
+ {
+ fields: ['user_id']
+ }
+ ]
+ });
+
+ return InboxItem;
+};
\ No newline at end of file
diff --git a/backend/models/index.js b/backend/models/index.js
new file mode 100644
index 0000000..005d36f
--- /dev/null
+++ b/backend/models/index.js
@@ -0,0 +1,93 @@
+const { Sequelize } = require('sequelize');
+const path = require('path');
+
+// Database configuration
+let dbConfig;
+
+if (process.env.NODE_ENV === 'test') {
+ // Use in-memory database for tests
+ dbConfig = {
+ dialect: 'sqlite',
+ storage: ':memory:',
+ logging: false,
+ define: {
+ timestamps: true,
+ underscored: true,
+ createdAt: 'created_at',
+ updatedAt: 'updated_at'
+ }
+ };
+} else {
+ const dbPath = process.env.DATABASE_URL
+ ? process.env.DATABASE_URL.replace('sqlite:///', '')
+ : path.join(__dirname, '../db', process.env.NODE_ENV === 'production' ? 'production.sqlite3' : 'development.sqlite3');
+
+ dbConfig = {
+ dialect: 'sqlite',
+ storage: dbPath,
+ logging: process.env.NODE_ENV === 'development' ? console.log : false,
+ define: {
+ timestamps: true,
+ underscored: true,
+ createdAt: 'created_at',
+ updatedAt: 'updated_at'
+ }
+ };
+}
+
+const sequelize = new Sequelize(dbConfig);
+
+// Import models
+const User = require('./user')(sequelize);
+const Area = require('./area')(sequelize);
+const Project = require('./project')(sequelize);
+const Task = require('./task')(sequelize);
+const Tag = require('./tag')(sequelize);
+const Note = require('./note')(sequelize);
+const InboxItem = require('./inbox_item')(sequelize);
+
+// Define associations
+User.hasMany(Area, { foreignKey: 'user_id' });
+Area.belongsTo(User, { foreignKey: 'user_id' });
+
+User.hasMany(Project, { foreignKey: 'user_id' });
+Project.belongsTo(User, { foreignKey: 'user_id' });
+Project.belongsTo(Area, { foreignKey: 'area_id', allowNull: true });
+Area.hasMany(Project, { foreignKey: 'area_id' });
+
+User.hasMany(Task, { foreignKey: 'user_id' });
+Task.belongsTo(User, { foreignKey: 'user_id' });
+Task.belongsTo(Project, { foreignKey: 'project_id', allowNull: true });
+Project.hasMany(Task, { foreignKey: 'project_id' });
+
+User.hasMany(Tag, { foreignKey: 'user_id' });
+Tag.belongsTo(User, { foreignKey: 'user_id' });
+
+User.hasMany(Note, { foreignKey: 'user_id' });
+Note.belongsTo(User, { foreignKey: 'user_id' });
+Note.belongsTo(Project, { foreignKey: 'project_id', allowNull: true });
+Project.hasMany(Note, { foreignKey: 'project_id' });
+
+User.hasMany(InboxItem, { foreignKey: 'user_id' });
+InboxItem.belongsTo(User, { foreignKey: 'user_id' });
+
+// Many-to-many associations
+Task.belongsToMany(Tag, { through: 'tasks_tags', foreignKey: 'task_id', otherKey: 'tag_id' });
+Tag.belongsToMany(Task, { through: 'tasks_tags', foreignKey: 'tag_id', otherKey: 'task_id' });
+
+Note.belongsToMany(Tag, { through: 'notes_tags', foreignKey: 'note_id', otherKey: 'tag_id' });
+Tag.belongsToMany(Note, { through: 'notes_tags', foreignKey: 'tag_id', otherKey: 'note_id' });
+
+Project.belongsToMany(Tag, { through: 'projects_tags', foreignKey: 'project_id', otherKey: 'tag_id' });
+Tag.belongsToMany(Project, { through: 'projects_tags', foreignKey: 'tag_id', otherKey: 'project_id' });
+
+module.exports = {
+ sequelize,
+ User,
+ Area,
+ Project,
+ Task,
+ Tag,
+ Note,
+ InboxItem
+};
\ No newline at end of file
diff --git a/backend/models/note.js b/backend/models/note.js
new file mode 100644
index 0000000..a704a4c
--- /dev/null
+++ b/backend/models/note.js
@@ -0,0 +1,47 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Note = sequelize.define('Note', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ title: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ content: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ user_id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ },
+ project_id: {
+ type: DataTypes.INTEGER,
+ allowNull: true,
+ references: {
+ model: 'projects',
+ key: 'id'
+ }
+ }
+ }, {
+ tableName: 'notes',
+ indexes: [
+ {
+ fields: ['user_id']
+ },
+ {
+ fields: ['project_id']
+ }
+ ]
+ });
+
+ return Note;
+};
\ No newline at end of file
diff --git a/backend/models/project.js b/backend/models/project.js
new file mode 100644
index 0000000..af801f3
--- /dev/null
+++ b/backend/models/project.js
@@ -0,0 +1,69 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Project = sequelize.define('Project', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ active: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false
+ },
+ pin_to_sidebar: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false
+ },
+ priority: {
+ type: DataTypes.INTEGER,
+ allowNull: true,
+ validate: {
+ min: 0,
+ max: 2
+ }
+ },
+ due_date_at: {
+ type: DataTypes.DATE,
+ allowNull: true
+ },
+ user_id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ },
+ area_id: {
+ type: DataTypes.INTEGER,
+ allowNull: true,
+ references: {
+ model: 'areas',
+ key: 'id'
+ }
+ }
+ }, {
+ tableName: 'projects',
+ indexes: [
+ {
+ fields: ['user_id']
+ },
+ {
+ fields: ['area_id']
+ }
+ ]
+ });
+
+ return Project;
+};
\ No newline at end of file
diff --git a/backend/models/tag.js b/backend/models/tag.js
new file mode 100644
index 0000000..e4894ff
--- /dev/null
+++ b/backend/models/tag.js
@@ -0,0 +1,32 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Tag = sequelize.define('Tag', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ user_id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ }
+ }, {
+ tableName: 'tags',
+ indexes: [
+ {
+ fields: ['user_id']
+ }
+ ]
+ });
+
+ return Tag;
+};
\ No newline at end of file
diff --git a/backend/models/task.js b/backend/models/task.js
new file mode 100644
index 0000000..805da80
--- /dev/null
+++ b/backend/models/task.js
@@ -0,0 +1,127 @@
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Task = sequelize.define('Task', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ due_date: {
+ type: DataTypes.DATE,
+ allowNull: true
+ },
+ today: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false
+ },
+ priority: {
+ type: DataTypes.INTEGER,
+ allowNull: true,
+ defaultValue: 0,
+ validate: {
+ min: 0,
+ max: 2
+ }
+ },
+ status: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ defaultValue: 0,
+ validate: {
+ min: 0,
+ max: 4
+ }
+ },
+ note: {
+ type: DataTypes.TEXT,
+ allowNull: true
+ },
+ recurrence_type: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'none'
+ },
+ recurrence_interval: {
+ type: DataTypes.INTEGER,
+ allowNull: true
+ },
+ recurrence_end_date: {
+ type: DataTypes.DATE,
+ allowNull: true
+ },
+ last_generated_date: {
+ type: DataTypes.DATE,
+ allowNull: true
+ },
+ user_id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
+ },
+ project_id: {
+ type: DataTypes.INTEGER,
+ allowNull: true,
+ references: {
+ model: 'projects',
+ key: 'id'
+ }
+ }
+ }, {
+ tableName: 'tasks',
+ indexes: [
+ {
+ fields: ['user_id']
+ },
+ {
+ fields: ['project_id']
+ },
+ {
+ fields: ['recurrence_type']
+ },
+ {
+ fields: ['last_generated_date']
+ }
+ ]
+ });
+
+ // Define enum constants
+ Task.PRIORITY = {
+ LOW: 0,
+ MEDIUM: 1,
+ HIGH: 2
+ };
+
+ Task.STATUS = {
+ NOT_STARTED: 0,
+ IN_PROGRESS: 1,
+ DONE: 2,
+ ARCHIVED: 3,
+ WAITING: 4
+ };
+
+ // Instance methods for priority and status
+ Task.prototype.getPriorityName = function() {
+ const priorities = ['low', 'medium', 'high'];
+ return priorities[this.priority] || 'low';
+ };
+
+ Task.prototype.getStatusName = function() {
+ const statuses = ['not_started', 'in_progress', 'done', 'archived', 'waiting'];
+ return statuses[this.status] || 'not_started';
+ };
+
+ return Task;
+};
\ No newline at end of file
diff --git a/backend/models/user.js b/backend/models/user.js
new file mode 100644
index 0000000..45c5bd7
--- /dev/null
+++ b/backend/models/user.js
@@ -0,0 +1,100 @@
+const { DataTypes } = require('sequelize');
+const bcrypt = require('bcrypt');
+
+module.exports = (sequelize) => {
+ const User = sequelize.define('User', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ email: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ unique: true,
+ validate: {
+ isEmail: true
+ }
+ },
+ password_digest: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ field: 'password_digest'
+ },
+ appearance: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'light',
+ validate: {
+ isIn: [['light', 'dark']]
+ }
+ },
+ language: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'en'
+ },
+ timezone: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'UTC'
+ },
+ avatar_image: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ telegram_bot_token: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ telegram_chat_id: {
+ type: DataTypes.STRING,
+ allowNull: true
+ },
+ task_summary_enabled: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false
+ },
+ task_summary_frequency: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ defaultValue: 'daily',
+ validate: {
+ isIn: [['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h']]
+ }
+ },
+ task_summary_last_run: {
+ type: DataTypes.DATE,
+ allowNull: true
+ },
+ task_summary_next_run: {
+ type: DataTypes.DATE,
+ allowNull: true
+ }
+ }, {
+ tableName: 'users',
+ hooks: {
+ beforeValidate: async (user) => {
+ if (user.password) {
+ user.password_digest = await bcrypt.hash(user.password, 10);
+ }
+ }
+ }
+ });
+
+ // Virtual field for password
+ User.prototype.setPassword = async function(password) {
+ this.password_digest = await bcrypt.hash(password, 10);
+ };
+
+ User.prototype.checkPassword = async function(password) {
+ return await bcrypt.compare(password, this.password_digest);
+ };
+
+ return User;
+};
\ No newline at end of file
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000..fff7bd9
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,8045 @@
+{
+ "name": "backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "backend",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "compression": "^1.8.0",
+ "connect-session-sequelize": "^7.1.7",
+ "cors": "^2.8.5",
+ "dotenv": "^16.5.0",
+ "express": "^4.18.2",
+ "express-session": "^1.18.1",
+ "helmet": "^8.1.0",
+ "js-yaml": "^4.1.0",
+ "morgan": "^1.10.0",
+ "multer": "^2.0.1",
+ "node-cron": "^4.1.0",
+ "sequelize": "^6.37.7",
+ "sqlite3": "^5.1.7"
+ },
+ "devDependencies": {
+ "cross-env": "^7.0.3",
+ "jest": "^30.0.0",
+ "nodemon": "^3.0.1",
+ "sequelize-cli": "^6.6.2",
+ "supertest": "^7.1.1"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
+ "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.27.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
+ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.4",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.27.4",
+ "@babel/types": "^7.27.3",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/core/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
+ "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.5",
+ "@babel/types": "^7.27.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
+ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
+ "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
+ "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.3",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
+ "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
+ "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.0.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
+ "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
+ "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@gar/promisify": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
+ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.0.tgz",
+ "integrity": "sha512-vfpJap6JZQ3I8sUN8dsFqNAKJYO4KIGxkcB+3Fw7Q/BJiWY5HwtMMiuT1oP0avsiDhjE/TCLaDgbGfHwDdBVeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "jest-message-util": "30.0.0",
+ "jest-util": "30.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.0.tgz",
+ "integrity": "sha512-1zU39zFtWSl5ZuDK3Rd6P8S28MmS4F11x6Z4CURrgJ99iaAJg68hmdJ2SAHEEO6ociaNk43UhUYtHxWKEWoNYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "30.0.0",
+ "@jest/pattern": "30.0.0",
+ "@jest/reporters": "30.0.0",
+ "@jest/test-result": "30.0.0",
+ "@jest/transform": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "exit-x": "^0.2.2",
+ "graceful-fs": "^4.2.11",
+ "jest-changed-files": "30.0.0",
+ "jest-config": "30.0.0",
+ "jest-haste-map": "30.0.0",
+ "jest-message-util": "30.0.0",
+ "jest-regex-util": "30.0.0",
+ "jest-resolve": "30.0.0",
+ "jest-resolve-dependencies": "30.0.0",
+ "jest-runner": "30.0.0",
+ "jest-runtime": "30.0.0",
+ "jest-snapshot": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-validate": "30.0.0",
+ "jest-watcher": "30.0.0",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/diff-sequences": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.0.tgz",
+ "integrity": "sha512-xMbtoCeKJDto86GW6AiwVv7M4QAuI56R7dVBr1RNGYbOT44M2TIzOiske2RxopBqkumDY+A1H55pGvuribRY9A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.0.tgz",
+ "integrity": "sha512-09sFbMMgS5JxYnvgmmtwIHhvoyzvR5fUPrVl8nOCrC5KdzmmErTcAxfWyAhJ2bv3rvHNQaKiS+COSG+O7oNbXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "jest-mock": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.0.tgz",
+ "integrity": "sha512-XZ3j6syhMeKiBknmmc8V3mNIb44kxLTbOQtaXA4IFdHy+vEN0cnXRzbRjdGBtrp4k1PWyMWNU3Fjz3iejrhpQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "30.0.0",
+ "jest-snapshot": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.0.tgz",
+ "integrity": "sha512-UiWfsqNi/+d7xepfOv8KDcbbzcYtkWBe3a3kVDtg6M1kuN6CJ7b4HzIp5e1YHrSaQaVS8sdCoyCMCZClTLNKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.0.tgz",
+ "integrity": "sha512-yzBmJcrMHAMcAEbV2w1kbxmx8WFpEz8Cth3wjLMSkq+LO8VeGKRhpr5+BUp7PPK+x4njq/b6mVnDR8e/tPL5ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.0",
+ "@sinonjs/fake-timers": "^13.0.0",
+ "@types/node": "*",
+ "jest-message-util": "30.0.0",
+ "jest-mock": "30.0.0",
+ "jest-util": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/get-type": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.0.tgz",
+ "integrity": "sha512-VZWMjrBzqfDKngQ7sUctKeLxanAbsBFoZnPxNIG6CmxK7Gv6K44yqd0nzveNIBfuhGZMmk1n5PGbvdSTOu0yTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.0.tgz",
+ "integrity": "sha512-OEzYes5A1xwBJVMPqFRa8NCao8Vr42nsUZuf/SpaJWoLE+4kyl6nCQZ1zqfipmCrIXQVALC5qJwKy/7NQQLPhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.0",
+ "@jest/expect": "30.0.0",
+ "@jest/types": "30.0.0",
+ "jest-mock": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/pattern": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.0.tgz",
+ "integrity": "sha512-k+TpEThzLVXMkbdxf8KHjZ83Wl+G54ytVJoDIGWwS96Ql4xyASRjc6SU1hs5jHVql+hpyK9G8N7WuFhLpGHRpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-regex-util": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.0.tgz",
+ "integrity": "sha512-5WHNlLO0Ok+/o6ML5IzgVm1qyERtLHBNhwn67PAq92H4hZ+n5uW/BYj1VVwmTdxIcNrZLxdV9qtpdZkXf16HxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "30.0.0",
+ "@jest/test-result": "30.0.0",
+ "@jest/transform": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "collect-v8-coverage": "^1.0.2",
+ "exit-x": "^0.2.2",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.11",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^5.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-worker": "30.0.0",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.2",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.0.tgz",
+ "integrity": "sha512-NID2VRyaEkevCRz6badhfqYwri/RvMbiHY81rk3AkK/LaiB0LSxi1RdVZ7MpZdTjNugtZeGfpL0mLs9Kp3MrQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/snapshot-utils": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.0.tgz",
+ "integrity": "sha512-C/QSFUmvZEYptg2Vin84FggAphwHvj6la39vkw1CNOZQORWZ7O/H0BXmdeeeGnvlXDYY8TlFM5jgFnxLAxpFjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.0",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "natural-compare": "^1.4.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.0.tgz",
+ "integrity": "sha512-oYBJ4d/NF4ZY3/7iq1VaeoERHRvlwKtrGClgescaXMIa1mmb+vfJd0xMgbW9yrI80IUA7qGbxpBWxlITrHkWoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "callsites": "^3.1.0",
+ "graceful-fs": "^4.2.11"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.0.tgz",
+ "integrity": "sha512-685zco9HdgBaaWiB9T4xjLtBuN0Q795wgaQPpmuAeZPHwHZSoKFAUnozUtU+ongfi4l5VCz8AclOE5LAQdyjxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "collect-v8-coverage": "^1.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.0.tgz",
+ "integrity": "sha512-Hmvv5Yg6UmghXIcVZIydkT0nAK7M/hlXx9WMHR5cLVwdmc14/qUQt3mC72T6GN0olPC6DhmKE6Cd/pHsgDbuqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "30.0.0",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.0.tgz",
+ "integrity": "sha512-8xhpsCGYJsUjqpJOgLyMkeOSSlhqggFZEWAnZquBsvATtueoEs7CkMRxOUmJliF3E5x+mXmZ7gEEsHank029Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@jest/types": "30.0.0",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "babel-plugin-istanbul": "^7.0.0",
+ "chalk": "^4.1.2",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.0",
+ "jest-regex-util": "30.0.0",
+ "jest-util": "30.0.0",
+ "micromatch": "^4.0.8",
+ "pirates": "^4.0.7",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^5.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.0.tgz",
+ "integrity": "sha512-1Nox8mAL52PKPfEnUQWBvKU/bp8FTT6AiDu76bFDEJj/qsRFSAVSldfCH3XYMqialti2zHXKvD5gN0AaHc0yKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.0",
+ "@jest/schemas": "30.0.0",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
+ "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.9.0"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
+ "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "@gar/promisify": "^1.0.1",
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/@npmcli/move-file": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
+ "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/move-file/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@paralleldrive/cuid2": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
+ "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.1.5"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
+ "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.35",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.35.tgz",
+ "integrity": "sha512-C6ypdODf2VZkgRT6sFM8E1F8vR+HcffniX0Kp8MsU8PIfrlXbNCBz0jzj17GjdmjTx1OtZzdH8+iALL21UjF5A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
+ "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+ "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.0.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz",
+ "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/validator": {
+ "version": "13.15.1",
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.1.tgz",
+ "integrity": "sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.33",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
+ "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.0.tgz",
+ "integrity": "sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-android-arm64": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.0.tgz",
+ "integrity": "sha512-sG1NHtgXtX8owEkJ11yn34vt0Xqzi3k9TJ8zppDmyG8GZV4kVWw44FHwKwHeEFl07uKPeC4ZoyuQaGh5ruJYPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.0.tgz",
+ "integrity": "sha512-nJ9z47kfFnCxN1z/oYZS7HSNsFh43y2asePzTEZpEvK7kGyuShSl3RRXnm/1QaqFL+iP+BjMwuB+DYUymOkA5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-x64": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.0.tgz",
+ "integrity": "sha512-TK+UA1TTa0qS53rjWn7cVlEKVGz2B6JYe0C++TdQjvWYIyx83ruwh0wd4LRxYBM5HeuAzXcylA9BH2trARXJTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-freebsd-x64": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.0.tgz",
+ "integrity": "sha512-6uZwzMRFcD7CcCd0vz3Hp+9qIL2jseE/bx3ZjaLwn8t714nYGwiE84WpaMCYjU+IQET8Vu/+BNAGtYD7BG/0yA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.0.tgz",
+ "integrity": "sha512-bPUBksQfrgcfv2+mm+AZinaKq8LCFvt5PThYqRotqSuuZK1TVKkhbVMS/jvSRfYl7jr3AoZLYbDkItxgqMKRkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.0.tgz",
+ "integrity": "sha512-uT6E7UBIrTdCsFQ+y0tQd3g5oudmrS/hds5pbU3h4s2t/1vsGWbbSKhBSCD9mcqaqkBwoqlECpUrRJCmldl8PA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.0.tgz",
+ "integrity": "sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.0.tgz",
+ "integrity": "sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.0.tgz",
+ "integrity": "sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.0.tgz",
+ "integrity": "sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.0.tgz",
+ "integrity": "sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.0.tgz",
+ "integrity": "sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.0.tgz",
+ "integrity": "sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.0.tgz",
+ "integrity": "sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.0.tgz",
+ "integrity": "sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.0.tgz",
+ "integrity": "sha512-rknkrTRuvujprrbPmGeHi8wYWxmNVlBoNW8+4XF2hXUnASOjmuC9FNF1tGbDiRQWn264q9U/oGtixyO3BT8adQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.0.tgz",
+ "integrity": "sha512-Ceymm+iBl+bgAICtgiHyMLz6hjxmLJKqBim8tDzpX61wpZOx2bPK6Gjuor7I2RiUynVjvvkoRIkrPyMwzBzF3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.0.tgz",
+ "integrity": "sha512-k59o9ZyeyS0hAlcaKFezYSH2agQeRFEB7KoQLXl3Nb3rgkqT1NY9Vwy+SqODiLmYnEjxWJVRE/yq2jFVqdIxZw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/agent-base/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/agent-base/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/aproba": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "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/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/babel-jest": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.0.tgz",
+ "integrity": "sha512-JQ0DhdFjODbSawDf0026uZuwaqfKkQzk+9mwWkq2XkKFIaMhFVOxlVmbFCOnnC76jATdxrff3IiUAvOAJec6tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "30.0.0",
+ "@types/babel__core": "^7.20.5",
+ "babel-plugin-istanbul": "^7.0.0",
+ "babel-preset-jest": "30.0.0",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz",
+ "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-instrument": "^6.0.2",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.0.tgz",
+ "integrity": "sha512-DSRm+US/FCB4xPDD6Rnslb6PAF9Bej1DZ+1u4aTiqJnk7ZX12eHsnDiIOqjGvITCq+u6wLqUhgS+faCNbVY8+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.3",
+ "@types/babel__core": "^7.20.5"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
+ "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.0.tgz",
+ "integrity": "sha512-hgEuu/W7gk8QOWUA9+m3Zk+WpGvKc1Egp6rFQEfYxEoM9Fk/q8nuTXNL65OkhwGrTApauEGgakOoWVXj+UfhKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "30.0.0",
+ "babel-preset-current-node-syntax": "^1.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.0"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/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/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
+ "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001718",
+ "electron-to-chromium": "^1.5.160",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz",
+ "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "@npmcli/fs": "^1.0.0",
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.1",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cacache/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001723",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
+ "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
+ "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
+ "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/connect-session-sequelize": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-7.1.7.tgz",
+ "integrity": "sha512-Wqq7rg0w+9bOVs6jC0nhZnssXJ3+iKNlDVWn2JfBuBPoY7oYaxzxfBKeUYrX6dHt3OWEWbZV6LJvapwi76iBQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "peerDependencies": {
+ "sequelize": ">= 6.1.0"
+ }
+ },
+ "node_modules/connect-session-sequelize/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/connect-session-sequelize/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
+ "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dottie": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
+ "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+ "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/editorconfig/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/editorconfig/node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.167",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
+ "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emittery": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
+ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit-x": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
+ "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expect": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.0.tgz",
+ "integrity": "sha512-xCdPp6gwiR9q9lsPCHANarIkFTN/IMZso6Kkq03sOm9IIGtzK/UJqml0dkhHibGh8HKOj8BIDIpZ0BZuU7QK6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "30.0.0",
+ "@jest/get-type": "30.0.0",
+ "jest-matcher-utils": "30.0.0",
+ "jest-message-util": "30.0.0",
+ "jest-mock": "30.0.0",
+ "jest-util": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-session": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+ "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.7",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
+ "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/formidable": {
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
+ "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@paralleldrive/cuid2": "^2.2.2",
+ "dezalgo": "^1.0.4",
+ "once": "^1.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+ "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "license": "BSD-2-Clause",
+ "optional": true
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/inflection": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
+ "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
+ "engines": [
+ "node >= 0.4.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.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",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jest": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.0.tgz",
+ "integrity": "sha512-/3G2iFwsUY95vkflmlDn/IdLyLWqpQXcftptooaPH4qkyU52V7qVYf1BjmdSPlp1+0fs6BmNtrGaSFwOfV07ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "30.0.0",
+ "@jest/types": "30.0.0",
+ "import-local": "^3.2.0",
+ "jest-cli": "30.0.0"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.0.tgz",
+ "integrity": "sha512-rzGpvCdPdEV1Ma83c1GbZif0L2KAm3vXSXGRlpx7yCt0vhruwCNouKNRh3SiVcISHP1mb3iJzjb7tAEnNu1laQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^5.1.1",
+ "jest-util": "30.0.0",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.0.tgz",
+ "integrity": "sha512-nTwah78qcKVyndBS650hAkaEmwWGaVsMMoWdJwMnH77XArRJow2Ir7hc+8p/mATtxVZuM9OTkA/3hQocRIK5Dw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.0",
+ "@jest/expect": "30.0.0",
+ "@jest/test-result": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "co": "^4.6.0",
+ "dedent": "^1.6.0",
+ "is-generator-fn": "^2.1.0",
+ "jest-each": "30.0.0",
+ "jest-matcher-utils": "30.0.0",
+ "jest-message-util": "30.0.0",
+ "jest-runtime": "30.0.0",
+ "jest-snapshot": "30.0.0",
+ "jest-util": "30.0.0",
+ "p-limit": "^3.1.0",
+ "pretty-format": "30.0.0",
+ "pure-rand": "^7.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.0.tgz",
+ "integrity": "sha512-fWKAgrhlwVVCfeizsmIrPRTBYTzO82WSba3gJniZNR3PKXADgdC0mmCSK+M+t7N8RCXOVfY6kvCkvjUNtzmHYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "30.0.0",
+ "@jest/test-result": "30.0.0",
+ "@jest/types": "30.0.0",
+ "chalk": "^4.1.2",
+ "exit-x": "^0.2.2",
+ "import-local": "^3.2.0",
+ "jest-config": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-validate": "30.0.0",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-cli/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jest-cli/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jest-cli/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.0.tgz",
+ "integrity": "sha512-p13a/zun+sbOMrBnTEUdq/5N7bZMOGd1yMfqtAJniPNuzURMay4I+vxZLK1XSDbjvIhmeVdG8h8RznqYyjctyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@jest/get-type": "30.0.0",
+ "@jest/pattern": "30.0.0",
+ "@jest/test-sequencer": "30.0.0",
+ "@jest/types": "30.0.0",
+ "babel-jest": "30.0.0",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "deepmerge": "^4.3.1",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.11",
+ "jest-circus": "30.0.0",
+ "jest-docblock": "30.0.0",
+ "jest-environment-node": "30.0.0",
+ "jest-regex-util": "30.0.0",
+ "jest-resolve": "30.0.0",
+ "jest-runner": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-validate": "30.0.0",
+ "micromatch": "^4.0.8",
+ "parse-json": "^5.2.0",
+ "pretty-format": "30.0.0",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "esbuild-register": ">=3.4.0",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "esbuild-register": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-config/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-config/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/jest-config/node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.0.tgz",
+ "integrity": "sha512-TgT1+KipV8JTLXXeFX0qSvIJR/UXiNNojjxb/awh3vYlBZyChU/NEmyKmq+wijKjWEztyrGJFL790nqMqNjTHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/diff-sequences": "30.0.0",
+ "@jest/get-type": "30.0.0",
+ "chalk": "^4.1.2",
+ "pretty-format": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.0.tgz",
+ "integrity": "sha512-By/iQ0nvTzghEecGzUMCp1axLtBh+8wB4Hpoi5o+x1stycjEmPcH1mHugL4D9Q+YKV++vKeX/3ZTW90QC8ICPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.0.tgz",
+ "integrity": "sha512-qkFEW3cfytEjG2KtrhwtldZfXYnWSanO8xUMXLe4A6yaiHMHJUalk0Yyv4MQH6aeaxgi4sGVrukvF0lPMM7U1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.0",
+ "@jest/types": "30.0.0",
+ "chalk": "^4.1.2",
+ "jest-util": "30.0.0",
+ "pretty-format": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.0.tgz",
+ "integrity": "sha512-sF6lxyA25dIURyDk4voYmGU9Uwz2rQKMfjxKnDd19yk+qxKGrimFqS5YsPHWTlAVBo+YhWzXsqZoaMzrTFvqfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.0",
+ "@jest/fake-timers": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "jest-mock": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-validate": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.0.tgz",
+ "integrity": "sha512-p4bXAhXTawTsADgQgTpbymdLaTyPW1xWNu1oIGG7/N3LIAbZVkH2JMJqS8/IUcnGR8Kc7WFE+vWbJvsqGCWZXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "anymatch": "^3.1.3",
+ "fb-watchman": "^2.0.2",
+ "graceful-fs": "^4.2.11",
+ "jest-regex-util": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-worker": "30.0.0",
+ "micromatch": "^4.0.8",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.3"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.0.tgz",
+ "integrity": "sha512-E/ly1azdVVbZrS0T6FIpyYHvsdek4FNaThJTtggjV/8IpKxh3p9NLndeUZy2+sjAI3ncS+aM0uLLon/dBg8htA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.0",
+ "pretty-format": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.0.tgz",
+ "integrity": "sha512-m5mrunqopkrqwG1mMdJxe1J4uGmS9AHHKYUmoxeQOxBcLjEvirIrIDwuKmUYrecPHVB/PUBpXs2gPoeA2FSSLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.0",
+ "chalk": "^4.1.2",
+ "jest-diff": "30.0.0",
+ "pretty-format": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.0.tgz",
+ "integrity": "sha512-pV3qcrb4utEsa/U7UI2VayNzSDQcmCllBZLSoIucrESRu0geKThFZOjjh0kACDJFJRAQwsK7GVsmS6SpEceD8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@jest/types": "30.0.0",
+ "@types/stack-utils": "^2.0.3",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.0.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.0.tgz",
+ "integrity": "sha512-W2sRA4ALXILrEetEOh2ooZG6fZ01iwVs0OWMKSSWRcUlaLr4ESHuiKXDNTg+ZVgOq8Ei5445i/Yxrv59VT+XkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "jest-util": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0.tgz",
+ "integrity": "sha512-rT84010qRu/5OOU7a9TeidC2Tp3Qgt9Sty4pOZ/VSDuEmRupIjKZAb53gU3jr4ooMlhwScrgC9UixJxWzVu9oQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.0.tgz",
+ "integrity": "sha512-zwWl1P15CcAfuQCEuxszjiKdsValhnWcj/aXg/R3aMHs8HVoCWHC4B/+5+1BirMoOud8NnN85GSP2LEZCbj3OA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.0",
+ "jest-pnp-resolver": "^1.2.3",
+ "jest-util": "30.0.0",
+ "jest-validate": "30.0.0",
+ "slash": "^3.0.0",
+ "unrs-resolver": "^1.7.11"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.0.tgz",
+ "integrity": "sha512-Yhh7odCAUNXhluK1bCpwIlHrN1wycYaTlZwq1GdfNBEESNNI/z1j1a7dUEWHbmB9LGgv0sanxw3JPmWU8NeebQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-regex-util": "30.0.0",
+ "jest-snapshot": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.0.tgz",
+ "integrity": "sha512-xbhmvWIc8X1IQ8G7xTv0AQJXKjBVyxoVJEJgy7A4RXsSaO+k/1ZSBbHwjnUhvYqMvwQPomWssDkUx6EoidEhlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "30.0.0",
+ "@jest/environment": "30.0.0",
+ "@jest/test-result": "30.0.0",
+ "@jest/transform": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "emittery": "^0.13.1",
+ "exit-x": "^0.2.2",
+ "graceful-fs": "^4.2.11",
+ "jest-docblock": "30.0.0",
+ "jest-environment-node": "30.0.0",
+ "jest-haste-map": "30.0.0",
+ "jest-leak-detector": "30.0.0",
+ "jest-message-util": "30.0.0",
+ "jest-resolve": "30.0.0",
+ "jest-runtime": "30.0.0",
+ "jest-util": "30.0.0",
+ "jest-watcher": "30.0.0",
+ "jest-worker": "30.0.0",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.0.tgz",
+ "integrity": "sha512-/O07qVgFrFAOGKGigojmdR3jUGz/y3+a/v9S/Yi2MHxsD+v6WcPppglZJw0gNJkRBArRDK8CFAwpM/VuEiiRjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.0.0",
+ "@jest/fake-timers": "30.0.0",
+ "@jest/globals": "30.0.0",
+ "@jest/source-map": "30.0.0",
+ "@jest/test-result": "30.0.0",
+ "@jest/transform": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "cjs-module-lexer": "^2.1.0",
+ "collect-v8-coverage": "^1.0.2",
+ "glob": "^10.3.10",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.0.0",
+ "jest-message-util": "30.0.0",
+ "jest-mock": "30.0.0",
+ "jest-regex-util": "30.0.0",
+ "jest-resolve": "30.0.0",
+ "jest-snapshot": "30.0.0",
+ "jest-util": "30.0.0",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.0.tgz",
+ "integrity": "sha512-6oCnzjpvfj/UIOMTqKZ6gedWAUgaycMdV8Y8h2dRJPvc2wSjckN03pzeoonw8y33uVngfx7WMo1ygdRGEKOT7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@babel/generator": "^7.27.5",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1",
+ "@babel/types": "^7.27.3",
+ "@jest/expect-utils": "30.0.0",
+ "@jest/get-type": "30.0.0",
+ "@jest/snapshot-utils": "30.0.0",
+ "@jest/transform": "30.0.0",
+ "@jest/types": "30.0.0",
+ "babel-preset-current-node-syntax": "^1.1.0",
+ "chalk": "^4.1.2",
+ "expect": "30.0.0",
+ "graceful-fs": "^4.2.11",
+ "jest-diff": "30.0.0",
+ "jest-matcher-utils": "30.0.0",
+ "jest-message-util": "30.0.0",
+ "jest-util": "30.0.0",
+ "pretty-format": "30.0.0",
+ "semver": "^7.7.2",
+ "synckit": "^0.11.8"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.0.tgz",
+ "integrity": "sha512-fhNBBM9uSUbd4Lzsf8l/kcAdaHD/4SgoI48en3HXcBEMwKwoleKFMZ6cYEYs21SB779PRuRCyNLmymApAm8tZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-util/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.0.tgz",
+ "integrity": "sha512-d6OkzsdlWItHAikUDs1hlLmpOIRhsZoXTCliV2XXalVQ3ZOeb9dy0CQ6AKulJu/XOZqpOEr/FiMH+FeOBVV+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/get-type": "30.0.0",
+ "@jest/types": "30.0.0",
+ "camelcase": "^6.3.0",
+ "chalk": "^4.1.2",
+ "leven": "^3.1.0",
+ "pretty-format": "30.0.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.0.tgz",
+ "integrity": "sha512-fbAkojcyS53bOL/B7XYhahORq9cIaPwOgd/p9qW/hybbC8l6CzxfWJJxjlPBAIVN8dRipLR0zdhpGQdam+YBtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "30.0.0",
+ "@jest/types": "30.0.0",
+ "@types/node": "*",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^4.1.2",
+ "emittery": "^0.13.1",
+ "jest-util": "30.0.0",
+ "string-length": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.0.tgz",
+ "integrity": "sha512-VZvxfWIybIvwK8N/Bsfe43LfQgd/rD0c4h5nLUx78CAqPxIQcW2qDjsVAC53iUR8yxzFIeCFFvWOh8en8hGzdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@ungap/structured-clone": "^1.3.0",
+ "jest-util": "30.0.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.1.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+ "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.4.2",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.1"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-beautify/node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/js-beautify/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/js-beautify/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/js-beautify/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/js-beautify/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/js-beautify/node_modules/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz",
+ "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "agentkeepalive": "^4.1.3",
+ "cacache": "^15.2.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^4.0.1",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.3",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^1.3.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.2",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^6.0.0",
+ "ssri": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
+ "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.1.0",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.12"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/moment-timezone": {
+ "version": "0.5.48",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.29.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz",
+ "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "mkdirp": "^0.5.6",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.18",
+ "xtend": "^4.0.2"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/napi-postinstall": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz",
+ "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "napi-postinstall": "lib/cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/napi-postinstall"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.75.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
+ "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz",
+ "integrity": "sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-cron": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.1.0.tgz",
+ "integrity": "sha512-OS+3ORu+h03/haS6Di8Qr7CrVs4YaKZZOynZwQpyPZDnR3tqRbwJmuP2gVR16JfhLgyNlloAV1VTrrWlRogCFA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
+ "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^9.1.0",
+ "nopt": "^5.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "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",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "license": "MIT",
+ "engines": {
+ "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/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz",
+ "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "30.0.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.0.tgz",
+ "integrity": "sha512-18NAOUr4ZOQiIR+BgI5NhQE7uREdx4ZyV0dyay5izh4yfQ+1T7BSvggxvRGoXocrRyevqW5OhScUjbi9GB8R8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.0",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
+ "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/retry-as-promised": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
+ "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
+ "license": "MIT"
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/sequelize": {
+ "version": "6.37.7",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
+ "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sequelize"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.1.8",
+ "@types/validator": "^13.7.17",
+ "debug": "^4.3.4",
+ "dottie": "^2.0.6",
+ "inflection": "^1.13.4",
+ "lodash": "^4.17.21",
+ "moment": "^2.29.4",
+ "moment-timezone": "^0.5.43",
+ "pg-connection-string": "^2.6.1",
+ "retry-as-promised": "^7.0.4",
+ "semver": "^7.5.4",
+ "sequelize-pool": "^7.1.0",
+ "toposort-class": "^1.0.1",
+ "uuid": "^8.3.2",
+ "validator": "^13.9.0",
+ "wkx": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ibm_db": {
+ "optional": true
+ },
+ "mariadb": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "oracledb": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "pg-hstore": {
+ "optional": true
+ },
+ "snowflake-sdk": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ },
+ "tedious": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sequelize-cli": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.3.tgz",
+ "integrity": "sha512-1YYPrcSRt/bpMDDSKM5ubY1mnJ2TEwIaGZcqITw4hLtGtE64nIqaBnLtMvH8VKHg6FbWpXTiFNc2mS/BtQCXZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fs-extra": "^9.1.0",
+ "js-beautify": "1.15.4",
+ "lodash": "^4.17.21",
+ "picocolors": "^1.1.1",
+ "resolve": "^1.22.1",
+ "umzug": "^2.3.0",
+ "yargs": "^16.2.0"
+ },
+ "bin": {
+ "sequelize": "lib/sequelize",
+ "sequelize-cli": "lib/sequelize"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/sequelize-pool": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
+ "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/sequelize/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sequelize/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
+ "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz",
+ "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/sqlite3": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz",
+ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "node-addon-api": "^7.0.0",
+ "prebuild-install": "^7.1.1",
+ "tar": "^6.1.11"
+ },
+ "optionalDependencies": {
+ "node-gyp": "8.x"
+ },
+ "peerDependencies": {
+ "node-gyp": "8.x"
+ },
+ "peerDependenciesMeta": {
+ "node-gyp": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sqlite3/node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
+ "node_modules/ssri": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
+ "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/superagent": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
+ "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "component-emitter": "^1.3.0",
+ "cookiejar": "^2.1.4",
+ "debug": "^4.3.4",
+ "fast-safe-stringify": "^2.1.1",
+ "form-data": "^4.0.0",
+ "formidable": "^3.5.4",
+ "methods": "^1.1.2",
+ "mime": "2.6.0",
+ "qs": "^6.11.0"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/superagent/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/superagent/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/superagent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/supertest": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
+ "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "methods": "^1.1.2",
+ "superagent": "^10.2.1"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
+ "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.2.4"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
+ "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/toposort-class": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
+ "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
+ "license": "MIT"
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "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/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "license": "MIT",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/umzug": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
+ "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "^3.7.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "license": "MIT"
+ },
+ "node_modules/unique-filename": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
+ "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "unique-slug": "^2.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz",
+ "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unrs-resolver": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.0.tgz",
+ "integrity": "sha512-wqaRu4UnzBD2ABTC1kLfBjAqIDZ5YUTr/MLGa7By47JV1bJDSW7jq/ZSLigB7enLe7ubNaJhtnBXgrc/50cEhg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "napi-postinstall": "^0.2.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unrs-resolver"
+ },
+ "optionalDependencies": {
+ "@unrs/resolver-binding-android-arm-eabi": "1.9.0",
+ "@unrs/resolver-binding-android-arm64": "1.9.0",
+ "@unrs/resolver-binding-darwin-arm64": "1.9.0",
+ "@unrs/resolver-binding-darwin-x64": "1.9.0",
+ "@unrs/resolver-binding-freebsd-x64": "1.9.0",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.0",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.0",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.9.0",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.9.0",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.0",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.0",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.9.0",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.9.0",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.9.0",
+ "@unrs/resolver-binding-linux-x64-musl": "1.9.0",
+ "@unrs/resolver-binding-wasm32-wasi": "1.9.0",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.9.0",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.9.0",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.9.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "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/v8-to-istanbul": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.15.15",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
+ "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wkx": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
+ "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?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/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/write-file-atomic/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..84a2dd3
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "backend",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "start": "node app.js",
+ "dev": "nodemon app.js",
+ "test": "cross-env NODE_ENV=test jest",
+ "test:watch": "cross-env NODE_ENV=test jest --watch",
+ "test:coverage": "cross-env NODE_ENV=test jest --coverage",
+ "test:unit": "cross-env NODE_ENV=test jest tests/unit",
+ "test:integration": "cross-env NODE_ENV=test jest tests/integration",
+ "db:init": "node scripts/db-init.js",
+ "db:sync": "node scripts/db-sync.js",
+ "db:migrate": "node scripts/db-migrate.js",
+ "db:reset": "node scripts/db-reset.js",
+ "db:status": "node scripts/db-status.js",
+ "user:create": "node scripts/user-create.js",
+ "migration:create": "node scripts/migration-create.js",
+ "migration:run": "npx sequelize-cli db:migrate",
+ "migration:undo": "npx sequelize-cli db:migrate:undo",
+ "migration:undo:all": "npx sequelize-cli db:migrate:undo:all",
+ "migration:status": "npx sequelize-cli db:migrate:status"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "compression": "^1.8.0",
+ "connect-session-sequelize": "^7.1.7",
+ "cors": "^2.8.5",
+ "dotenv": "^16.5.0",
+ "express": "^4.18.2",
+ "express-session": "^1.18.1",
+ "helmet": "^8.1.0",
+ "js-yaml": "^4.1.0",
+ "morgan": "^1.10.0",
+ "multer": "^2.0.1",
+ "node-cron": "^4.1.0",
+ "sequelize": "^6.37.7",
+ "sqlite3": "^5.1.7"
+ },
+ "devDependencies": {
+ "cross-env": "^7.0.3",
+ "jest": "^30.0.0",
+ "nodemon": "^3.0.1",
+ "sequelize-cli": "^6.6.2",
+ "supertest": "^7.1.1"
+ }
+}
diff --git a/backend/routes/areas.js b/backend/routes/areas.js
new file mode 100644
index 0000000..84607b8
--- /dev/null
+++ b/backend/routes/areas.js
@@ -0,0 +1,130 @@
+const express = require('express');
+const { Area } = require('../models');
+const router = express.Router();
+
+// GET /api/areas
+router.get('/areas', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const areas = await Area.findAll({
+ where: { user_id: req.session.userId },
+ order: [['name', 'ASC']]
+ });
+
+ res.json(areas);
+ } catch (error) {
+ console.error('Error fetching areas:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// GET /api/areas/:id
+router.get('/areas/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const area = await Area.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!area) {
+ return res.status(404).json({ error: "Area not found or doesn't belong to the current user." });
+ }
+
+ res.json(area);
+ } catch (error) {
+ console.error('Error fetching area:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/areas
+router.post('/areas', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { name, description } = req.body;
+
+ if (!name || !name.trim()) {
+ return res.status(400).json({ error: 'Area name is required.' });
+ }
+
+ const area = await Area.create({
+ name: name.trim(),
+ description: description || '',
+ user_id: req.session.userId
+ });
+
+ res.status(201).json(area);
+ } catch (error) {
+ console.error('Error creating area:', error);
+ res.status(400).json({
+ error: 'There was a problem creating the area.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// PATCH /api/areas/:id
+router.patch('/areas/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const area = await Area.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!area) {
+ return res.status(404).json({ error: 'Area not found.' });
+ }
+
+ const { name, description } = req.body;
+ const updateData = {};
+
+ if (name !== undefined) updateData.name = name;
+ if (description !== undefined) updateData.description = description;
+
+ await area.update(updateData);
+ res.json(area);
+ } catch (error) {
+ console.error('Error updating area:', error);
+ res.status(400).json({
+ error: 'There was a problem updating the area.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// DELETE /api/areas/:id
+router.delete('/areas/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const area = await Area.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!area) {
+ return res.status(404).json({ error: 'Area not found.' });
+ }
+
+ await area.destroy();
+ res.status(204).send();
+ } catch (error) {
+ console.error('Error deleting area:', error);
+ res.status(400).json({ error: 'There was a problem deleting the area.' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
new file mode 100644
index 0000000..e6ea09e
--- /dev/null
+++ b/backend/routes/auth.js
@@ -0,0 +1,78 @@
+const express = require('express');
+const { User } = require('../models');
+const router = express.Router();
+
+// Get current user
+router.get('/current_user', async (req, res) => {
+ try {
+ if (req.session && req.session.userId) {
+ const user = await User.findByPk(req.session.userId);
+ if (user) {
+ return res.json({
+ user: {
+ id: user.id,
+ email: user.email,
+ language: user.language,
+ appearance: user.appearance,
+ timezone: user.timezone
+ }
+ });
+ }
+ }
+
+ res.json({ user: null });
+ } catch (error) {
+ console.error('Error fetching current user:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Login
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({ error: 'Invalid login parameters.' });
+ }
+
+ const user = await User.findOne({ where: { email } });
+ if (!user) {
+ return res.status(401).json({ errors: ['Invalid credentials'] });
+ }
+
+ const isValidPassword = await user.checkPassword(password);
+ if (!isValidPassword) {
+ return res.status(401).json({ errors: ['Invalid credentials'] });
+ }
+
+ req.session.userId = user.id;
+
+ res.json({
+ user: {
+ id: user.id,
+ email: user.email,
+ language: user.language,
+ appearance: user.appearance,
+ timezone: user.timezone
+ }
+ });
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Logout
+router.get('/logout', (req, res) => {
+ req.session.destroy((err) => {
+ if (err) {
+ console.error('Logout error:', err);
+ return res.status(500).json({ error: 'Could not log out' });
+ }
+
+ res.json({ message: 'Logged out successfully' });
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/inbox.js b/backend/routes/inbox.js
new file mode 100644
index 0000000..a7bde05
--- /dev/null
+++ b/backend/routes/inbox.js
@@ -0,0 +1,157 @@
+const express = require('express');
+const { InboxItem } = require('../models');
+const router = express.Router();
+
+// GET /api/inbox
+router.get('/inbox', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const items = await InboxItem.findAll({
+ where: {
+ user_id: req.session.userId,
+ status: 'added'
+ },
+ order: [['created_at', 'DESC']]
+ });
+
+ res.json(items);
+ } catch (error) {
+ console.error('Error fetching inbox items:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/inbox
+router.post('/inbox', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { content, source } = req.body;
+
+ if (!content || !content.trim()) {
+ return res.status(400).json({ error: 'Content is required' });
+ }
+
+ const item = await InboxItem.create({
+ content: content.trim(),
+ source: source || 'tududi',
+ user_id: req.session.userId
+ });
+
+ res.status(201).json(item);
+ } catch (error) {
+ console.error('Error creating inbox item:', error);
+ res.status(400).json({
+ error: 'There was a problem creating the inbox item.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// GET /api/inbox/:id
+router.get('/inbox/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const item = await InboxItem.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!item) {
+ return res.status(404).json({ error: 'Inbox item not found.' });
+ }
+
+ res.json(item);
+ } catch (error) {
+ console.error('Error fetching inbox item:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// PATCH /api/inbox/:id
+router.patch('/inbox/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const item = await InboxItem.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!item) {
+ return res.status(404).json({ error: 'Inbox item not found.' });
+ }
+
+ const { content, status } = req.body;
+ const updateData = {};
+
+ if (content !== undefined) updateData.content = content;
+ if (status !== undefined) updateData.status = status;
+
+ await item.update(updateData);
+ res.json(item);
+ } catch (error) {
+ console.error('Error updating inbox item:', error);
+ res.status(400).json({
+ error: 'There was a problem updating the inbox item.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// DELETE /api/inbox/:id
+router.delete('/inbox/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const item = await InboxItem.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!item) {
+ return res.status(404).json({ error: 'Inbox item not found.' });
+ }
+
+ // Mark as deleted instead of actual deletion
+ await item.update({ status: 'deleted' });
+ res.json({ message: 'Inbox item successfully deleted' });
+ } catch (error) {
+ console.error('Error deleting inbox item:', error);
+ res.status(400).json({ error: 'There was a problem deleting the inbox item.' });
+ }
+});
+
+// PATCH /api/inbox/:id/process
+router.patch('/inbox/:id/process', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const item = await InboxItem.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!item) {
+ return res.status(404).json({ error: 'Inbox item not found.' });
+ }
+
+ await item.update({ status: 'processed' });
+ res.json(item);
+ } catch (error) {
+ console.error('Error processing inbox item:', error);
+ res.status(400).json({ error: 'There was a problem processing the inbox item.' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/notes.js b/backend/routes/notes.js
new file mode 100644
index 0000000..ea080d6
--- /dev/null
+++ b/backend/routes/notes.js
@@ -0,0 +1,242 @@
+const express = require('express');
+const { Note, Tag, Project, sequelize } = require('../models');
+const { Op } = require('sequelize');
+const router = express.Router();
+
+// Helper function to update note tags
+async function updateNoteTags(note, tagsArray, userId) {
+ if (!tagsArray || tagsArray.length === 0) {
+ await note.setTags([]);
+ return;
+ }
+
+ try {
+ const tagNames = tagsArray.filter((name, index, arr) => arr.indexOf(name) === index); // unique
+ const tags = await Promise.all(
+ tagNames.map(async (name) => {
+ const [tag] = await Tag.findOrCreate({
+ where: { name, user_id: userId },
+ defaults: { name, user_id: userId }
+ });
+ return tag;
+ })
+ );
+ await note.setTags(tags);
+ } catch (error) {
+ console.error('Failed to update tags:', error.message);
+ }
+}
+
+// GET /api/notes
+router.get('/notes', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const orderBy = req.query.order_by || 'title:asc';
+ const [orderColumn, orderDirection] = orderBy.split(':');
+
+ let whereClause = { user_id: req.session.userId };
+ let includeClause = [
+ { model: Tag, through: { attributes: [] } },
+ { model: Project, required: false, attributes: ['id', 'name'] }
+ ];
+
+ // Filter by tag
+ if (req.query.tag) {
+ includeClause[0].where = { name: req.query.tag };
+ includeClause[0].required = true;
+ }
+
+ const notes = await Note.findAll({
+ where: whereClause,
+ include: includeClause,
+ order: [[orderColumn, orderDirection.toUpperCase()]],
+ distinct: true
+ });
+
+ res.json(notes);
+ } catch (error) {
+ console.error('Error fetching notes:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// GET /api/note/:id
+router.get('/note/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const note = await Note.findOne({
+ where: { id: req.params.id, user_id: req.session.userId },
+ include: [
+ { model: Tag, through: { attributes: [] } },
+ { model: Project, required: false, attributes: ['id', 'name'] }
+ ]
+ });
+
+ if (!note) {
+ return res.status(404).json({ error: 'Note not found.' });
+ }
+
+ res.json(note);
+ } catch (error) {
+ console.error('Error fetching note:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/note
+router.post('/note', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { title, content, project_id, tags } = req.body;
+
+ const noteAttributes = {
+ title,
+ content,
+ user_id: req.session.userId
+ };
+
+ // Handle project assignment
+ if (project_id && project_id.toString().trim()) {
+ const project = await Project.findOne({
+ where: { id: project_id, user_id: req.session.userId }
+ });
+ if (!project) {
+ return res.status(400).json({ error: 'Invalid project.' });
+ }
+ noteAttributes.project_id = project_id;
+ }
+
+ const note = await Note.create(noteAttributes);
+
+ // Handle tags - can be array of strings or array of objects with name property
+ let tagNames = [];
+ if (Array.isArray(tags)) {
+ if (tags.every(t => typeof t === 'string')) {
+ tagNames = tags;
+ } else if (tags.every(t => typeof t === 'object' && t.name)) {
+ tagNames = tags.map(t => t.name);
+ }
+ }
+
+ await updateNoteTags(note, tagNames, req.session.userId);
+
+ // Reload note with associations
+ const noteWithAssociations = await Note.findByPk(note.id, {
+ include: [
+ { model: Tag, through: { attributes: [] } },
+ { model: Project, required: false, attributes: ['id', 'name'] }
+ ]
+ });
+
+ res.status(201).json(noteWithAssociations);
+ } catch (error) {
+ console.error('Error creating note:', error);
+ res.status(400).json({
+ error: 'There was a problem creating the note.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// PATCH /api/note/:id
+router.patch('/note/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const note = await Note.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!note) {
+ return res.status(404).json({ error: 'Note not found.' });
+ }
+
+ const { title, content, project_id, tags } = req.body;
+
+ const updateData = {};
+ if (title !== undefined) updateData.title = title;
+ if (content !== undefined) updateData.content = content;
+
+ // Handle project assignment
+ if (project_id !== undefined) {
+ if (project_id && project_id.toString().trim()) {
+ const project = await Project.findOne({
+ where: { id: project_id, user_id: req.session.userId }
+ });
+ if (!project) {
+ return res.status(400).json({ error: 'Invalid project.' });
+ }
+ updateData.project_id = project_id;
+ } else {
+ updateData.project_id = null;
+ }
+ }
+
+ await note.update(updateData);
+
+ // Handle tags if provided
+ if (tags !== undefined) {
+ let tagNames = [];
+ if (Array.isArray(tags)) {
+ if (tags.every(t => typeof t === 'string')) {
+ tagNames = tags;
+ } else if (tags.every(t => typeof t === 'object' && t.name)) {
+ tagNames = tags.map(t => t.name);
+ }
+ }
+ await updateNoteTags(note, tagNames, req.session.userId);
+ }
+
+ // Reload note with associations
+ const noteWithAssociations = await Note.findByPk(note.id, {
+ include: [
+ { model: Tag, through: { attributes: [] } },
+ { model: Project, required: false, attributes: ['id', 'name'] }
+ ]
+ });
+
+ res.json(noteWithAssociations);
+ } catch (error) {
+ console.error('Error updating note:', error);
+ res.status(400).json({
+ error: 'There was a problem updating the note.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// DELETE /api/note/:id
+router.delete('/note/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const note = await Note.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!note) {
+ return res.status(404).json({ error: 'Note not found.' });
+ }
+
+ await note.destroy();
+ res.json({ message: 'Note deleted successfully.' });
+ } catch (error) {
+ console.error('Error deleting note:', error);
+ res.status(400).json({ error: 'There was a problem deleting the note.' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/projects.js b/backend/routes/projects.js
new file mode 100644
index 0000000..c9781b2
--- /dev/null
+++ b/backend/routes/projects.js
@@ -0,0 +1,275 @@
+const express = require('express');
+const { Project, Task, Tag, Area, sequelize } = require('../models');
+const { Op } = require('sequelize');
+const router = express.Router();
+
+// Helper function to update project tags
+async function updateProjectTags(project, tagsData, userId) {
+ if (!tagsData) return;
+
+ const tagNames = tagsData
+ .map(tag => tag.name)
+ .filter(name => name && name.trim())
+ .filter((name, index, arr) => arr.indexOf(name) === index); // unique
+
+ if (tagNames.length === 0) {
+ await project.setTags([]);
+ return;
+ }
+
+ // Find existing tags
+ const existingTags = await Tag.findAll({
+ where: { user_id: userId, name: tagNames }
+ });
+
+ // Create new tags
+ const existingTagNames = existingTags.map(tag => tag.name);
+ const newTagNames = tagNames.filter(name => !existingTagNames.includes(name));
+
+ const createdTags = await Promise.all(
+ newTagNames.map(name => Tag.create({ name, user_id: userId }))
+ );
+
+ // Set all tags to project
+ const allTags = [...existingTags, ...createdTags];
+ await project.setTags(allTags);
+}
+
+// GET /api/projects
+router.get('/projects', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { active, pin_to_sidebar, area_id } = req.query;
+
+ let whereClause = { user_id: req.session.userId };
+
+ // Filter by active status
+ if (active === 'true') {
+ whereClause.active = true;
+ } else if (active === 'false') {
+ whereClause.active = false;
+ }
+
+ // Filter by pinned status
+ if (pin_to_sidebar === 'true') {
+ whereClause.pin_to_sidebar = true;
+ } else if (pin_to_sidebar === 'false') {
+ whereClause.pin_to_sidebar = false;
+ }
+
+ // Filter by area
+ if (area_id && area_id !== '') {
+ whereClause.area_id = area_id;
+ }
+
+ const projects = await Project.findAll({
+ where: whereClause,
+ include: [
+ {
+ model: Task,
+ required: false,
+ attributes: ['id', 'status']
+ },
+ {
+ model: Area,
+ required: false,
+ attributes: ['name']
+ },
+ {
+ model: Tag,
+ attributes: ['id', 'name'],
+ through: { attributes: [] }
+ }
+ ],
+ order: [['name', 'ASC']]
+ });
+
+ // Calculate task status counts for each project
+ const taskStatusCounts = {};
+ projects.forEach(project => {
+ const tasks = project.Tasks || [];
+ taskStatusCounts[project.id] = {
+ total: tasks.length,
+ done: tasks.filter(t => t.status === 2).length,
+ in_progress: tasks.filter(t => t.status === 1).length,
+ not_started: tasks.filter(t => t.status === 0).length
+ };
+ });
+
+ // Group projects by area
+ const groupedProjects = {};
+ projects.forEach(project => {
+ const areaName = project.Area ? project.Area.name : 'No Area';
+ if (!groupedProjects[areaName]) {
+ groupedProjects[areaName] = [];
+ }
+ groupedProjects[areaName].push(project);
+ });
+
+ res.json({
+ projects: projects.map(project => ({
+ ...project.toJSON(),
+ due_date_at: project.due_date_at ? project.due_date_at.toISOString() : null
+ })),
+ task_status_counts: taskStatusCounts,
+ grouped_projects: groupedProjects
+ });
+ } catch (error) {
+ console.error('Error fetching projects:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// GET /api/project/:id
+router.get('/project/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const project = await Project.findOne({
+ where: { id: req.params.id, user_id: req.session.userId },
+ include: [
+ { model: Task, required: false },
+ { model: Area, required: false, attributes: ['id', 'name'] },
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } }
+ ]
+ });
+
+ if (!project) {
+ return res.status(404).json({ error: 'Project not found' });
+ }
+
+ res.json({
+ ...project.toJSON(),
+ due_date_at: project.due_date_at ? project.due_date_at.toISOString() : null
+ });
+ } catch (error) {
+ console.error('Error fetching project:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/project
+router.post('/project', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { name, description, area_id, priority, due_date_at, tags } = req.body;
+
+ if (!name || !name.trim()) {
+ return res.status(400).json({ error: 'Project name is required' });
+ }
+
+ const projectData = {
+ name: name.trim(),
+ description: description || '',
+ area_id: area_id || null,
+ active: true,
+ pin_to_sidebar: false,
+ priority: priority || null,
+ due_date_at: due_date_at || null,
+ user_id: req.session.userId
+ };
+
+ const project = await Project.create(projectData);
+ await updateProjectTags(project, tags, req.session.userId);
+
+ // Reload project with associations
+ const projectWithAssociations = await Project.findByPk(project.id, {
+ include: [
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } }
+ ]
+ });
+
+ res.status(201).json({
+ ...projectWithAssociations.toJSON(),
+ due_date_at: projectWithAssociations.due_date_at ? projectWithAssociations.due_date_at.toISOString() : null
+ });
+ } catch (error) {
+ console.error('Error creating project:', error);
+ res.status(400).json({
+ error: 'There was a problem creating the project.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// PATCH /api/project/:id
+router.patch('/project/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const project = await Project.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!project) {
+ return res.status(404).json({ error: 'Project not found.' });
+ }
+
+ const { name, description, area_id, active, pin_to_sidebar, priority, due_date_at, tags } = req.body;
+
+ const updateData = {};
+ if (name !== undefined) updateData.name = name;
+ if (description !== undefined) updateData.description = description;
+ if (area_id !== undefined) updateData.area_id = area_id;
+ if (active !== undefined) updateData.active = active;
+ if (pin_to_sidebar !== undefined) updateData.pin_to_sidebar = pin_to_sidebar;
+ if (priority !== undefined) updateData.priority = priority;
+ if (due_date_at !== undefined) updateData.due_date_at = due_date_at;
+
+ await project.update(updateData);
+ await updateProjectTags(project, tags, req.session.userId);
+
+ // Reload project with associations
+ const projectWithAssociations = await Project.findByPk(project.id, {
+ include: [
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } }
+ ]
+ });
+
+ res.json({
+ ...projectWithAssociations.toJSON(),
+ due_date_at: projectWithAssociations.due_date_at ? projectWithAssociations.due_date_at.toISOString() : null
+ });
+ } catch (error) {
+ console.error('Error updating project:', error);
+ res.status(400).json({
+ error: 'There was a problem updating the project.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// DELETE /api/project/:id
+router.delete('/project/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const project = await Project.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!project) {
+ return res.status(404).json({ error: 'Project not found.' });
+ }
+
+ await project.destroy();
+ res.json({ message: 'Project successfully deleted' });
+ } catch (error) {
+ console.error('Error deleting project:', error);
+ res.status(400).json({ error: 'There was a problem deleting the project.' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/quotes.js b/backend/routes/quotes.js
new file mode 100644
index 0000000..f596909
--- /dev/null
+++ b/backend/routes/quotes.js
@@ -0,0 +1,30 @@
+const express = require('express');
+const router = express.Router();
+const quotesService = require('../services/quotesService');
+
+// GET /api/quotes/random - Get a random quote
+router.get('/quotes/random', (req, res) => {
+ try {
+ const quote = quotesService.getRandomQuote();
+ res.json({ quote });
+ } catch (error) {
+ console.error('Error getting random quote:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// GET /api/quotes - Get all quotes
+router.get('/quotes', (req, res) => {
+ try {
+ const quotes = quotesService.getAllQuotes();
+ res.json({
+ quotes,
+ count: quotesService.getQuotesCount()
+ });
+ } catch (error) {
+ console.error('Error getting quotes:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/tags.js b/backend/routes/tags.js
new file mode 100644
index 0000000..a1f0b2e
--- /dev/null
+++ b/backend/routes/tags.js
@@ -0,0 +1,132 @@
+const express = require('express');
+const { Tag } = require('../models');
+const router = express.Router();
+
+// GET /api/tags
+router.get('/tags', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const tags = await Tag.findAll({
+ where: { user_id: req.session.userId },
+ attributes: ['id', 'name'],
+ order: [['name', 'ASC']]
+ });
+
+ res.json(tags);
+ } catch (error) {
+ console.error('Error fetching tags:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// GET /api/tag/:id
+router.get('/tag/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const tag = await Tag.findOne({
+ where: { id: req.params.id, user_id: req.session.userId },
+ attributes: ['id', 'name']
+ });
+
+ if (!tag) {
+ return res.status(404).json({ error: 'Tag not found' });
+ }
+
+ res.json(tag);
+ } catch (error) {
+ console.error('Error fetching tag:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/tag
+router.post('/tag', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { name } = req.body;
+
+ if (!name || !name.trim()) {
+ return res.status(400).json({ error: 'Tag name is required' });
+ }
+
+ const tag = await Tag.create({
+ name: name.trim(),
+ user_id: req.session.userId
+ });
+
+ res.status(201).json({
+ id: tag.id,
+ name: tag.name
+ });
+ } catch (error) {
+ console.error('Error creating tag:', error);
+ res.status(400).json({ error: 'There was a problem creating the tag.' });
+ }
+});
+
+// PATCH /api/tag/:id
+router.patch('/tag/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const tag = await Tag.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!tag) {
+ return res.status(404).json({ error: 'Tag not found' });
+ }
+
+ const { name } = req.body;
+
+ if (!name || !name.trim()) {
+ return res.status(400).json({ error: 'Tag name is required' });
+ }
+
+ await tag.update({ name: name.trim() });
+
+ res.json({
+ id: tag.id,
+ name: tag.name
+ });
+ } catch (error) {
+ console.error('Error updating tag:', error);
+ res.status(400).json({ error: 'There was a problem updating the tag.' });
+ }
+});
+
+// DELETE /api/tag/:id
+router.delete('/tag/:id', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const tag = await Tag.findOne({
+ where: { id: req.params.id, user_id: req.session.userId }
+ });
+
+ if (!tag) {
+ return res.status(404).json({ error: 'Tag not found' });
+ }
+
+ await tag.destroy();
+ res.json({ message: 'Tag successfully deleted' });
+ } catch (error) {
+ console.error('Error deleting tag:', error);
+ res.status(400).json({ error: 'There was a problem deleting the tag.' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/tasks.js b/backend/routes/tasks.js
new file mode 100644
index 0000000..6a1a9e1
--- /dev/null
+++ b/backend/routes/tasks.js
@@ -0,0 +1,394 @@
+const express = require('express');
+const { Task, Tag, Project, sequelize } = require('../models');
+const { Op } = require('sequelize');
+const router = express.Router();
+
+// Helper function to update task tags
+async function updateTaskTags(task, tagsData, userId) {
+ if (!tagsData) return;
+
+ const tagNames = tagsData
+ .map(tag => tag.name)
+ .filter(name => name && name.trim())
+ .filter((name, index, arr) => arr.indexOf(name) === index); // unique
+
+ if (tagNames.length === 0) {
+ await task.setTags([]);
+ return;
+ }
+
+ // Find existing tags
+ const existingTags = await Tag.findAll({
+ where: { user_id: userId, name: tagNames }
+ });
+
+ // Create new tags
+ const existingTagNames = existingTags.map(tag => tag.name);
+ const newTagNames = tagNames.filter(name => !existingTagNames.includes(name));
+
+ const createdTags = await Promise.all(
+ newTagNames.map(name => Tag.create({ name, user_id: userId }))
+ );
+
+ // Set all tags to task
+ const allTags = [...existingTags, ...createdTags];
+ await task.setTags(allTags);
+}
+
+// Filter tasks by parameters
+async function filterTasksByParams(params, userId) {
+ let whereClause = { user_id: userId };
+ let includeClause = [
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } },
+ { model: Project, attributes: ['name'], required: false }
+ ];
+
+ // Filter by type
+ switch (params.type) {
+ case 'today':
+ // Just user tasks, no additional filtering
+ break;
+ case 'upcoming':
+ whereClause.due_date = {
+ [Op.between]: [new Date(), new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]
+ };
+ whereClause.status = { [Op.ne]: Task.STATUS.DONE };
+ break;
+ case 'next':
+ whereClause.due_date = null;
+ whereClause.project_id = null;
+ whereClause.status = { [Op.ne]: Task.STATUS.DONE };
+ break;
+ case 'inbox':
+ whereClause[Op.or] = [
+ { due_date: null },
+ { project_id: null }
+ ];
+ whereClause.status = { [Op.ne]: Task.STATUS.DONE };
+ break;
+ case 'someday':
+ whereClause.due_date = null;
+ whereClause.status = { [Op.ne]: Task.STATUS.DONE };
+ break;
+ case 'waiting':
+ whereClause.status = Task.STATUS.WAITING;
+ break;
+ default:
+ if (params.status === 'done') {
+ whereClause.status = Task.STATUS.DONE;
+ } else {
+ whereClause.status = { [Op.ne]: Task.STATUS.DONE };
+ }
+ }
+
+ // Filter by tag
+ if (params.tag) {
+ includeClause[0].where = { name: params.tag };
+ includeClause[0].required = true;
+ }
+
+ let orderClause = [['created_at', 'ASC']];
+
+ // Apply ordering
+ if (params.order_by) {
+ const [orderColumn, orderDirection = 'asc'] = params.order_by.split(':');
+ const allowedColumns = ['created_at', 'updated_at', 'name', 'priority', 'status', 'due_date'];
+
+ if (!allowedColumns.includes(orderColumn)) {
+ throw new Error('Invalid order column specified.');
+ }
+
+ if (orderColumn === 'due_date') {
+ orderClause = [
+ [sequelize.literal('CASE WHEN due_date IS NULL THEN 1 ELSE 0 END'), 'ASC'],
+ ['due_date', orderDirection.toUpperCase()]
+ ];
+ } else {
+ orderClause = [[orderColumn, orderDirection.toUpperCase()]];
+ }
+ }
+
+ return await Task.findAll({
+ where: whereClause,
+ include: includeClause,
+ order: orderClause,
+ distinct: true
+ });
+}
+
+// Compute task metrics
+async function computeTaskMetrics(userId) {
+ const totalOpenTasks = await Task.count({
+ where: { user_id: userId, status: { [Op.ne]: Task.STATUS.DONE } }
+ });
+
+ const oneMonthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
+ const tasksPendingOverMonth = await Task.count({
+ where: {
+ user_id: userId,
+ status: { [Op.ne]: Task.STATUS.DONE },
+ created_at: { [Op.lt]: oneMonthAgo }
+ }
+ });
+
+ const tasksInProgress = await Task.findAll({
+ where: {
+ user_id: userId,
+ status: Task.STATUS.IN_PROGRESS
+ },
+ include: [
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } },
+ { model: Project, attributes: ['name'], required: false }
+ ],
+ order: [['priority', 'DESC']]
+ });
+
+ const today = new Date();
+ today.setHours(23, 59, 59, 999);
+
+ const tasksDueToday = await Task.findAll({
+ where: {
+ user_id: userId,
+ status: { [Op.ne]: Task.STATUS.DONE },
+ [Op.or]: [
+ { due_date: { [Op.lte]: today } },
+ sequelize.literal(`EXISTS (
+ SELECT 1 FROM projects
+ WHERE projects.id = Task.project_id
+ AND projects.due_date_at <= '${today.toISOString()}'
+ )`)
+ ]
+ },
+ include: [
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } },
+ { model: Project, attributes: ['name'], required: false }
+ ]
+ });
+
+ // Get suggested tasks (simplified version)
+ const excludedTaskIds = [
+ ...tasksInProgress.map(t => t.id),
+ ...tasksDueToday.map(t => t.id)
+ ];
+
+ const suggestedTasks = await Task.findAll({
+ where: {
+ user_id: userId,
+ status: Task.STATUS.NOT_STARTED,
+ id: { [Op.notIn]: excludedTaskIds }
+ },
+ include: [
+ { model: Tag, attributes: ['id', 'name'], through: { attributes: [] } },
+ { model: Project, attributes: ['name'], required: false }
+ ],
+ order: [['priority', 'DESC']],
+ limit: 10
+ });
+
+ return {
+ total_open_tasks: totalOpenTasks,
+ tasks_pending_over_month: tasksPendingOverMonth,
+ tasks_in_progress_count: tasksInProgress.length,
+ tasks_in_progress: tasksInProgress,
+ tasks_due_today: tasksDueToday,
+ suggested_tasks: suggestedTasks
+ };
+}
+
+// GET /api/tasks
+router.get('/tasks', async (req, res) => {
+ try {
+ const tasks = await filterTasksByParams(req.query, req.currentUser.id);
+ const metrics = await computeTaskMetrics(req.currentUser.id);
+
+ res.json({
+ tasks: tasks.map(task => ({
+ ...task.toJSON(),
+ due_date: task.due_date ? task.due_date.toISOString().split('T')[0] : null
+ })),
+ metrics: {
+ total_open_tasks: metrics.total_open_tasks,
+ tasks_pending_over_month: metrics.tasks_pending_over_month,
+ tasks_in_progress_count: metrics.tasks_in_progress_count,
+ tasks_in_progress: metrics.tasks_in_progress.map(task => ({
+ ...task.toJSON(),
+ due_date: task.due_date ? task.due_date.toISOString().split('T')[0] : null
+ })),
+ tasks_due_today: metrics.tasks_due_today.map(task => ({
+ ...task.toJSON(),
+ due_date: task.due_date ? task.due_date.toISOString().split('T')[0] : null
+ })),
+ suggested_tasks: metrics.suggested_tasks.map(task => ({
+ ...task.toJSON(),
+ due_date: task.due_date ? task.due_date.toISOString().split('T')[0] : null
+ }))
+ }
+ });
+ } catch (error) {
+ console.error('Error fetching tasks:', error);
+ if (error.message === 'Invalid order column specified.') {
+ return res.status(400).json({ error: error.message });
+ }
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/task
+router.post('/task', async (req, res) => {
+ try {
+ const { name, priority, due_date, status, note, project_id, tags } = req.body;
+
+ // Validate required fields
+ if (!name || name.trim() === '') {
+ return res.status(400).json({ error: 'Task name is required.' });
+ }
+
+ const taskAttributes = {
+ name: name.trim(),
+ priority: priority || Task.PRIORITY.LOW,
+ due_date: due_date || null,
+ status: status || Task.STATUS.NOT_STARTED,
+ note,
+ user_id: req.currentUser.id
+ };
+
+ // Handle project assignment
+ if (project_id && project_id.toString().trim()) {
+ const project = await Project.findOne({
+ where: { id: project_id, user_id: req.currentUser.id }
+ });
+ if (!project) {
+ return res.status(400).json({ error: 'Invalid project.' });
+ }
+ taskAttributes.project_id = project_id;
+ }
+
+ const task = await Task.create(taskAttributes);
+ await updateTaskTags(task, tags, req.currentUser.id);
+
+ // Reload task with associations
+ const taskWithAssociations = await Task.findByPk(task.id, {
+ include: [
+ { model: Tag, attributes: ['name'], through: { attributes: [] } },
+ { model: Project, attributes: ['name'], required: false }
+ ]
+ });
+
+ res.status(201).json({
+ ...taskWithAssociations.toJSON(),
+ due_date: taskWithAssociations.due_date ? taskWithAssociations.due_date.toISOString().split('T')[0] : null
+ });
+ } catch (error) {
+ console.error('Error creating task:', error);
+ res.status(400).json({
+ error: 'There was a problem creating the task.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// PATCH /api/task/:id
+router.patch('/task/:id', async (req, res) => {
+ try {
+ const { name, priority, status, note, due_date, project_id, tags } = req.body;
+
+ const task = await Task.findOne({
+ where: { id: req.params.id, user_id: req.currentUser.id }
+ });
+
+ if (!task) {
+ return res.status(404).json({ error: 'Task not found.' });
+ }
+
+ const taskAttributes = {
+ name,
+ priority,
+ status: status || Task.STATUS.NOT_STARTED,
+ note,
+ due_date: due_date || null
+ };
+
+ // Handle project assignment
+ if (project_id && project_id.toString().trim()) {
+ const project = await Project.findOne({
+ where: { id: project_id, user_id: req.currentUser.id }
+ });
+ if (!project) {
+ return res.status(400).json({ error: 'Invalid project.' });
+ }
+ taskAttributes.project_id = project_id;
+ } else {
+ taskAttributes.project_id = null;
+ }
+
+ await task.update(taskAttributes);
+ await updateTaskTags(task, tags, req.currentUser.id);
+
+ // Reload task with associations
+ const taskWithAssociations = await Task.findByPk(task.id, {
+ include: [
+ { model: Tag, attributes: ['name'], through: { attributes: [] } },
+ { model: Project, attributes: ['name'], required: false }
+ ]
+ });
+
+ res.json({
+ ...taskWithAssociations.toJSON(),
+ due_date: taskWithAssociations.due_date ? taskWithAssociations.due_date.toISOString().split('T')[0] : null
+ });
+ } catch (error) {
+ console.error('Error updating task:', error);
+ res.status(400).json({
+ error: 'There was a problem updating the task.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// PATCH /api/task/:id/toggle_completion
+router.patch('/task/:id/toggle_completion', async (req, res) => {
+ try {
+ const task = await Task.findOne({
+ where: { id: req.params.id, user_id: req.currentUser.id }
+ });
+
+ if (!task) {
+ return res.status(404).json({ error: 'Task not found.' });
+ }
+
+ const newStatus = task.status === Task.STATUS.DONE
+ ? (task.note ? Task.STATUS.IN_PROGRESS : Task.STATUS.NOT_STARTED)
+ : Task.STATUS.DONE;
+
+ await task.update({ status: newStatus });
+
+ res.json({
+ ...task.toJSON(),
+ due_date: task.due_date ? task.due_date.toISOString().split('T')[0] : null
+ });
+ } catch (error) {
+ console.error('Error toggling task completion:', error);
+ res.status(422).json({ error: 'Unable to update task' });
+ }
+});
+
+// DELETE /api/task/:id
+router.delete('/task/:id', async (req, res) => {
+ try {
+ const task = await Task.findOne({
+ where: { id: req.params.id, user_id: req.currentUser.id }
+ });
+
+ if (!task) {
+ return res.status(404).json({ error: 'Task not found.' });
+ }
+
+ await task.destroy();
+ res.json({ message: 'Task successfully deleted' });
+ } catch (error) {
+ console.error('Error deleting task:', error);
+ res.status(400).json({ error: 'There was a problem deleting the task.' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/telegram.js b/backend/routes/telegram.js
new file mode 100644
index 0000000..2a5cce3
--- /dev/null
+++ b/backend/routes/telegram.js
@@ -0,0 +1,113 @@
+const express = require('express');
+const { User } = require('../models');
+const TelegramPoller = require('../services/telegramPoller');
+const router = express.Router();
+
+// POST /api/telegram/start-polling
+router.post('/telegram/start-polling', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user || !user.telegram_bot_token) {
+ return res.status(400).json({ error: 'Telegram bot token not set.' });
+ }
+
+ const poller = TelegramPoller.getInstance();
+ const success = await poller.addUser(user);
+
+ if (success) {
+ res.json({
+ success: true,
+ message: 'Telegram polling started',
+ status: poller.getStatus()
+ });
+ } else {
+ res.status(500).json({ error: 'Failed to start Telegram polling.' });
+ }
+ } catch (error) {
+ console.error('Error starting Telegram polling:', error);
+ res.status(500).json({ error: 'Failed to start Telegram polling.' });
+ }
+});
+
+// POST /api/telegram/stop-polling
+router.post('/telegram/stop-polling', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const poller = TelegramPoller.getInstance();
+ const success = poller.removeUser(req.session.userId);
+
+ res.json({
+ success: true,
+ message: 'Telegram polling stopped',
+ status: poller.getStatus()
+ });
+ } catch (error) {
+ console.error('Error stopping Telegram polling:', error);
+ res.status(500).json({ error: 'Failed to stop Telegram polling.' });
+ }
+});
+
+// GET /api/telegram/polling-status
+router.get('/telegram/polling-status', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const poller = TelegramPoller.getInstance();
+
+ res.json({
+ success: true,
+ status: poller.getStatus()
+ });
+ } catch (error) {
+ console.error('Error getting Telegram polling status:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/telegram/setup
+router.post('/telegram/setup', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { token } = req.body;
+
+ if (!token) {
+ return res.status(400).json({ error: 'Telegram bot token is required.' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ return res.status(404).json({ error: 'User not found.' });
+ }
+
+ // Basic token validation - check if it looks like a Telegram bot token
+ if (!/^\d+:[A-Za-z0-9_-]{35}$/.test(token)) {
+ return res.status(400).json({ error: 'Invalid Telegram bot token format.' });
+ }
+
+ // Update user's telegram bot token
+ await user.update({ telegram_bot_token: token });
+
+ res.json({
+ success: true,
+ message: 'Telegram bot token updated successfully',
+ token: token
+ });
+ } catch (error) {
+ console.error('Error setting up Telegram:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/url.js b/backend/routes/url.js
new file mode 100644
index 0000000..b4edd6c
--- /dev/null
+++ b/backend/routes/url.js
@@ -0,0 +1,156 @@
+const express = require('express');
+const https = require('https');
+const http = require('http');
+const { URL } = require('url');
+const router = express.Router();
+
+// Helper function to extract title from HTML
+function extractTitleFromHtml(html) {
+ const titleMatch = html.match(/]*>([^<]+)<\/title>/i);
+ if (titleMatch && titleMatch[1]) {
+ // Decode HTML entities and clean up
+ return titleMatch[1]
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .trim();
+ }
+ return null;
+}
+
+// Helper function to check if text is a URL
+function isUrl(text) {
+ const urlRegex = /^(https?:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i;
+ return urlRegex.test(text.trim());
+}
+
+// Helper function to fetch URL title
+async function fetchUrlTitle(url) {
+ return new Promise((resolve) => {
+ // Add protocol if missing
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
+ url = 'http://' + url;
+ }
+
+ try {
+ const urlObj = new URL(url);
+ const isHttps = urlObj.protocol === 'https:';
+ const client = isHttps ? https : http;
+
+ const options = {
+ hostname: urlObj.hostname,
+ port: urlObj.port || (isHttps ? 443 : 80),
+ path: urlObj.pathname + urlObj.search,
+ method: 'GET',
+ timeout: 5000,
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ }
+ };
+
+ const req = client.request(options, (res) => {
+ let data = '';
+ let totalBytes = 0;
+ const maxBytes = 50000;
+
+ res.on('data', (chunk) => {
+ totalBytes += chunk.length;
+ if (totalBytes > maxBytes) {
+ req.destroy();
+ return;
+ }
+ data += chunk;
+
+ // Stop if we find the title tag
+ if (data.includes('')) {
+ req.destroy();
+ }
+ });
+
+ res.on('end', () => {
+ const title = extractTitleFromHtml(data);
+ resolve(title);
+ });
+ });
+
+ req.on('error', () => {
+ resolve(null);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ resolve(null);
+ });
+
+ req.end();
+ } catch (error) {
+ resolve(null);
+ }
+ });
+}
+
+// GET /api/url/title
+router.get('/url/title', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { url } = req.query;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL parameter is required' });
+ }
+
+ const title = await fetchUrlTitle(url);
+
+ if (title) {
+ res.json({ url, title });
+ } else {
+ res.json({ url, title: null, error: 'Could not extract title' });
+ }
+ } catch (error) {
+ console.error('Error extracting URL title:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// POST /api/url/extract-from-text
+router.post('/url/extract-from-text', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { text } = req.body;
+
+ if (!text) {
+ return res.status(400).json({ error: 'Text parameter is required' });
+ }
+
+ // Simple URL extraction - look for URLs in text
+ const urlRegex = /(https?:\/\/[^\s]+)/gi;
+ const urls = text.match(urlRegex);
+
+ if (urls && urls.length > 0) {
+ const firstUrl = urls[0];
+ const title = await fetchUrlTitle(firstUrl);
+
+ res.json({
+ found: true,
+ url: firstUrl,
+ title: title,
+ originalText: text
+ });
+ } else {
+ res.json({ found: false });
+ }
+ } catch (error) {
+ console.error('Error extracting URL from text:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/users.js b/backend/routes/users.js
new file mode 100644
index 0000000..24abfff
--- /dev/null
+++ b/backend/routes/users.js
@@ -0,0 +1,206 @@
+const express = require('express');
+const { User } = require('../models');
+const TaskSummaryService = require('../services/taskSummaryService');
+const router = express.Router();
+
+const VALID_FREQUENCIES = ['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h'];
+
+// GET /api/profile
+router.get('/profile', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId, {
+ attributes: [
+ 'id', 'email', 'appearance', 'language', 'timezone',
+ 'avatar_image', 'telegram_bot_token', 'telegram_chat_id',
+ 'task_summary_enabled', 'task_summary_frequency'
+ ]
+ });
+
+ if (!user) {
+ return res.status(404).json({ error: 'Profile not found.' });
+ }
+
+ res.json(user);
+ } catch (error) {
+ console.error('Error fetching profile:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// PATCH /api/profile
+router.patch('/profile', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ return res.status(404).json({ error: 'Profile not found.' });
+ }
+
+ const { appearance, language, timezone, avatar_image, telegram_bot_token } = req.body;
+
+ const allowedUpdates = {};
+ if (appearance !== undefined) allowedUpdates.appearance = appearance;
+ if (language !== undefined) allowedUpdates.language = language;
+ if (timezone !== undefined) allowedUpdates.timezone = timezone;
+ if (avatar_image !== undefined) allowedUpdates.avatar_image = avatar_image;
+ if (telegram_bot_token !== undefined) allowedUpdates.telegram_bot_token = telegram_bot_token;
+
+ await user.update(allowedUpdates);
+
+ // Return updated user with limited fields
+ const updatedUser = await User.findByPk(user.id, {
+ attributes: ['id', 'email', 'appearance', 'language', 'timezone', 'avatar_image', 'telegram_bot_token', 'telegram_chat_id']
+ });
+
+ res.json(updatedUser);
+ } catch (error) {
+ console.error('Error updating profile:', error);
+ res.status(400).json({
+ error: 'Failed to update profile.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// POST /api/profile/task-summary/toggle
+router.post('/profile/task-summary/toggle', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ return res.status(404).json({ error: 'User not found.' });
+ }
+
+ const enabled = !user.task_summary_enabled;
+
+ await user.update({ task_summary_enabled: enabled });
+
+ // Note: Telegram integration would need to be implemented separately
+ const message = enabled
+ ? 'Task summary notifications have been enabled.'
+ : 'Task summary notifications have been disabled.';
+
+ res.json({
+ success: true,
+ enabled: enabled,
+ message: message
+ });
+ } catch (error) {
+ console.error('Error toggling task summary:', error);
+ res.status(400).json({
+ error: 'Failed to update task summary settings.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// POST /api/profile/task-summary/frequency
+router.post('/profile/task-summary/frequency', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const { frequency } = req.body;
+
+ if (!frequency) {
+ return res.status(400).json({ error: 'Frequency is required.' });
+ }
+
+ if (!VALID_FREQUENCIES.includes(frequency)) {
+ return res.status(400).json({ error: 'Invalid frequency value.' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ return res.status(404).json({ error: 'User not found.' });
+ }
+
+ await user.update({ task_summary_frequency: frequency });
+
+ res.json({
+ success: true,
+ frequency: frequency,
+ message: `Task summary frequency has been set to ${frequency}.`
+ });
+ } catch (error) {
+ console.error('Error updating task summary frequency:', error);
+ res.status(400).json({
+ error: 'Failed to update task summary frequency.',
+ details: error.errors ? error.errors.map(e => e.message) : [error.message]
+ });
+ }
+});
+
+// POST /api/profile/task-summary/send-now
+router.post('/profile/task-summary/send-now', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ return res.status(404).json({ error: 'User not found.' });
+ }
+
+ if (!user.telegram_bot_token || !user.telegram_chat_id) {
+ return res.status(400).json({ error: 'Telegram bot is not properly configured.' });
+ }
+
+ // Send the task summary
+ const success = await TaskSummaryService.sendSummaryToUser(user.id);
+
+ if (success) {
+ res.json({
+ success: true,
+ message: 'Task summary was sent to your Telegram.'
+ });
+ } else {
+ res.status(400).json({ error: 'Failed to send message to Telegram.' });
+ }
+ } catch (error) {
+ console.error('Error sending task summary:', error);
+ res.status(400).json({
+ error: 'Error sending message to Telegram.',
+ details: error.message
+ });
+ }
+});
+
+// GET /api/profile/task-summary/status
+router.get('/profile/task-summary/status', async (req, res) => {
+ try {
+ if (!req.session || !req.session.userId) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const user = await User.findByPk(req.session.userId);
+ if (!user) {
+ return res.status(404).json({ error: 'User not found.' });
+ }
+
+ res.json({
+ success: true,
+ enabled: user.task_summary_enabled,
+ frequency: user.task_summary_frequency,
+ last_run: user.task_summary_last_run,
+ next_run: user.task_summary_next_run
+ });
+ } catch (error) {
+ console.error('Error fetching task summary status:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/scripts/db-init.js b/backend/scripts/db-init.js
new file mode 100755
index 0000000..734630c
--- /dev/null
+++ b/backend/scripts/db-init.js
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+
+/**
+ * Database Initialization Script
+ * Initializes the database by creating all tables and dropping existing data
+ */
+
+require('dotenv').config();
+const { sequelize } = require('../models');
+
+async function initDatabase() {
+ try {
+ console.log('Initializing database...');
+ console.log('WARNING: This will drop all existing data!');
+
+ await sequelize.sync({ force: true });
+
+ console.log('✅ Database initialized successfully');
+ console.log('All tables have been created and existing data has been cleared');
+ process.exit(0);
+ } catch (error) {
+ console.error('❌ Error initializing database:', error.message);
+ process.exit(1);
+ }
+}
+
+initDatabase();
\ No newline at end of file
diff --git a/backend/scripts/db-migrate.js b/backend/scripts/db-migrate.js
new file mode 100755
index 0000000..d23c5fc
--- /dev/null
+++ b/backend/scripts/db-migrate.js
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+
+/**
+ * Database Migration Script
+ * Migrates the database by altering existing tables to match current models
+ */
+
+require('dotenv').config();
+const { sequelize } = require('../models');
+
+async function migrateDatabase() {
+ try {
+ console.log('Migrating database...');
+ console.log('This will alter existing tables to match current models');
+
+ await sequelize.sync({ alter: true });
+
+ console.log('✅ Database migrated successfully');
+ console.log('All tables have been updated to match current models');
+ process.exit(0);
+ } catch (error) {
+ console.error('❌ Error migrating database:', error.message);
+ process.exit(1);
+ }
+}
+
+migrateDatabase();
\ No newline at end of file
diff --git a/backend/scripts/db-reset.js b/backend/scripts/db-reset.js
new file mode 100755
index 0000000..dddd7cf
--- /dev/null
+++ b/backend/scripts/db-reset.js
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+
+/**
+ * Database Reset Script
+ * Resets the database by dropping and recreating all tables
+ */
+
+require('dotenv').config();
+const { sequelize } = require('../models');
+
+async function resetDatabase() {
+ try {
+ console.log('Resetting database...');
+ console.log('WARNING: This will permanently delete all data!');
+
+ await sequelize.sync({ force: true });
+
+ console.log('✅ Database reset successfully');
+ console.log('All tables have been dropped and recreated');
+ process.exit(0);
+ } catch (error) {
+ console.error('❌ Error resetting database:', error.message);
+ process.exit(1);
+ }
+}
+
+resetDatabase();
\ No newline at end of file
diff --git a/backend/scripts/db-status.js b/backend/scripts/db-status.js
new file mode 100755
index 0000000..131ca93
--- /dev/null
+++ b/backend/scripts/db-status.js
@@ -0,0 +1,69 @@
+#!/usr/bin/env node
+
+/**
+ * Database Status Script
+ * Shows database connection status and basic information
+ */
+
+require('dotenv').config();
+const { sequelize, User, Task, Project, Area, Note, Tag, InboxItem } = require('../models');
+const fs = require('fs');
+const path = require('path');
+
+async function checkDatabaseStatus() {
+ try {
+ console.log('🔍 Checking database status...\n');
+
+ // Check database file
+ const dbConfig = sequelize.config || sequelize.options;
+ const dbPath = dbConfig.storage || sequelize.options.storage;
+
+ console.log('📂 Database Configuration:');
+ console.log(` Storage: ${dbPath}`);
+ console.log(` Dialect: ${dbConfig.dialect || sequelize.options.dialect || 'sqlite'}`);
+ console.log(` Environment: ${process.env.NODE_ENV || 'development'}`);
+
+ // Check if database file exists
+ if (fs.existsSync(dbPath)) {
+ const stats = fs.statSync(dbPath);
+ console.log(` File size: ${(stats.size / 1024).toFixed(2)} KB`);
+ console.log(` Last modified: ${stats.mtime.toISOString()}`);
+ } else {
+ console.log(' ⚠️ Database file does not exist');
+ }
+
+ console.log('\n🔌 Testing database connection...');
+ await sequelize.authenticate();
+ console.log('✅ Database connection successful\n');
+
+ // Get table information
+ console.log('📊 Table Statistics:');
+ const models = [
+ { name: 'Users', model: User },
+ { name: 'Areas', model: Area },
+ { name: 'Projects', model: Project },
+ { name: 'Tasks', model: Task },
+ { name: 'Notes', model: Note },
+ { name: 'Tags', model: Tag },
+ { name: 'Inbox Items', model: InboxItem }
+ ];
+
+ for (const { name, model } of models) {
+ try {
+ const count = await model.count();
+ console.log(` ${name}: ${count} records`);
+ } catch (error) {
+ console.log(` ${name}: ❌ Error (${error.message})`);
+ }
+ }
+
+ console.log('\n✅ Database status check completed');
+ process.exit(0);
+ } catch (error) {
+ console.error('\n❌ Database connection failed:', error.message);
+ console.error('\n💡 Try running: npm run db:init');
+ process.exit(1);
+ }
+}
+
+checkDatabaseStatus();
\ No newline at end of file
diff --git a/backend/scripts/db-sync.js b/backend/scripts/db-sync.js
new file mode 100755
index 0000000..81ca901
--- /dev/null
+++ b/backend/scripts/db-sync.js
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+
+/**
+ * Database Sync Script
+ * Syncs the database by creating tables if they don't exist (without dropping existing data)
+ */
+
+require('dotenv').config();
+const { sequelize } = require('../models');
+
+async function syncDatabase() {
+ try {
+ console.log('Syncing database...');
+
+ await sequelize.sync();
+
+ console.log('✅ Database synchronized successfully');
+ console.log('All tables have been created (existing data preserved)');
+ process.exit(0);
+ } catch (error) {
+ console.error('❌ Error syncing database:', error.message);
+ process.exit(1);
+ }
+}
+
+syncDatabase();
\ No newline at end of file
diff --git a/backend/scripts/migration-create.js b/backend/scripts/migration-create.js
new file mode 100755
index 0000000..64e4e9a
--- /dev/null
+++ b/backend/scripts/migration-create.js
@@ -0,0 +1,108 @@
+#!/usr/bin/env node
+
+/**
+ * Migration Creation Script
+ * Creates a new Sequelize migration file
+ * Usage: node scripts/migration-create.js
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+function createMigration() {
+ const migrationName = process.argv[2];
+
+ if (!migrationName) {
+ console.error('❌ Usage: npm run migration:create ');
+ console.error('Example: npm run migration:create add-description-to-tasks');
+ process.exit(1);
+ }
+
+ // Generate timestamp (YYYYMMDDHHMMSS format)
+ const now = new Date();
+ const timestamp = now.getFullYear().toString() +
+ (now.getMonth() + 1).toString().padStart(2, '0') +
+ now.getDate().toString().padStart(2, '0') +
+ now.getHours().toString().padStart(2, '0') +
+ now.getMinutes().toString().padStart(2, '0') +
+ now.getSeconds().toString().padStart(2, '0');
+
+ const fileName = `${timestamp}-${migrationName}.js`;
+ const filePath = path.join(__dirname, '..', 'migrations', fileName);
+
+ // Migration template
+ const template = `'use strict';
+
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ // Add your migration logic here
+ // Examples:
+
+ // Add a new column:
+ // await queryInterface.addColumn('table_name', 'column_name', {
+ // type: Sequelize.STRING,
+ // allowNull: true
+ // });
+
+ // Create a new table:
+ // await queryInterface.createTable('table_name', {
+ // id: {
+ // allowNull: false,
+ // autoIncrement: true,
+ // primaryKey: true,
+ // type: Sequelize.INTEGER
+ // },
+ // created_at: {
+ // allowNull: false,
+ // type: Sequelize.DATE,
+ // defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
+ // },
+ // updated_at: {
+ // allowNull: false,
+ // type: Sequelize.DATE,
+ // defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
+ // }
+ // });
+
+ // Add an index:
+ // await queryInterface.addIndex('table_name', ['column_name']);
+
+ throw new Error('Migration not implemented yet!');
+ },
+
+ async down(queryInterface, Sequelize) {
+ // Add your rollback logic here
+ // Examples:
+
+ // Remove a column:
+ // await queryInterface.removeColumn('table_name', 'column_name');
+
+ // Drop a table:
+ // await queryInterface.dropTable('table_name');
+
+ // Remove an index:
+ // await queryInterface.removeIndex('table_name', ['column_name']);
+
+ throw new Error('Rollback not implemented yet!');
+ }
+};`;
+
+ try {
+ fs.writeFileSync(filePath, template);
+ console.log('✅ Migration created successfully');
+ console.log(`📁 File: ${fileName}`);
+ console.log(`📂 Path: ${filePath}`);
+ console.log('');
+ console.log('📝 Next steps:');
+ console.log('1. Edit the migration file to add your schema changes');
+ console.log('2. Run: npm run migration:run');
+ console.log('3. To rollback: npm run migration:undo');
+
+ process.exit(0);
+ } catch (error) {
+ console.error('❌ Error creating migration:', error.message);
+ process.exit(1);
+ }
+}
+
+createMigration();
\ No newline at end of file
diff --git a/backend/scripts/user-create.js b/backend/scripts/user-create.js
new file mode 100755
index 0000000..8ebe3a1
--- /dev/null
+++ b/backend/scripts/user-create.js
@@ -0,0 +1,66 @@
+#!/usr/bin/env node
+
+/**
+ * User Creation Script
+ * Creates a new user with email and password
+ * Usage: node user-create.js
+ */
+
+require('dotenv').config();
+const { User } = require('../models');
+const bcrypt = require('bcrypt');
+
+async function createUser() {
+ const [email, password] = process.argv.slice(2);
+
+ if (!email || !password) {
+ console.error('❌ Usage: npm run user:create ');
+ console.error('Example: npm run user:create admin@example.com mypassword123');
+ process.exit(1);
+ }
+
+ // Basic email validation
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ console.error('❌ Invalid email format');
+ process.exit(1);
+ }
+
+ // Basic password validation
+ if (password.length < 6) {
+ console.error('❌ Password must be at least 6 characters long');
+ process.exit(1);
+ }
+
+ try {
+ console.log(`Creating user with email: ${email}`);
+
+ // Check if user already exists
+ const existingUser = await User.findOne({ where: { email } });
+ if (existingUser) {
+ console.error(`❌ User with email ${email} already exists`);
+ process.exit(1);
+ }
+
+ // Hash the password
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // Create the user
+ const user = await User.create({
+ email,
+ password: hashedPassword
+ });
+
+ console.log('✅ User created successfully');
+ console.log(`📧 Email: ${user.email}`);
+ console.log(`🆔 User ID: ${user.id}`);
+ console.log(`📅 Created: ${user.created_at}`);
+
+ process.exit(0);
+ } catch (error) {
+ console.error('❌ Error creating user:', error.message);
+ process.exit(1);
+ }
+}
+
+createUser();
\ No newline at end of file
diff --git a/backend/services/quotesService.js b/backend/services/quotesService.js
new file mode 100644
index 0000000..0b1bdf2
--- /dev/null
+++ b/backend/services/quotesService.js
@@ -0,0 +1,65 @@
+const fs = require('fs');
+const path = require('path');
+const yaml = require('js-yaml');
+
+class QuotesService {
+ constructor() {
+ this.quotes = [];
+ this.loadQuotes();
+ }
+
+ loadQuotes() {
+ try {
+ const quotesPath = path.join(__dirname, '../config/quotes.yml');
+
+ if (fs.existsSync(quotesPath)) {
+ const fileContents = fs.readFileSync(quotesPath, 'utf8');
+ const data = yaml.load(fileContents);
+
+ if (data && data.quotes && Array.isArray(data.quotes)) {
+ this.quotes = data.quotes;
+ console.log(`Loaded ${this.quotes.length} quotes from configuration`);
+ } else {
+ console.warn('No quotes found in configuration file');
+ this.setDefaultQuotes();
+ }
+ } else {
+ console.warn('Quotes configuration file not found, using defaults');
+ this.setDefaultQuotes();
+ }
+ } catch (error) {
+ console.error('Error loading quotes:', error.message);
+ this.setDefaultQuotes();
+ }
+ }
+
+ setDefaultQuotes() {
+ this.quotes = [
+ "Believe you can and you're halfway there.",
+ "The only way to do great work is to love what you do.",
+ "It always seems impossible until it's done.",
+ "Focus on progress, not perfection.",
+ "One task at a time leads to great accomplishments."
+ ];
+ }
+
+ getRandomQuote() {
+ if (this.quotes.length === 0) {
+ return "Stay focused and keep going!";
+ }
+
+ const randomIndex = Math.floor(Math.random() * this.quotes.length);
+ return this.quotes[randomIndex];
+ }
+
+ getAllQuotes() {
+ return this.quotes;
+ }
+
+ getQuotesCount() {
+ return this.quotes.length;
+ }
+}
+
+// Export singleton instance
+module.exports = new QuotesService();
\ No newline at end of file
diff --git a/backend/services/taskScheduler.js b/backend/services/taskScheduler.js
new file mode 100644
index 0000000..7d24281
--- /dev/null
+++ b/backend/services/taskScheduler.js
@@ -0,0 +1,181 @@
+const cron = require('node-cron');
+const { User } = require('../models');
+const TaskSummaryService = require('./taskSummaryService');
+
+class TaskScheduler {
+ constructor() {
+ this.jobs = new Map();
+ this.isInitialized = false;
+ }
+
+ static getInstance() {
+ if (!TaskScheduler.instance) {
+ TaskScheduler.instance = new TaskScheduler();
+ }
+ return TaskScheduler.instance;
+ }
+
+ async initialize() {
+ if (this.isInitialized) {
+ console.log('Task scheduler already initialized');
+ return;
+ }
+
+ // Don't schedule in test environment
+ if (process.env.NODE_ENV === 'test' || process.env.DISABLE_SCHEDULER === 'true') {
+ console.log('Task scheduler disabled for test environment');
+ return;
+ }
+
+ console.log('Initializing task scheduler...');
+
+ // Daily schedule at 7 AM (for users with daily frequency)
+ const dailyJob = cron.schedule('0 7 * * *', async () => {
+ console.log('Running scheduled task: Daily task summary');
+ await this.processSummariesForFrequency('daily');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ // Weekdays schedule at 7 AM (Monday through Friday)
+ const weekdaysJob = cron.schedule('0 7 * * 1-5', async () => {
+ console.log('Running scheduled task: Weekday task summary');
+ await this.processSummariesForFrequency('weekdays');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ // Weekly schedule at 7 AM on Monday
+ const weeklyJob = cron.schedule('0 7 * * 1', async () => {
+ console.log('Running scheduled task: Weekly task summary');
+ await this.processSummariesForFrequency('weekly');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ // Hourly schedules
+ const hourlyJob = cron.schedule('0 * * * *', async () => {
+ console.log('Running scheduled task: Hourly (1h) task summary');
+ await this.processSummariesForFrequency('1h');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ const twoHourlyJob = cron.schedule('0 */2 * * *', async () => {
+ console.log('Running scheduled task: 2-hour task summary');
+ await this.processSummariesForFrequency('2h');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ const fourHourlyJob = cron.schedule('0 */4 * * *', async () => {
+ console.log('Running scheduled task: 4-hour task summary');
+ await this.processSummariesForFrequency('4h');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ const eightHourlyJob = cron.schedule('0 */8 * * *', async () => {
+ console.log('Running scheduled task: 8-hour task summary');
+ await this.processSummariesForFrequency('8h');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ const twelveHourlyJob = cron.schedule('0 */12 * * *', async () => {
+ console.log('Running scheduled task: 12-hour task summary');
+ await this.processSummariesForFrequency('12h');
+ }, {
+ scheduled: false,
+ timezone: 'UTC'
+ });
+
+ // Store jobs for later management
+ this.jobs.set('daily', dailyJob);
+ this.jobs.set('weekdays', weekdaysJob);
+ this.jobs.set('weekly', weeklyJob);
+ this.jobs.set('1h', hourlyJob);
+ this.jobs.set('2h', twoHourlyJob);
+ this.jobs.set('4h', fourHourlyJob);
+ this.jobs.set('8h', eightHourlyJob);
+ this.jobs.set('12h', twelveHourlyJob);
+
+ // Start all jobs
+ this.jobs.forEach((job, frequency) => {
+ job.start();
+ console.log(`Started scheduler for frequency: ${frequency}`);
+ });
+
+ this.isInitialized = true;
+ console.log('Task scheduler initialized successfully');
+ }
+
+ async processSummariesForFrequency(frequency) {
+ try {
+ const users = await User.findAll({
+ where: {
+ telegram_bot_token: { [require('sequelize').Op.ne]: null },
+ telegram_chat_id: { [require('sequelize').Op.ne]: null },
+ task_summary_enabled: true,
+ task_summary_frequency: frequency
+ }
+ });
+
+ console.log(`Processing ${users.length} users for frequency: ${frequency}`);
+
+ for (const user of users) {
+ try {
+ const success = await TaskSummaryService.sendSummaryToUser(user.id);
+ if (success) {
+ console.log(`Sent ${frequency} summary to user ${user.id}`);
+ } else {
+ console.log(`Failed to send ${frequency} summary to user ${user.id}`);
+ }
+ } catch (error) {
+ console.error(`Error sending ${frequency} summary to user ${user.id}:`, error.message);
+ }
+ }
+ } catch (error) {
+ console.error(`Error processing summaries for frequency ${frequency}:`, error);
+ }
+ }
+
+ async stop() {
+ if (!this.isInitialized) {
+ console.log('Task scheduler not initialized, nothing to stop');
+ return;
+ }
+
+ console.log('Stopping task scheduler...');
+ this.jobs.forEach((job, frequency) => {
+ job.stop();
+ console.log(`Stopped scheduler for frequency: ${frequency}`);
+ });
+
+ this.jobs.clear();
+ this.isInitialized = false;
+ console.log('Task scheduler stopped');
+ }
+
+ async restart() {
+ await this.stop();
+ await this.initialize();
+ }
+
+ getStatus() {
+ return {
+ initialized: this.isInitialized,
+ jobCount: this.jobs.size,
+ jobs: Array.from(this.jobs.keys())
+ };
+ }
+}
+
+module.exports = TaskScheduler;
\ No newline at end of file
diff --git a/backend/services/taskSummaryService.js b/backend/services/taskSummaryService.js
new file mode 100644
index 0000000..07e05c5
--- /dev/null
+++ b/backend/services/taskSummaryService.js
@@ -0,0 +1,238 @@
+const { User, Task, Project, Tag } = require('../models');
+const { Op } = require('sequelize');
+const TelegramPoller = require('./telegramPoller');
+
+class TaskSummaryService {
+ // Helper method to escape special characters for MarkdownV2
+ static escapeMarkdown(text) {
+ if (!text) return '';
+ // Characters that need to be escaped in MarkdownV2: _*[]()~`>#+-=|{}.!
+ return text.toString().replace(/([_*\[\]()~`>#+\-=|{}.!])/g, '\\$1');
+ }
+
+ static async generateSummaryForUser(userId) {
+ try {
+ const user = await User.findByPk(userId);
+ if (!user) return null;
+
+ // Get today's date
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const tomorrow = new Date(today);
+ tomorrow.setDate(tomorrow.getDate() + 1);
+
+ // Get today's tasks, in progress tasks, etc.
+ const dueToday = await Task.findAll({
+ where: {
+ user_id: userId,
+ due_date: {
+ [Op.gte]: today,
+ [Op.lt]: tomorrow
+ },
+ status: { [Op.ne]: 2 } // not done
+ },
+ include: [{ model: Project, attributes: ['name'] }],
+ order: [['name', 'ASC']]
+ });
+
+ const inProgress = await Task.findAll({
+ where: {
+ user_id: userId,
+ status: 1 // in_progress
+ },
+ include: [{ model: Project, attributes: ['name'] }],
+ order: [['name', 'ASC']]
+ });
+
+ const completedToday = await Task.findAll({
+ where: {
+ user_id: userId,
+ status: 2, // done
+ updated_at: {
+ [Op.gte]: today,
+ [Op.lt]: tomorrow
+ }
+ },
+ include: [{ model: Project, attributes: ['name'] }],
+ order: [['name', 'ASC']]
+ });
+
+ // Generate summary message
+ let message = "📋 *Today's Task Summary*\n\n";
+ message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
+ message += "✏️ *Today's Plan*\n\n";
+
+ // Add due today tasks
+ if (dueToday.length > 0) {
+ message += "🚀 *Tasks Due Today:*\n";
+ dueToday.forEach((task, index) => {
+ const priorityEmoji = this.getPriorityEmoji(task.priority);
+ const taskName = this.escapeMarkdown(task.name);
+ const projectInfo = task.Project ? ` \\[${this.escapeMarkdown(task.Project.name)}\\]` : '';
+ message += `${index + 1}\\. ${priorityEmoji} ${taskName}${projectInfo}\n`;
+ });
+ message += "\n";
+ }
+
+ // Add in progress tasks
+ if (inProgress.length > 0) {
+ message += "⚙️ *In Progress Tasks:*\n";
+ inProgress.forEach((task, index) => {
+ const priorityEmoji = this.getPriorityEmoji(task.priority);
+ const taskName = this.escapeMarkdown(task.name);
+ const projectInfo = task.Project ? ` \\[${this.escapeMarkdown(task.Project.name)}\\]` : '';
+ message += `${index + 1}\\. ${priorityEmoji} ${taskName}${projectInfo}\n`;
+ });
+ message += "\n";
+ }
+
+ // Get suggested tasks (not done, not in due today or in progress)
+ const excludedIds = [...dueToday.map(t => t.id), ...inProgress.map(t => t.id)];
+
+ const suggestedTasks = await Task.findAll({
+ where: {
+ user_id: userId,
+ status: { [Op.ne]: 2 }, // not done
+ id: { [Op.notIn]: excludedIds }
+ },
+ include: [{ model: Project, attributes: ['name'] }],
+ order: [['priority', 'DESC'], ['name', 'ASC']],
+ limit: 5
+ });
+
+ if (suggestedTasks.length > 0) {
+ message += "💡 *Suggested Tasks:*\n";
+ suggestedTasks.forEach((task, index) => {
+ const priorityEmoji = this.getPriorityEmoji(task.priority);
+ const taskName = this.escapeMarkdown(task.name);
+ const projectInfo = task.Project ? ` \\[${this.escapeMarkdown(task.Project.name)}\\]` : '';
+ message += `${index + 1}\\. ${priorityEmoji} ${taskName}${projectInfo}\n`;
+ });
+ message += "\n";
+ }
+
+ // Add completed tasks
+ if (completedToday.length > 0) {
+ message += "✅ *Completed Today:*\n";
+ completedToday.forEach((task, index) => {
+ const taskName = this.escapeMarkdown(task.name);
+ const projectInfo = task.Project ? ` \\[${this.escapeMarkdown(task.Project.name)}\\]` : '';
+ message += `${index + 1}\\. ✅ ${taskName}${projectInfo}\n`;
+ });
+ message += "\n";
+ }
+
+ // Add footer
+ message += "━━━━━━━━━━━━━━━━━━━━━━━━\n";
+ message += "🎯 *Stay focused and make it happen\\!*";
+
+ return message;
+ } catch (error) {
+ console.error('Error generating task summary:', error);
+ return null;
+ }
+ }
+
+ static getPriorityEmoji(priority) {
+ switch (priority) {
+ case 2: return '🔴'; // high
+ case 1: return '🟠'; // medium
+ case 0: return '🟢'; // low
+ default: return '⚪';
+ }
+ }
+
+ static async sendSummaryToUser(userId) {
+ try {
+ const user = await User.findByPk(userId);
+ if (!user || !user.telegram_bot_token || !user.telegram_chat_id) {
+ return false;
+ }
+
+ const summary = await this.generateSummaryForUser(userId);
+ if (!summary) return false;
+
+ // Send the message via Telegram
+ const poller = TelegramPoller.getInstance();
+ await poller.sendTelegramMessage(
+ user.telegram_bot_token,
+ user.telegram_chat_id,
+ summary
+ );
+
+ // Update the last run time and calculate the next run time
+ const now = new Date();
+ const nextRun = this.calculateNextRunTime(user, now);
+
+ // Update the user's tracking fields
+ await user.update({
+ task_summary_last_run: now,
+ task_summary_next_run: nextRun
+ });
+
+ return true;
+ } catch (error) {
+ console.error(`Error sending task summary to user ${userId}:`, error.message);
+ return false;
+ }
+ }
+
+ static calculateNextRunTime(user, fromTime = new Date()) {
+ const frequency = user.task_summary_frequency;
+ const from = new Date(fromTime);
+
+ switch (frequency) {
+ case 'daily':
+ // Next day at 7 AM
+ const nextDay = new Date(from);
+ nextDay.setDate(nextDay.getDate() + 1);
+ nextDay.setHours(7, 0, 0, 0);
+ return nextDay;
+
+ case 'weekdays':
+ // Next weekday at 7 AM
+ const currentDay = from.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
+ let daysToAdd = 1;
+ if (currentDay === 5) { // Friday
+ daysToAdd = 3; // Skip to Monday
+ } else if (currentDay === 6) { // Saturday
+ daysToAdd = 2; // Skip to Monday
+ }
+ const nextWeekday = new Date(from);
+ nextWeekday.setDate(nextWeekday.getDate() + daysToAdd);
+ nextWeekday.setHours(7, 0, 0, 0);
+ return nextWeekday;
+
+ case 'weekly':
+ // Next Monday at 7 AM
+ const nextWeek = new Date(from);
+ nextWeek.setDate(nextWeek.getDate() + 7);
+ nextWeek.setHours(7, 0, 0, 0);
+ return nextWeek;
+
+ case '1h':
+ return new Date(from.getTime() + 60 * 60 * 1000);
+
+ case '2h':
+ return new Date(from.getTime() + 2 * 60 * 60 * 1000);
+
+ case '4h':
+ return new Date(from.getTime() + 4 * 60 * 60 * 1000);
+
+ case '8h':
+ return new Date(from.getTime() + 8 * 60 * 60 * 1000);
+
+ case '12h':
+ return new Date(from.getTime() + 12 * 60 * 60 * 1000);
+
+ default:
+ // Default to daily
+ const defaultNext = new Date(from);
+ defaultNext.setDate(defaultNext.getDate() + 1);
+ defaultNext.setHours(7, 0, 0, 0);
+ return defaultNext;
+ }
+ }
+}
+
+module.exports = TaskSummaryService;
\ No newline at end of file
diff --git a/backend/services/telegramInitializer.js b/backend/services/telegramInitializer.js
new file mode 100644
index 0000000..07b9ed0
--- /dev/null
+++ b/backend/services/telegramInitializer.js
@@ -0,0 +1,43 @@
+const TelegramPoller = require('./telegramPoller');
+const { User } = require('../models');
+
+async function initializeTelegramPolling() {
+ if (process.env.NODE_ENV === 'test' || process.env.DISABLE_TELEGRAM === 'true') {
+ return;
+ }
+
+ console.log('Initializing Telegram polling for configured users...');
+
+ try {
+ // Get singleton instance of the poller
+ const poller = TelegramPoller.getInstance();
+
+ // Find users with configured Telegram tokens
+ const usersWithTelegram = await User.findAll({
+ where: {
+ telegram_bot_token: {
+ [require('sequelize').Op.ne]: null
+ }
+ }
+ });
+
+ if (usersWithTelegram.length > 0) {
+ console.log(`Found ${usersWithTelegram.length} users with Telegram configuration`);
+
+ // Add each user to the polling list
+ for (const user of usersWithTelegram) {
+ console.log(`Starting Telegram polling for user ${user.id}`);
+ await poller.addUser(user);
+ }
+
+ console.log('Telegram polling initialized successfully');
+ } else {
+ console.log('No users with Telegram configuration found');
+ }
+ } catch (error) {
+ console.error('Error initializing Telegram polling:', error.message);
+ console.error('Telegram polling will be initialized later when the database is available.');
+ }
+}
+
+module.exports = { initializeTelegramPolling };
\ No newline at end of file
diff --git a/backend/services/telegramPoller.js b/backend/services/telegramPoller.js
new file mode 100644
index 0000000..2a73050
--- /dev/null
+++ b/backend/services/telegramPoller.js
@@ -0,0 +1,261 @@
+const https = require('https');
+const { User, InboxItem } = require('../models');
+
+class TelegramPoller {
+ constructor() {
+ this.running = false;
+ this.interval = null;
+ this.pollInterval = 5000; // 5 seconds
+ this.usersToPool = [];
+ this.userStatus = {};
+ }
+
+ // Singleton pattern
+ static getInstance() {
+ if (!TelegramPoller.instance) {
+ TelegramPoller.instance = new TelegramPoller();
+ }
+ return TelegramPoller.instance;
+ }
+
+ // Add user to polling list
+ async addUser(user) {
+ if (!user || !user.telegram_bot_token) {
+ return false;
+ }
+
+ // Check if user already in list
+ const exists = this.usersToPool.find(u => u.id === user.id);
+ if (!exists) {
+ this.usersToPool.push(user);
+ }
+
+ // Start polling if not already running
+ if (this.usersToPool.length > 0 && !this.running) {
+ this.startPolling();
+ }
+
+ return true;
+ }
+
+ // Remove user from polling list
+ removeUser(userId) {
+ this.usersToPool = this.usersToPool.filter(u => u.id !== userId);
+ delete this.userStatus[userId];
+
+ // Stop polling if no users left
+ if (this.usersToPool.length === 0 && this.running) {
+ this.stopPolling();
+ }
+
+ return true;
+ }
+
+ // Start the polling process
+ startPolling() {
+ if (this.running) return;
+
+ console.log('Starting Telegram polling...');
+ this.running = true;
+
+ this.interval = setInterval(async () => {
+ try {
+ await this.pollUpdates();
+ } catch (error) {
+ console.error('Error polling Telegram:', error.message);
+ }
+ }, this.pollInterval);
+ }
+
+ // Stop the polling process
+ stopPolling() {
+ if (!this.running) return;
+
+ console.log('Stopping Telegram polling...');
+ this.running = false;
+
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ }
+
+ // Poll for updates from Telegram
+ async pollUpdates() {
+ for (const user of this.usersToPool) {
+ const token = user.telegram_bot_token;
+ if (!token) continue;
+
+ try {
+ const lastUpdateId = this.userStatus[user.id]?.lastUpdateId || 0;
+ const updates = await this.getTelegramUpdates(token, lastUpdateId + 1);
+
+ if (updates && updates.length > 0) {
+ await this.processUpdates(user, updates);
+ }
+ } catch (error) {
+ console.error(`Error getting updates for user ${user.id}:`, error.message);
+ }
+ }
+ }
+
+ // Get updates from Telegram API
+ getTelegramUpdates(token, offset) {
+ return new Promise((resolve, reject) => {
+ const url = `https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=1`;
+
+ https.get(url, { timeout: 5000 }, (res) => {
+ let data = '';
+
+ res.on('data', (chunk) => {
+ data += chunk;
+ });
+
+ res.on('end', () => {
+ try {
+ const response = JSON.parse(data);
+ if (response.ok && Array.isArray(response.result)) {
+ resolve(response.result);
+ } else {
+ console.error('Telegram API error:', response);
+ resolve([]);
+ }
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }).on('error', (error) => {
+ reject(error);
+ }).on('timeout', () => {
+ reject(new Error('Request timeout'));
+ });
+ });
+ }
+
+ // Process updates received from Telegram
+ async processUpdates(user, updates) {
+ if (!updates.length) return;
+
+ // Track the highest update_id
+ const highestUpdateId = Math.max(...updates.map(u => u.update_id));
+
+ // Save the last update ID for this user
+ if (!this.userStatus[user.id]) {
+ this.userStatus[user.id] = {};
+ }
+ this.userStatus[user.id].lastUpdateId = highestUpdateId;
+
+ for (const update of updates) {
+ try {
+ if (update.message && update.message.text) {
+ await this.processMessage(user, update);
+ }
+ } catch (error) {
+ console.error(`Error processing update ${update.update_id}:`, error.message);
+ }
+ }
+ }
+
+ // Process a single message
+ async processMessage(user, update) {
+ const message = update.message;
+ const text = message.text;
+ const chatId = message.chat.id.toString();
+ const messageId = message.message_id;
+
+ console.log(`Processing message from user ${user.id}: ${text}`);
+
+ // Save the chat_id if not already saved
+ if (!user.telegram_chat_id) {
+ console.log(`Updating user's telegram_chat_id to ${chatId}`);
+ await User.update(
+ { telegram_chat_id: chatId },
+ { where: { id: user.id } }
+ );
+ user.telegram_chat_id = chatId; // Update local object
+ }
+
+ try {
+ // Create an inbox item
+ const inboxItem = await InboxItem.create({
+ content: text,
+ source: 'telegram',
+ user_id: user.id
+ });
+
+ console.log(`Created inbox item ${inboxItem.id} from Telegram message`);
+
+ // Send confirmation
+ await this.sendTelegramMessage(
+ user.telegram_bot_token,
+ chatId,
+ `✅ Added to Tududi inbox: "${text}"`,
+ messageId
+ );
+ } catch (error) {
+ console.error('Failed to create inbox item:', error.message);
+
+ // Send error message
+ await this.sendTelegramMessage(
+ user.telegram_bot_token,
+ chatId,
+ `❌ Failed to add to inbox: ${error.message}`,
+ messageId
+ );
+ }
+ }
+
+ // Send a message to Telegram
+ sendTelegramMessage(token, chatId, text, replyToMessageId = null) {
+ return new Promise((resolve, reject) => {
+ const messageParams = {
+ chat_id: chatId,
+ text: text
+ };
+
+ if (replyToMessageId) {
+ messageParams.reply_to_message_id = replyToMessageId;
+ }
+
+ const postData = JSON.stringify(messageParams);
+ const url = `https://api.telegram.org/bot${token}/sendMessage`;
+
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(postData)
+ }
+ };
+
+ const req = https.request(url, options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => data += chunk);
+ res.on('end', () => {
+ try {
+ const response = JSON.parse(data);
+ resolve(response);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+
+ req.on('error', reject);
+ req.write(postData);
+ req.end();
+ });
+ }
+
+ // Get status of the poller
+ getStatus() {
+ return {
+ running: this.running,
+ usersCount: this.usersToPool.length,
+ pollInterval: this.pollInterval,
+ userStatus: this.userStatus
+ };
+ }
+}
+
+module.exports = TelegramPoller;
\ No newline at end of file
diff --git a/backend/start.sh b/backend/start.sh
new file mode 100755
index 0000000..3fd3f0e
--- /dev/null
+++ b/backend/start.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+# Start script for Express backend
+
+echo "Starting Express backend..."
+echo "Make sure to set environment variables if needed:"
+echo " TUDUDI_SESSION_SECRET=your_secret_here"
+echo " TUDUDI_USER_EMAIL=your_email@example.com"
+echo " TUDUDI_USER_PASSWORD=your_password"
+echo ""
+
+PORT=3001 npm start
\ No newline at end of file
diff --git a/backend/tests/README.md b/backend/tests/README.md
new file mode 100644
index 0000000..e76f1fb
--- /dev/null
+++ b/backend/tests/README.md
@@ -0,0 +1,82 @@
+# Backend Test Suite
+
+This directory contains the test suite for the tududi backend Express application.
+
+## Structure
+
+```
+tests/
+├── unit/ # Unit tests for individual components
+│ ├── models/ # Model tests
+│ ├── middleware/ # Middleware tests
+│ └── services/ # Service tests
+├── integration/ # Integration tests for API endpoints
+├── fixtures/ # Test data fixtures
+└── helpers/ # Test utilities and helpers
+```
+
+## Running Tests
+
+### All Tests
+```bash
+npm test
+```
+
+### Unit Tests Only
+```bash
+npm run test:unit
+```
+
+### Integration Tests Only
+```bash
+npm run test:integration
+```
+
+### Watch Mode (for development)
+```bash
+npm run test:watch
+```
+
+### Coverage Report
+```bash
+npm run test:coverage
+```
+
+## Test Environment
+
+Tests run in a separate test environment with:
+- In-memory SQLite database (isolated from development data)
+- Test-specific configuration from `.env.test`
+- Automatic database cleanup between tests
+
+## Writing Tests
+
+### Unit Tests
+- Test individual functions, models, or middleware in isolation
+- Mock external dependencies
+- Focus on business logic and edge cases
+
+### Integration Tests
+- Test complete API endpoints
+- Use authenticated requests where needed
+- Test real database interactions
+- Verify response formats and status codes
+
+### Test Utilities
+- `tests/helpers/testUtils.js` provides utilities for creating test data
+- `tests/helpers/setup.js` handles database setup and cleanup
+- Use `createTestUser()` for creating authenticated test users
+
+## Best Practices
+
+1. **Isolation**: Each test should be independent and not rely on other tests
+2. **Cleanup**: Database is automatically cleaned between tests
+3. **Authentication**: Use test utilities for creating authenticated requests
+4. **Descriptive Names**: Test names should clearly describe what is being tested
+5. **Coverage**: Aim for high test coverage of critical business logic
+
+## Dependencies
+
+- **Jest**: Test framework
+- **Supertest**: HTTP testing library for integration tests
+- **cross-env**: Cross-platform environment variable setting
\ No newline at end of file
diff --git a/backend/tests/helpers/setup.js b/backend/tests/helpers/setup.js
new file mode 100644
index 0000000..651bc9a
--- /dev/null
+++ b/backend/tests/helpers/setup.js
@@ -0,0 +1,39 @@
+// Set test environment before importing models
+process.env.NODE_ENV = 'test';
+
+const { sequelize } = require('../../models');
+
+beforeAll(async () => {
+ await sequelize.sync({ force: true });
+}, 30000);
+
+beforeEach(async () => {
+ // Clean all tables except Sessions to avoid conflicts
+ try {
+ const models = Object.values(sequelize.models);
+ const nonSessionModels = models.filter(model => model.name !== 'Session');
+ await Promise.all(nonSessionModels.map(model => model.destroy({ truncate: true, cascade: true })));
+ } catch (error) {
+ // Ignore errors during cleanup
+ }
+});
+
+afterEach(async () => {
+ // Clean up sessions after each test
+ try {
+ const Session = sequelize.models.Session;
+ if (Session) {
+ await Session.destroy({ truncate: true });
+ }
+ } catch (error) {
+ // Ignore errors during session cleanup
+ }
+});
+
+afterAll(async () => {
+ try {
+ await sequelize.close();
+ } catch (error) {
+ // Database may already be closed
+ }
+}, 30000);
\ No newline at end of file
diff --git a/backend/tests/helpers/testUtils.js b/backend/tests/helpers/testUtils.js
new file mode 100644
index 0000000..18d0571
--- /dev/null
+++ b/backend/tests/helpers/testUtils.js
@@ -0,0 +1,28 @@
+const bcrypt = require('bcrypt');
+const { User } = require('../../models');
+
+const createTestUser = async (userData = {}) => {
+ const defaultUser = {
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10),
+ ...userData
+ };
+
+ return await User.create(defaultUser);
+};
+
+const authenticateUser = async (request, user) => {
+ const response = await request
+ .post('/api/login')
+ .send({
+ email: user.email,
+ password: 'password123'
+ });
+
+ return response.headers['set-cookie'];
+};
+
+module.exports = {
+ createTestUser,
+ authenticateUser
+};
\ No newline at end of file
diff --git a/backend/tests/integration/areas.test.js b/backend/tests/integration/areas.test.js
new file mode 100644
index 0000000..99140e5
--- /dev/null
+++ b/backend/tests/integration/areas.test.js
@@ -0,0 +1,280 @@
+const request = require('supertest');
+const app = require('../../app');
+const { Area, User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Areas Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/areas', () => {
+ it('should create a new area', async () => {
+ const areaData = {
+ name: 'Work',
+ description: 'Work related projects'
+ };
+
+ const response = await agent
+ .post('/api/areas')
+ .send(areaData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.name).toBe(areaData.name);
+ expect(response.body.description).toBe(areaData.description);
+ expect(response.body.user_id).toBe(user.id);
+ });
+
+ it('should require authentication', async () => {
+ const areaData = {
+ name: 'Work'
+ };
+
+ const response = await request(app)
+ .post('/api/areas')
+ .send(areaData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require area name', async () => {
+ const areaData = {
+ description: 'Area without name'
+ };
+
+ const response = await agent
+ .post('/api/areas')
+ .send(areaData);
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Area name is required.');
+ });
+ });
+
+ describe('GET /api/areas', () => {
+ let area1, area2;
+
+ beforeEach(async () => {
+ area1 = await Area.create({
+ name: 'Work',
+ description: 'Work projects',
+ user_id: user.id
+ });
+
+ area2 = await Area.create({
+ name: 'Personal',
+ description: 'Personal projects',
+ user_id: user.id
+ });
+ });
+
+ it('should get all user areas', async () => {
+ const response = await agent.get('/api/areas');
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveLength(2);
+ expect(response.body.map(a => a.id)).toContain(area1.id);
+ expect(response.body.map(a => a.id)).toContain(area2.id);
+ });
+
+ it('should order areas by name', async () => {
+ const response = await agent.get('/api/areas');
+
+ expect(response.status).toBe(200);
+ expect(response.body[0].name).toBe('Personal'); // P comes before W
+ expect(response.body[1].name).toBe('Work');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/areas');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/areas/:id', () => {
+ let area;
+
+ beforeEach(async () => {
+ area = await Area.create({
+ name: 'Work',
+ description: 'Work projects',
+ user_id: user.id
+ });
+ });
+
+ it('should get area by id', async () => {
+ const response = await agent.get(`/api/areas/${area.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBe(area.id);
+ expect(response.body.name).toBe(area.name);
+ expect(response.body.description).toBe(area.description);
+ });
+
+ it('should return 404 for non-existent area', async () => {
+ const response = await agent.get('/api/areas/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe("Area not found or doesn't belong to the current user.");
+ });
+
+ it('should not allow access to other user\'s areas', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherArea = await Area.create({
+ name: 'Other Area',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.get(`/api/areas/${otherArea.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe("Area not found or doesn't belong to the current user.");
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get(`/api/areas/${area.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('PATCH /api/areas/:id', () => {
+ let area;
+
+ beforeEach(async () => {
+ area = await Area.create({
+ name: 'Work',
+ description: 'Work projects',
+ user_id: user.id
+ });
+ });
+
+ it('should update area', async () => {
+ const updateData = {
+ name: 'Updated Work',
+ description: 'Updated description'
+ };
+
+ const response = await agent
+ .patch(`/api/areas/${area.id}`)
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.name).toBe(updateData.name);
+ expect(response.body.description).toBe(updateData.description);
+ });
+
+ it('should return 404 for non-existent area', async () => {
+ const response = await agent
+ .patch('/api/areas/999999')
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Area not found.');
+ });
+
+ it('should not allow updating other user\'s areas', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherArea = await Area.create({
+ name: 'Other Area',
+ user_id: otherUser.id
+ });
+
+ const response = await agent
+ .patch(`/api/areas/${otherArea.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Area not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .patch(`/api/areas/${area.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('DELETE /api/areas/:id', () => {
+ let area;
+
+ beforeEach(async () => {
+ area = await Area.create({
+ name: 'Work',
+ user_id: user.id
+ });
+ });
+
+ it('should delete area', async () => {
+ const response = await agent.delete(`/api/areas/${area.id}`);
+
+ expect(response.status).toBe(204);
+
+ // Verify area is deleted
+ const deletedArea = await Area.findByPk(area.id);
+ expect(deletedArea).toBeNull();
+ });
+
+ it('should return 404 for non-existent area', async () => {
+ const response = await agent.delete('/api/areas/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Area not found.');
+ });
+
+ it('should not allow deleting other user\'s areas', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherArea = await Area.create({
+ name: 'Other Area',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.delete(`/api/areas/${otherArea.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Area not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).delete(`/api/areas/${area.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/auth.test.js b/backend/tests/integration/auth.test.js
new file mode 100644
index 0000000..81e61f9
--- /dev/null
+++ b/backend/tests/integration/auth.test.js
@@ -0,0 +1,155 @@
+const request = require('supertest');
+const app = require('../../app');
+const { User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Auth Routes', () => {
+ describe('POST /api/login', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+ });
+
+ it('should login with valid credentials', async () => {
+ const response = await request(app)
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.body.user).toBeDefined();
+ expect(response.body.user.email).toBe('test@example.com');
+ expect(response.body.user.id).toBe(user.id);
+ expect(response.body.user.language).toBe('en');
+ expect(response.body.user.appearance).toBe('light');
+ expect(response.body.user.timezone).toBe('UTC');
+ });
+
+ it('should return 400 for missing email', async () => {
+ const response = await request(app)
+ .post('/api/login')
+ .send({
+ password: 'password123'
+ });
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Invalid login parameters.');
+ });
+
+ it('should return 400 for missing password', async () => {
+ const response = await request(app)
+ .post('/api/login')
+ .send({
+ email: 'test@example.com'
+ });
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Invalid login parameters.');
+ });
+
+ it('should return 401 for non-existent user', async () => {
+ const response = await request(app)
+ .post('/api/login')
+ .send({
+ email: 'nonexistent@example.com',
+ password: 'password123'
+ });
+
+ expect(response.status).toBe(401);
+ expect(response.body.errors).toEqual(['Invalid credentials']);
+ });
+
+ it('should return 401 for invalid password', async () => {
+ const response = await request(app)
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'wrongpassword'
+ });
+
+ expect(response.status).toBe(401);
+ expect(response.body.errors).toEqual(['Invalid credentials']);
+ });
+ });
+
+ describe('GET /api/current_user', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+ });
+
+ it('should return current user when logged in', async () => {
+ const agent = request.agent(app);
+
+ // Login first
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+
+ // Check current user
+ const response = await agent.get('/api/current_user');
+
+ expect(response.status).toBe(200);
+ expect(response.body.user).toBeDefined();
+ expect(response.body.user.email).toBe('test@example.com');
+ expect(response.body.user.id).toBe(user.id);
+ });
+
+ it('should return null user when not logged in', async () => {
+ const response = await request(app).get('/api/current_user');
+
+ expect(response.status).toBe(200);
+ expect(response.body.user).toBeNull();
+ });
+ });
+
+ describe('GET /api/logout', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+ });
+
+ it('should logout successfully', async () => {
+ const agent = request.agent(app);
+
+ // Login first
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+
+ // Logout
+ const response = await agent.get('/api/logout');
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Logged out successfully');
+
+ // Verify user is logged out
+ const currentUserResponse = await agent.get('/api/current_user');
+ expect(currentUserResponse.body.user).toBeNull();
+ });
+
+ it('should handle logout when not logged in', async () => {
+ const response = await request(app).get('/api/logout');
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Logged out successfully');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/inbox.test.js b/backend/tests/integration/inbox.test.js
new file mode 100644
index 0000000..cc7d6b5
--- /dev/null
+++ b/backend/tests/integration/inbox.test.js
@@ -0,0 +1,275 @@
+const request = require('supertest');
+const app = require('../../app');
+const { InboxItem, User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Inbox Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/inbox', () => {
+ it('should create a new inbox item', async () => {
+ const inboxData = {
+ content: 'Remember to buy groceries',
+ source: 'web'
+ };
+
+ const response = await agent
+ .post('/api/inbox')
+ .send(inboxData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.content).toBe(inboxData.content);
+ expect(response.body.source).toBe(inboxData.source);
+ expect(response.body.status).toBe('added');
+ expect(response.body.user_id).toBe(user.id);
+ });
+
+ it('should require authentication', async () => {
+ const inboxData = {
+ content: 'Test content'
+ };
+
+ const response = await request(app)
+ .post('/api/inbox')
+ .send(inboxData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require content', async () => {
+ const inboxData = {};
+
+ const response = await agent
+ .post('/api/inbox')
+ .send(inboxData);
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Content is required');
+ });
+ });
+
+ describe('GET /api/inbox', () => {
+ let inboxItem1, inboxItem2;
+
+ beforeEach(async () => {
+ inboxItem1 = await InboxItem.create({
+ content: 'First item',
+ status: 'added',
+ user_id: user.id
+ });
+
+ inboxItem2 = await InboxItem.create({
+ content: 'Second item',
+ status: 'processed',
+ user_id: user.id
+ });
+ });
+
+ it('should get all user inbox items', async () => {
+ const response = await agent.get('/api/inbox');
+
+ expect(response.status).toBe(200);
+ expect(Array.isArray(response.body)).toBe(true);
+ expect(response.body.length).toBe(1); // Only items with status 'added' are returned
+ expect(response.body.map(i => i.id)).toContain(inboxItem1.id);
+ });
+
+ it('should only return items with added status', async () => {
+ const response = await agent.get('/api/inbox');
+
+ expect(response.status).toBe(200);
+ expect(response.body.length).toBe(1);
+ expect(response.body[0].id).toBe(inboxItem1.id);
+ expect(response.body[0].status).toBe('added');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/inbox');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/inbox/:id', () => {
+ let inboxItem;
+
+ beforeEach(async () => {
+ inboxItem = await InboxItem.create({
+ content: 'Test content',
+ user_id: user.id
+ });
+ });
+
+ it('should get inbox item by id', async () => {
+ const response = await agent.get(`/api/inbox/${inboxItem.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBe(inboxItem.id);
+ expect(response.body.content).toBe(inboxItem.content);
+ });
+
+ it('should return 404 for non-existent inbox item', async () => {
+ const response = await agent.get('/api/inbox/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Inbox item not found.');
+ });
+
+ it('should not allow access to other user\'s inbox items', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherInboxItem = await InboxItem.create({
+ content: 'Other content',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.get(`/api/inbox/${otherInboxItem.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Inbox item not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get(`/api/inbox/${inboxItem.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('PATCH /api/inbox/:id', () => {
+ let inboxItem;
+
+ beforeEach(async () => {
+ inboxItem = await InboxItem.create({
+ content: 'Test content',
+ status: 'added',
+ user_id: user.id
+ });
+ });
+
+ it('should update inbox item', async () => {
+ const updateData = {
+ content: 'Updated content',
+ status: 'processed'
+ };
+
+ const response = await agent
+ .patch(`/api/inbox/${inboxItem.id}`)
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.content).toBe(updateData.content);
+ expect(response.body.status).toBe(updateData.status);
+ });
+
+ it('should return 404 for non-existent inbox item', async () => {
+ const response = await agent
+ .patch('/api/inbox/999999')
+ .send({ content: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Inbox item not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .patch(`/api/inbox/${inboxItem.id}`)
+ .send({ content: 'Updated' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('DELETE /api/inbox/:id', () => {
+ let inboxItem;
+
+ beforeEach(async () => {
+ inboxItem = await InboxItem.create({
+ content: 'Test content',
+ user_id: user.id
+ });
+ });
+
+ it('should delete inbox item', async () => {
+ const response = await agent.delete(`/api/inbox/${inboxItem.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Inbox item successfully deleted');
+
+ // Verify inbox item status is updated to deleted
+ const deletedItem = await InboxItem.findByPk(inboxItem.id);
+ expect(deletedItem).not.toBeNull();
+ expect(deletedItem.status).toBe('deleted');
+ });
+
+ it('should return 404 for non-existent inbox item', async () => {
+ const response = await agent.delete('/api/inbox/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Inbox item not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).delete(`/api/inbox/${inboxItem.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('PATCH /api/inbox/:id/process', () => {
+ let inboxItem;
+
+ beforeEach(async () => {
+ inboxItem = await InboxItem.create({
+ content: 'Test content',
+ status: 'added',
+ user_id: user.id
+ });
+ });
+
+ it('should process inbox item', async () => {
+ const response = await agent.patch(`/api/inbox/${inboxItem.id}/process`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.status).toBe('processed');
+ });
+
+ it('should return 404 for non-existent inbox item', async () => {
+ const response = await agent.patch('/api/inbox/999999/process');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Inbox item not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).patch(`/api/inbox/${inboxItem.id}/process`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/notes.test.js b/backend/tests/integration/notes.test.js
new file mode 100644
index 0000000..7ac001b
--- /dev/null
+++ b/backend/tests/integration/notes.test.js
@@ -0,0 +1,308 @@
+const request = require('supertest');
+const app = require('../../app');
+const { Note, User, Project } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Notes Routes', () => {
+ let user, project, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/note', () => {
+ it('should create a new note', async () => {
+ const noteData = {
+ title: 'Test Note',
+ content: 'This is a test note content',
+ project_id: project.id
+ };
+
+ const response = await agent
+ .post('/api/note')
+ .send(noteData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.title).toBe(noteData.title);
+ expect(response.body.content).toBe(noteData.content);
+ expect(response.body.project_id).toBe(project.id);
+ expect(response.body.user_id).toBe(user.id);
+ });
+
+ it('should create note without project', async () => {
+ const noteData = {
+ title: 'Test Note',
+ content: 'This is a test note content'
+ };
+
+ const response = await agent
+ .post('/api/note')
+ .send(noteData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.title).toBe(noteData.title);
+ expect(response.body.content).toBe(noteData.content);
+ expect(response.body.project_id).toBeNull();
+ expect(response.body.user_id).toBe(user.id);
+ });
+
+ it('should require authentication', async () => {
+ const noteData = {
+ title: 'Test Note',
+ content: 'Test content'
+ };
+
+ const response = await request(app)
+ .post('/api/note')
+ .send(noteData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/notes', () => {
+ let note1, note2;
+
+ beforeEach(async () => {
+ note1 = await Note.create({
+ title: 'Note 1',
+ content: 'First note content',
+ user_id: user.id,
+ project_id: project.id
+ });
+
+ note2 = await Note.create({
+ title: 'Note 2',
+ content: 'Second note content',
+ user_id: user.id
+ });
+ });
+
+ it('should get all user notes', async () => {
+ const response = await agent.get('/api/notes');
+
+ expect(response.status).toBe(200);
+ expect(Array.isArray(response.body)).toBe(true);
+ expect(response.body.length).toBe(2);
+ expect(response.body.map(n => n.id)).toContain(note1.id);
+ expect(response.body.map(n => n.id)).toContain(note2.id);
+ });
+
+ it('should include project information', async () => {
+ const response = await agent.get('/api/notes');
+
+ expect(response.status).toBe(200);
+ const noteWithProject = response.body.find(n => n.id === note1.id);
+ expect(noteWithProject.Project).toBeDefined();
+ expect(noteWithProject.Project.name).toBe(project.name);
+ });
+
+ it('should return all notes when no filter is applied', async () => {
+ const response = await agent.get('/api/notes');
+
+ expect(response.status).toBe(200);
+ expect(response.body.length).toBe(2);
+ expect(response.body.map(n => n.id)).toContain(note1.id);
+ expect(response.body.map(n => n.id)).toContain(note2.id);
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/notes');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/note/:id', () => {
+ let note;
+
+ beforeEach(async () => {
+ note = await Note.create({
+ title: 'Test Note',
+ content: 'Test content',
+ user_id: user.id,
+ project_id: project.id
+ });
+ });
+
+ it('should get note by id', async () => {
+ const response = await agent.get(`/api/note/${note.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBe(note.id);
+ expect(response.body.title).toBe(note.title);
+ expect(response.body.content).toBe(note.content);
+ });
+
+ it('should return 404 for non-existent note', async () => {
+ const response = await agent.get('/api/note/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Note not found.');
+ });
+
+ it('should not allow access to other user\'s notes', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherNote = await Note.create({
+ title: 'Other Note',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.get(`/api/note/${otherNote.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Note not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get(`/api/note/${note.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('PATCH /api/note/:id', () => {
+ let note;
+
+ beforeEach(async () => {
+ note = await Note.create({
+ title: 'Test Note',
+ content: 'Test content',
+ user_id: user.id
+ });
+ });
+
+ it('should update note', async () => {
+ const updateData = {
+ title: 'Updated Note',
+ content: 'Updated content',
+ project_id: project.id
+ };
+
+ const response = await agent
+ .patch(`/api/note/${note.id}`)
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.title).toBe(updateData.title);
+ expect(response.body.content).toBe(updateData.content);
+ expect(response.body.project_id).toBe(project.id);
+ });
+
+ it('should return 404 for non-existent note', async () => {
+ const response = await agent
+ .patch('/api/note/999999')
+ .send({ title: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Note not found.');
+ });
+
+ it('should not allow updating other user\'s notes', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherNote = await Note.create({
+ title: 'Other Note',
+ user_id: otherUser.id
+ });
+
+ const response = await agent
+ .patch(`/api/note/${otherNote.id}`)
+ .send({ title: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Note not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .patch(`/api/note/${note.id}`)
+ .send({ title: 'Updated' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('DELETE /api/note/:id', () => {
+ let note;
+
+ beforeEach(async () => {
+ note = await Note.create({
+ title: 'Test Note',
+ user_id: user.id
+ });
+ });
+
+ it('should delete note', async () => {
+ const response = await agent.delete(`/api/note/${note.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Note deleted successfully.');
+
+ // Verify note is deleted
+ const deletedNote = await Note.findByPk(note.id);
+ expect(deletedNote).toBeNull();
+ });
+
+ it('should return 404 for non-existent note', async () => {
+ const response = await agent.delete('/api/note/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Note not found.');
+ });
+
+ it('should not allow deleting other user\'s notes', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherNote = await Note.create({
+ title: 'Other Note',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.delete(`/api/note/${otherNote.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Note not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).delete(`/api/note/${note.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/projects.test.js b/backend/tests/integration/projects.test.js
new file mode 100644
index 0000000..016064a
--- /dev/null
+++ b/backend/tests/integration/projects.test.js
@@ -0,0 +1,303 @@
+const request = require('supertest');
+const app = require('../../app');
+const { Project, User, Area } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Projects Routes', () => {
+ let user, area, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ area = await Area.create({
+ name: 'Work',
+ user_id: user.id
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/project', () => {
+ it('should create a new project', async () => {
+ const projectData = {
+ name: 'Test Project',
+ description: 'Test Description',
+ active: true,
+ pin_to_sidebar: false,
+ priority: 1,
+ area_id: area.id
+ };
+
+ const response = await agent
+ .post('/api/project')
+ .send(projectData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.name).toBe(projectData.name);
+ expect(response.body.description).toBe(projectData.description);
+ expect(response.body.active).toBe(projectData.active);
+ expect(response.body.pin_to_sidebar).toBe(projectData.pin_to_sidebar);
+ expect(response.body.priority).toBe(projectData.priority);
+ expect(response.body.area_id).toBe(area.id);
+ expect(response.body.user_id).toBe(user.id);
+ });
+
+ it('should require authentication', async () => {
+ const projectData = {
+ name: 'Test Project'
+ };
+
+ const response = await request(app)
+ .post('/api/project')
+ .send(projectData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require project name', async () => {
+ const projectData = {
+ description: 'Project without name'
+ };
+
+ const response = await agent
+ .post('/api/project')
+ .send(projectData);
+
+ expect(response.status).toBe(400);
+ });
+ });
+
+ describe('GET /api/projects', () => {
+ let project1, project2;
+
+ beforeEach(async () => {
+ project1 = await Project.create({
+ name: 'Project 1',
+ description: 'First project',
+ user_id: user.id,
+ area_id: area.id
+ });
+
+ project2 = await Project.create({
+ name: 'Project 2',
+ description: 'Second project',
+ user_id: user.id
+ });
+ });
+
+ it('should get all user projects', async () => {
+ const response = await agent.get('/api/projects');
+
+ expect(response.status).toBe(200);
+ expect(response.body.projects).toBeDefined();
+ expect(response.body.projects.length).toBe(2);
+ expect(response.body.projects.map(p => p.id)).toContain(project1.id);
+ expect(response.body.projects.map(p => p.id)).toContain(project2.id);
+ });
+
+ it('should include area information', async () => {
+ const response = await agent.get('/api/projects');
+
+ expect(response.status).toBe(200);
+ const projectWithArea = response.body.projects.find(p => p.id === project1.id);
+ expect(projectWithArea.Area).toBeDefined();
+ expect(projectWithArea.Area.name).toBe(area.name);
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/projects');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/project/:id', () => {
+ let project;
+
+ beforeEach(async () => {
+ project = await Project.create({
+ name: 'Test Project',
+ description: 'Test Description',
+ user_id: user.id,
+ area_id: area.id
+ });
+ });
+
+ it('should get project by id', async () => {
+ const response = await agent.get(`/api/project/${project.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBe(project.id);
+ expect(response.body.name).toBe(project.name);
+ expect(response.body.description).toBe(project.description);
+ });
+
+ it('should return 404 for non-existent project', async () => {
+ const response = await agent.get('/api/project/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Project not found');
+ });
+
+ it('should not allow access to other user\'s projects', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherProject = await Project.create({
+ name: 'Other Project',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.get(`/api/project/${otherProject.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Project not found');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get(`/api/project/${project.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('PATCH /api/project/:id', () => {
+ let project;
+
+ beforeEach(async () => {
+ project = await Project.create({
+ name: 'Test Project',
+ description: 'Test Description',
+ active: false,
+ priority: 0,
+ user_id: user.id
+ });
+ });
+
+ it('should update project', async () => {
+ const updateData = {
+ name: 'Updated Project',
+ description: 'Updated Description',
+ active: true,
+ priority: 2
+ };
+
+ const response = await agent
+ .patch(`/api/project/${project.id}`)
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.name).toBe(updateData.name);
+ expect(response.body.description).toBe(updateData.description);
+ expect(response.body.active).toBe(updateData.active);
+ expect(response.body.priority).toBe(updateData.priority);
+ });
+
+ it('should return 404 for non-existent project', async () => {
+ const response = await agent
+ .patch('/api/project/999999')
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Project not found.');
+ });
+
+ it('should not allow updating other user\'s projects', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherProject = await Project.create({
+ name: 'Other Project',
+ user_id: otherUser.id
+ });
+
+ const response = await agent
+ .patch(`/api/project/${otherProject.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Project not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .patch(`/api/project/${project.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('DELETE /api/project/:id', () => {
+ let project;
+
+ beforeEach(async () => {
+ project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id
+ });
+ });
+
+ it('should delete project', async () => {
+ const response = await agent.delete(`/api/project/${project.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Project successfully deleted');
+
+ // Verify project is deleted
+ const deletedProject = await Project.findByPk(project.id);
+ expect(deletedProject).toBeNull();
+ });
+
+ it('should return 404 for non-existent project', async () => {
+ const response = await agent.delete('/api/project/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Project not found.');
+ });
+
+ it('should not allow deleting other user\'s projects', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherProject = await Project.create({
+ name: 'Other Project',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.delete(`/api/project/${otherProject.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Project not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).delete(`/api/project/${project.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/quotes.test.js b/backend/tests/integration/quotes.test.js
new file mode 100644
index 0000000..57b671b
--- /dev/null
+++ b/backend/tests/integration/quotes.test.js
@@ -0,0 +1,168 @@
+const request = require('supertest');
+const app = require('../../app');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Quotes Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('GET /api/quotes/random', () => {
+ it('should return a random quote', async () => {
+ const response = await agent
+ .get('/api/quotes/random');
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('quote');
+ expect(typeof response.body.quote).toBe('string');
+ expect(response.body.quote.length).toBeGreaterThan(0);
+ });
+
+ it('should return different quotes on multiple requests', async () => {
+ const responses = await Promise.all([
+ agent.get('/api/quotes/random'),
+ agent.get('/api/quotes/random'),
+ agent.get('/api/quotes/random'),
+ agent.get('/api/quotes/random'),
+ agent.get('/api/quotes/random')
+ ]);
+
+ // All responses should be successful
+ responses.forEach(response => {
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('quote');
+ expect(typeof response.body.quote).toBe('string');
+ });
+
+ // With multiple requests, we should get at least some variety
+ // (though it's possible to get the same quote multiple times due to randomness)
+ const quotes = responses.map(r => r.body.quote);
+ const uniqueQuotes = new Set(quotes);
+
+ // We expect at least 1 unique quote, but likely more
+ expect(uniqueQuotes.size).toBeGreaterThanOrEqual(1);
+ });
+
+ it('should return valid quote structure', async () => {
+ const response = await agent
+ .get('/api/quotes/random');
+
+ expect(response.status).toBe(200);
+ expect(Object.keys(response.body)).toEqual(['quote']);
+ expect(response.body.quote).toBeTruthy();
+ expect(response.body.quote.trim()).toBe(response.body.quote);
+ });
+ });
+
+ describe('GET /api/quotes', () => {
+ it('should return all quotes with count', async () => {
+ const response = await agent
+ .get('/api/quotes');
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('quotes');
+ expect(response.body).toHaveProperty('count');
+ expect(Array.isArray(response.body.quotes)).toBe(true);
+ expect(typeof response.body.count).toBe('number');
+ expect(response.body.quotes.length).toBe(response.body.count);
+ expect(response.body.count).toBeGreaterThan(0);
+ });
+
+ it('should return valid quote array', async () => {
+ const response = await agent
+ .get('/api/quotes');
+
+ expect(response.status).toBe(200);
+
+ // All quotes should be non-empty strings
+ response.body.quotes.forEach(quote => {
+ expect(typeof quote).toBe('string');
+ expect(quote.length).toBeGreaterThan(0);
+ expect(quote.trim()).toBe(quote);
+ });
+ });
+
+ it('should return consistent data across requests', async () => {
+ const response1 = await agent.get('/api/quotes');
+ const response2 = await agent.get('/api/quotes');
+
+ expect(response1.status).toBe(200);
+ expect(response2.status).toBe(200);
+
+ // The quotes array should be the same across requests
+ expect(response1.body.quotes.length).toBe(response2.body.quotes.length);
+ expect(response1.body.count).toBe(response2.body.count);
+
+ // Verify the actual content is the same
+ expect(response1.body.quotes).toEqual(response2.body.quotes);
+ });
+
+ it('should return expected quote count', async () => {
+ const response = await agent
+ .get('/api/quotes');
+
+ expect(response.status).toBe(200);
+
+ // Based on the configuration, we expect 20 quotes, but allow for fallback quotes
+ expect(response.body.count).toBeGreaterThanOrEqual(5);
+ expect(response.body.quotes.length).toBe(response.body.count);
+ });
+
+ it('should contain productivity-focused quotes', async () => {
+ const response = await agent
+ .get('/api/quotes');
+
+ expect(response.status).toBe(200);
+
+ // Look for some productivity-related keywords in the quotes
+ const allQuotesText = response.body.quotes.join(' ').toLowerCase();
+
+ // These are common themes in productivity quotes
+ const productivityKeywords = [
+ 'progress', 'task', 'goal', 'focus', 'accomplish',
+ 'success', 'work', 'effort', 'achieve', 'time'
+ ];
+
+ // At least some quotes should contain productivity-related terms
+ const hasProductivityContent = productivityKeywords.some(keyword =>
+ allQuotesText.includes(keyword)
+ );
+
+ expect(hasProductivityContent).toBe(true);
+ });
+ });
+
+ describe('Quote randomness and consistency', () => {
+ it('should have random quotes that are part of the full quote set', async () => {
+ // Get all quotes first
+ const allQuotesResponse = await agent.get('/api/quotes');
+ const allQuotes = allQuotesResponse.body.quotes;
+
+ // Get several random quotes
+ const randomQuoteResponses = await Promise.all([
+ agent.get('/api/quotes/random'),
+ agent.get('/api/quotes/random'),
+ agent.get('/api/quotes/random')
+ ]);
+
+ // Each random quote should be from the full set
+ randomQuoteResponses.forEach(response => {
+ expect(response.status).toBe(200);
+ expect(allQuotes).toContain(response.body.quote);
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/tags.test.js b/backend/tests/integration/tags.test.js
new file mode 100644
index 0000000..be6e4e5
--- /dev/null
+++ b/backend/tests/integration/tags.test.js
@@ -0,0 +1,270 @@
+const request = require('supertest');
+const app = require('../../app');
+const { Tag, User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Tags Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/tag', () => {
+ it('should create a new tag', async () => {
+ const tagData = {
+ name: 'work'
+ };
+
+ const response = await agent
+ .post('/api/tag')
+ .send(tagData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.name).toBe(tagData.name);
+ expect(response.body.id).toBeDefined();
+ });
+
+ it('should require authentication', async () => {
+ const tagData = {
+ name: 'work'
+ };
+
+ const response = await request(app)
+ .post('/api/tag')
+ .send(tagData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require tag name', async () => {
+ const tagData = {};
+
+ const response = await agent
+ .post('/api/tag')
+ .send(tagData);
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Tag name is required');
+ });
+ });
+
+ describe('GET /api/tags', () => {
+ let tag1, tag2;
+
+ beforeEach(async () => {
+ tag1 = await Tag.create({
+ name: 'work',
+ user_id: user.id
+ });
+
+ tag2 = await Tag.create({
+ name: 'personal',
+ user_id: user.id
+ });
+ });
+
+ it('should get all user tags', async () => {
+ const response = await agent.get('/api/tags');
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveLength(2);
+ expect(response.body.map(t => t.id)).toContain(tag1.id);
+ expect(response.body.map(t => t.id)).toContain(tag2.id);
+ });
+
+ it('should order tags by name', async () => {
+ const response = await agent.get('/api/tags');
+
+ expect(response.status).toBe(200);
+ expect(response.body[0].name).toBe('personal'); // P comes before W
+ expect(response.body[1].name).toBe('work');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/tags');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/tag/:id', () => {
+ let tag;
+
+ beforeEach(async () => {
+ tag = await Tag.create({
+ name: 'work',
+ user_id: user.id
+ });
+ });
+
+ it('should get tag by id', async () => {
+ const response = await agent.get(`/api/tag/${tag.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBe(tag.id);
+ expect(response.body.name).toBe(tag.name);
+ });
+
+ it('should return 404 for non-existent tag', async () => {
+ const response = await agent.get('/api/tag/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Tag not found');
+ });
+
+ it('should not allow access to other user\'s tags', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherTag = await Tag.create({
+ name: 'other-tag',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.get(`/api/tag/${otherTag.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Tag not found');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get(`/api/tag/${tag.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('PATCH /api/tag/:id', () => {
+ let tag;
+
+ beforeEach(async () => {
+ tag = await Tag.create({
+ name: 'work',
+ user_id: user.id
+ });
+ });
+
+ it('should update tag', async () => {
+ const updateData = {
+ name: 'updated-work'
+ };
+
+ const response = await agent
+ .patch(`/api/tag/${tag.id}`)
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.name).toBe(updateData.name);
+ });
+
+ it('should return 404 for non-existent tag', async () => {
+ const response = await agent
+ .patch('/api/tag/999999')
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Tag not found');
+ });
+
+ it('should not allow updating other user\'s tags', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherTag = await Tag.create({
+ name: 'other-tag',
+ user_id: otherUser.id
+ });
+
+ const response = await agent
+ .patch(`/api/tag/${otherTag.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Tag not found');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .patch(`/api/tag/${tag.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('DELETE /api/tag/:id', () => {
+ let tag;
+
+ beforeEach(async () => {
+ tag = await Tag.create({
+ name: 'work',
+ user_id: user.id
+ });
+ });
+
+ it('should delete tag', async () => {
+ const response = await agent.delete(`/api/tag/${tag.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Tag successfully deleted');
+
+ // Verify tag is deleted
+ const deletedTag = await Tag.findByPk(tag.id);
+ expect(deletedTag).toBeNull();
+ });
+
+ it('should return 404 for non-existent tag', async () => {
+ const response = await agent.delete('/api/tag/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Tag not found');
+ });
+
+ it('should not allow deleting other user\'s tags', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherTag = await Tag.create({
+ name: 'other-tag',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.delete(`/api/tag/${otherTag.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Tag not found');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).delete(`/api/tag/${tag.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/tasks.test.js b/backend/tests/integration/tasks.test.js
new file mode 100644
index 0000000..c6026c0
--- /dev/null
+++ b/backend/tests/integration/tasks.test.js
@@ -0,0 +1,274 @@
+const request = require('supertest');
+const app = require('../../app');
+const { Task, User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Tasks Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/task', () => {
+ it('should create a new task', async () => {
+ const taskData = {
+ name: 'Test Task',
+ note: 'Test Note',
+ priority: 1,
+ status: 0
+ };
+
+ const response = await agent
+ .post('/api/task')
+ .send(taskData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.id).toBeDefined();
+ expect(response.body.name).toBe(taskData.name);
+ expect(response.body.note).toBe(taskData.note);
+ expect(response.body.priority).toBe(taskData.priority);
+ expect(response.body.status).toBe(taskData.status);
+ expect(response.body.user_id).toBe(user.id);
+ });
+
+ it('should require authentication', async () => {
+ const taskData = {
+ name: 'Test Task'
+ };
+
+ const response = await request(app)
+ .post('/api/task')
+ .send(taskData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require task name', async () => {
+ // Mock console.error to suppress expected error log in test output
+ const originalConsoleError = console.error;
+ console.error = jest.fn();
+
+ const taskData = {
+ description: 'Test Description'
+ };
+
+ const response = await agent
+ .post('/api/task')
+ .send(taskData);
+
+ expect(response.status).toBe(400);
+
+ // Restore original console.error
+ console.error = originalConsoleError;
+ });
+ });
+
+ describe('GET /api/tasks', () => {
+ let task1, task2;
+
+ beforeEach(async () => {
+ task1 = await Task.create({
+ name: 'Task 1',
+ description: 'Description 1',
+ user_id: user.id,
+ today: true
+ });
+
+ task2 = await Task.create({
+ name: 'Task 2',
+ description: 'Description 2',
+ user_id: user.id,
+ today: false
+ });
+ });
+
+ it('should get all user tasks', async () => {
+ const response = await agent.get('/api/tasks');
+
+ expect(response.status).toBe(200);
+ expect(response.body.tasks).toBeDefined();
+ expect(response.body.tasks.length).toBe(2);
+ expect(response.body.tasks.map(t => t.id)).toContain(task1.id);
+ expect(response.body.tasks.map(t => t.id)).toContain(task2.id);
+ });
+
+ it('should filter today tasks (returns all user tasks)', async () => {
+ const response = await agent.get('/api/tasks?type=today');
+
+ expect(response.status).toBe(200);
+ expect(response.body.tasks).toBeDefined();
+ expect(response.body.tasks.length).toBe(2);
+ // Both tasks should be returned as "today" doesn't filter by the today field
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/tasks');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ // Note: No individual task GET route exists in the current API
+
+ describe('PATCH /api/task/:id', () => {
+ let task;
+
+ beforeEach(async () => {
+ task = await Task.create({
+ name: 'Test Task',
+ description: 'Test Description',
+ priority: 0,
+ status: 0,
+ user_id: user.id
+ });
+ });
+
+ it('should update task', async () => {
+ const updateData = {
+ name: 'Updated Task',
+ note: 'Updated Note',
+ priority: 2,
+ status: 1
+ };
+
+ const response = await agent
+ .patch(`/api/task/${task.id}`)
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBeDefined();
+ expect(response.body.name).toBe(updateData.name);
+ expect(response.body.note).toBe(updateData.note);
+ expect(response.body.priority).toBe(updateData.priority);
+ expect(response.body.status).toBe(updateData.status);
+ });
+
+ it('should return 404 for non-existent task', async () => {
+ const response = await agent
+ .patch('/api/task/999999')
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Task not found.');
+ });
+
+ it('should not allow updating other user\'s tasks', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherTask = await Task.create({
+ name: 'Other Task',
+ user_id: otherUser.id
+ });
+
+ const response = await agent
+ .patch(`/api/task/${otherTask.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Task not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .patch(`/api/task/${task.id}`)
+ .send({ name: 'Updated' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('DELETE /api/task/:id', () => {
+ let task;
+
+ beforeEach(async () => {
+ task = await Task.create({
+ name: 'Test Task',
+ user_id: user.id
+ });
+ });
+
+ it('should delete task', async () => {
+ const response = await agent.delete(`/api/task/${task.id}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Task successfully deleted');
+
+ // Verify task is deleted
+ const deletedTask = await Task.findByPk(task.id);
+ expect(deletedTask).toBeNull();
+ });
+
+ it('should return 404 for non-existent task', async () => {
+ const response = await agent.delete('/api/task/999999');
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Task not found.');
+ });
+
+ it('should not allow deleting other user\'s tasks', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const otherTask = await Task.create({
+ name: 'Other Task',
+ user_id: otherUser.id
+ });
+
+ const response = await agent.delete(`/api/task/${otherTask.id}`);
+
+ expect(response.status).toBe(404);
+ expect(response.body.error).toBe('Task not found.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).delete(`/api/task/${task.id}`);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('Task with tags', () => {
+ it('should create task with tags', async () => {
+ const taskData = {
+ name: 'Test Task',
+ tags: [
+ { name: 'work' },
+ { name: 'urgent' }
+ ]
+ };
+
+ const response = await agent
+ .post('/api/task')
+ .send(taskData);
+
+ expect(response.status).toBe(201);
+ expect(response.body.Tags).toBeDefined();
+ expect(response.body.Tags.length).toBe(2);
+ expect(response.body.Tags.map(t => t.name)).toContain('work');
+ expect(response.body.Tags.map(t => t.name)).toContain('urgent');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/telegram.test.js b/backend/tests/integration/telegram.test.js
new file mode 100644
index 0000000..d7972e1
--- /dev/null
+++ b/backend/tests/integration/telegram.test.js
@@ -0,0 +1,152 @@
+const request = require('supertest');
+const app = require('../../app');
+const { User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Telegram Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('POST /api/telegram/setup', () => {
+ it('should setup telegram bot token', async () => {
+ const botToken = '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678';
+
+ const response = await agent
+ .post('/api/telegram/setup')
+ .send({ token: botToken });
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Telegram bot token updated successfully');
+
+ // Verify token was saved to user
+ const updatedUser = await User.findByPk(user.id);
+ expect(updatedUser.telegram_bot_token).toBe(botToken);
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .post('/api/telegram/setup')
+ .send({ token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-1234567890' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require token parameter', async () => {
+ const response = await agent
+ .post('/api/telegram/setup')
+ .send({});
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Telegram bot token is required.');
+ });
+
+ it('should validate token format', async () => {
+ const response = await agent
+ .post('/api/telegram/setup')
+ .send({ token: 'invalid-token-format' });
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Invalid Telegram bot token format.');
+ });
+
+ it('should validate token format with correct pattern', async () => {
+ // Test various invalid formats
+ const invalidTokens = [
+ '123456:short',
+ 'notnum:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
+ '123456789-ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
+ '123456789:',
+ ':ABCdefGHIjklMNOPQRSTUVwxyz-12345678'
+ ];
+
+ for (const token of invalidTokens) {
+ const response = await agent
+ .post('/api/telegram/setup')
+ .send({ token });
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Invalid Telegram bot token format.');
+ }
+ });
+
+ it('should accept valid token formats', async () => {
+ const validTokens = [
+ '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
+ '987654321:XYZabcDEFghiJKLmnoPQRstUVW_09876543',
+ '555555555:abcdefghijklmnopqrstuvwxyzABCDEFGHI'
+ ];
+
+ for (const token of validTokens) {
+ const response = await agent
+ .post('/api/telegram/setup')
+ .send({ token });
+
+ expect(response.status).toBe(200);
+ expect(response.body.message).toBe('Telegram bot token updated successfully');
+ }
+ });
+ });
+
+ describe('POST /api/telegram/start-polling', () => {
+ beforeEach(async () => {
+ // Setup bot token first
+ await user.update({
+ telegram_bot_token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678'
+ });
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .post('/api/telegram/start-polling');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require bot token to be configured', async () => {
+ // Remove bot token
+ await user.update({ telegram_bot_token: null });
+
+ const response = await agent
+ .post('/api/telegram/start-polling');
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Telegram bot token not set.');
+ });
+ });
+
+ describe('POST /api/telegram/stop-polling', () => {
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .post('/api/telegram/stop-polling');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+
+ describe('GET /api/telegram/polling-status', () => {
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .get('/api/telegram/polling-status');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/url.test.js b/backend/tests/integration/url.test.js
new file mode 100644
index 0000000..0de3ea6
--- /dev/null
+++ b/backend/tests/integration/url.test.js
@@ -0,0 +1,178 @@
+const request = require('supertest');
+const app = require('../../app');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('URL Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('GET /api/url/title', () => {
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .get('/api/url/title')
+ .query({ url: 'https://example.com' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require url parameter', async () => {
+ const response = await agent
+ .get('/api/url/title');
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('URL parameter is required');
+ });
+
+ it('should return title for valid URL', async () => {
+ const response = await agent
+ .get('/api/url/title')
+ .query({ url: 'https://httpbin.org/html' });
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('url');
+ expect(response.body).toHaveProperty('title');
+ expect(response.body.url).toBe('https://httpbin.org/html');
+ // Title could be extracted or null depending on network conditions
+ expect(typeof response.body.title === 'string' || response.body.title === null).toBe(true);
+ }, 10000);
+
+ it('should handle URL without protocol', async () => {
+ const response = await agent
+ .get('/api/url/title')
+ .query({ url: 'httpbin.org/html' });
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('url');
+ expect(response.body).toHaveProperty('title');
+ expect(response.body.url).toBe('httpbin.org/html');
+ // Title could be extracted or null depending on network conditions
+ expect(typeof response.body.title === 'string' || response.body.title === null).toBe(true);
+ }, 10000);
+
+ it('should handle invalid URL gracefully', async () => {
+ const response = await agent
+ .get('/api/url/title')
+ .query({ url: 'not-a-valid-url' });
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('url');
+ expect(response.body).toHaveProperty('title');
+ expect(response.body.url).toBe('not-a-valid-url');
+ // Title could be null or error message
+ expect(response.body.title === null || typeof response.body.title === 'string').toBe(true);
+ });
+
+ it('should handle unreachable URL', async () => {
+ const response = await agent
+ .get('/api/url/title')
+ .query({ url: 'https://nonexistent-domain-12345.com' });
+
+ expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('url');
+ expect(response.body).toHaveProperty('title');
+ expect(response.body.url).toBe('https://nonexistent-domain-12345.com');
+ expect(response.body.title).toBe(null);
+ });
+ });
+
+ describe('POST /api/url/extract-from-text', () => {
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .post('/api/url/extract-from-text')
+ .send({ text: 'Check out https://example.com' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should require text parameter', async () => {
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({});
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Text parameter is required');
+ });
+
+ it('should extract URL from text and get title', async () => {
+ const testText = 'Check out this interesting site: https://httpbin.org/html';
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({ text: testText });
+
+ expect(response.status).toBe(200);
+ expect(response.body.found).toBe(true);
+ expect(response.body.url).toBe('https://httpbin.org/html');
+ expect(response.body.originalText).toBe(testText);
+ expect(response.body).toHaveProperty('title');
+ // Title could be extracted or null depending on network conditions
+ expect(typeof response.body.title === 'string' || response.body.title === null).toBe(true);
+ }, 10000);
+
+ it('should extract first URL when multiple URLs in text', async () => {
+ const testText = 'Check out https://httpbin.org/html and also https://example.com';
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({ text: testText });
+
+ expect(response.status).toBe(200);
+ expect(response.body.found).toBe(true);
+ expect(response.body.url).toBe('https://httpbin.org/html');
+ expect(response.body.originalText).toBe(testText);
+ expect(response.body).toHaveProperty('title');
+ }, 10000);
+
+ it('should return found false for URL without protocol', async () => {
+ const testText = 'Visit httpbin.org/html for testing';
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({ text: testText });
+
+ expect(response.status).toBe(200);
+ expect(response.body.found).toBe(false);
+ });
+
+ it('should return found false when no URL in text', async () => {
+ const testText = 'This text has no URLs in it at all';
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({ text: testText });
+
+ expect(response.status).toBe(200);
+ expect(response.body.found).toBe(false);
+ });
+
+ it('should handle empty text', async () => {
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({ text: '' });
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Text parameter is required');
+ });
+
+ it('should handle text with only whitespace', async () => {
+ const response = await agent
+ .post('/api/url/extract-from-text')
+ .send({ text: ' \n\t ' });
+
+ expect(response.status).toBe(200);
+ expect(response.body.found).toBe(false);
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/integration/users.test.js b/backend/tests/integration/users.test.js
new file mode 100644
index 0000000..cec9aeb
--- /dev/null
+++ b/backend/tests/integration/users.test.js
@@ -0,0 +1,283 @@
+const request = require('supertest');
+const app = require('../../app');
+const { User } = require('../../models');
+const { createTestUser } = require('../helpers/testUtils');
+
+describe('Users Routes', () => {
+ let user, agent;
+
+ beforeEach(async () => {
+ user = await createTestUser({
+ email: 'test@example.com'
+ });
+
+ // Create authenticated agent
+ agent = request.agent(app);
+ await agent
+ .post('/api/login')
+ .send({
+ email: 'test@example.com',
+ password: 'password123'
+ });
+ });
+
+ describe('GET /api/profile', () => {
+ it('should get user profile', async () => {
+ const response = await agent.get('/api/profile');
+
+ expect(response.status).toBe(200);
+ expect(response.body.id).toBe(user.id);
+ expect(response.body.email).toBe(user.email);
+ expect(response.body).toHaveProperty('appearance');
+ expect(response.body).toHaveProperty('language');
+ expect(response.body).toHaveProperty('timezone');
+ expect(response.body).toHaveProperty('avatar_image');
+ expect(response.body).toHaveProperty('telegram_bot_token');
+ expect(response.body).toHaveProperty('telegram_chat_id');
+ expect(response.body).toHaveProperty('task_summary_enabled');
+ expect(response.body).toHaveProperty('task_summary_frequency');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/profile');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should return 401 when session user no longer exists', async () => {
+ await User.destroy({ where: { id: user.id } });
+
+ const response = await agent.get('/api/profile');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('User not found');
+ });
+ });
+
+ describe('PATCH /api/profile', () => {
+ it('should update user profile', async () => {
+ const updateData = {
+ appearance: 'dark',
+ language: 'es',
+ timezone: 'UTC',
+ avatar_image: 'new-avatar.png',
+ telegram_bot_token: 'new-token'
+ };
+
+ const response = await agent
+ .patch('/api/profile')
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.appearance).toBe(updateData.appearance);
+ expect(response.body.language).toBe(updateData.language);
+ expect(response.body.timezone).toBe(updateData.timezone);
+ expect(response.body.avatar_image).toBe(updateData.avatar_image);
+ expect(response.body.telegram_bot_token).toBe(updateData.telegram_bot_token);
+ });
+
+ it('should allow partial updates', async () => {
+ const updateData = {
+ appearance: 'dark'
+ };
+
+ const response = await agent
+ .patch('/api/profile')
+ .send(updateData);
+
+ expect(response.status).toBe(200);
+ expect(response.body.appearance).toBe(updateData.appearance);
+ expect(response.body.language).toBe(user.language);
+ });
+
+ it('should require authentication', async () => {
+ const updateData = {
+ appearance: 'dark'
+ };
+
+ const response = await request(app)
+ .patch('/api/profile')
+ .send(updateData);
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should return 401 when session user no longer exists', async () => {
+ await User.destroy({ where: { id: user.id } });
+
+ const response = await agent
+ .patch('/api/profile')
+ .send({ appearance: 'dark' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('User not found');
+ });
+ });
+
+ describe('POST /api/profile/task-summary/toggle', () => {
+ beforeEach(async () => {
+ await user.update({ task_summary_enabled: false });
+ });
+
+ it('should toggle task summary on', async () => {
+ const response = await agent.post('/api/profile/task-summary/toggle');
+
+ expect(response.status).toBe(200);
+ expect(response.body.success).toBe(true);
+ expect(response.body.enabled).toBe(true);
+ expect(response.body.message).toBe('Task summary notifications have been enabled.');
+ });
+
+ it('should toggle task summary off', async () => {
+ await user.update({ task_summary_enabled: true });
+
+ const response = await agent.post('/api/profile/task-summary/toggle');
+
+ expect(response.status).toBe(200);
+ expect(response.body.success).toBe(true);
+ expect(response.body.enabled).toBe(false);
+ expect(response.body.message).toBe('Task summary notifications have been disabled.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).post('/api/profile/task-summary/toggle');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should return 401 when session user no longer exists', async () => {
+ await User.destroy({ where: { id: user.id } });
+
+ const response = await agent.post('/api/profile/task-summary/toggle');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('User not found');
+ });
+ });
+
+ describe('POST /api/profile/task-summary/frequency', () => {
+ it('should update task summary frequency', async () => {
+ const response = await agent
+ .post('/api/profile/task-summary/frequency')
+ .send({ frequency: 'daily' });
+
+ expect(response.status).toBe(200);
+ expect(response.body.success).toBe(true);
+ expect(response.body.frequency).toBe('daily');
+ expect(response.body.message).toBe('Task summary frequency has been set to daily.');
+ });
+
+ it('should require frequency parameter', async () => {
+ const response = await agent
+ .post('/api/profile/task-summary/frequency')
+ .send({});
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Frequency is required.');
+ });
+
+ it('should validate frequency value', async () => {
+ const response = await agent
+ .post('/api/profile/task-summary/frequency')
+ .send({ frequency: 'invalid' });
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Invalid frequency value.');
+ });
+
+ it('should accept valid frequencies', async () => {
+ const validFrequencies = ['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h'];
+
+ for (const frequency of validFrequencies) {
+ const response = await agent
+ .post('/api/profile/task-summary/frequency')
+ .send({ frequency });
+
+ expect(response.status).toBe(200);
+ expect(response.body.frequency).toBe(frequency);
+ }
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app)
+ .post('/api/profile/task-summary/frequency')
+ .send({ frequency: 'daily' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should return 401 when session user no longer exists', async () => {
+ await User.destroy({ where: { id: user.id } });
+
+ const response = await agent
+ .post('/api/profile/task-summary/frequency')
+ .send({ frequency: 'daily' });
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('User not found');
+ });
+ });
+
+ describe('POST /api/profile/task-summary/send-now', () => {
+ it('should require telegram configuration', async () => {
+ const response = await agent.post('/api/profile/task-summary/send-now');
+
+ expect(response.status).toBe(400);
+ expect(response.body.error).toBe('Telegram bot is not properly configured.');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).post('/api/profile/task-summary/send-now');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should return 401 when session user no longer exists', async () => {
+ await User.destroy({ where: { id: user.id } });
+
+ const response = await agent.post('/api/profile/task-summary/send-now');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('User not found');
+ });
+ });
+
+ describe('GET /api/profile/task-summary/status', () => {
+ it('should get task summary status', async () => {
+ await user.update({
+ task_summary_enabled: true,
+ task_summary_frequency: 'daily'
+ });
+
+ const response = await agent.get('/api/profile/task-summary/status');
+
+ expect(response.status).toBe(200);
+ expect(response.body.success).toBe(true);
+ expect(response.body.enabled).toBe(true);
+ expect(response.body.frequency).toBe('daily');
+ expect(response.body).toHaveProperty('last_run');
+ expect(response.body).toHaveProperty('next_run');
+ });
+
+ it('should require authentication', async () => {
+ const response = await request(app).get('/api/profile/task-summary/status');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('Authentication required');
+ });
+
+ it('should return 401 when session user no longer exists', async () => {
+ await User.destroy({ where: { id: user.id } });
+
+ const response = await agent.get('/api/profile/task-summary/status');
+
+ expect(response.status).toBe(401);
+ expect(response.body.error).toBe('User not found');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/middleware/auth.test.js b/backend/tests/unit/middleware/auth.test.js
new file mode 100644
index 0000000..92f45eb
--- /dev/null
+++ b/backend/tests/unit/middleware/auth.test.js
@@ -0,0 +1,130 @@
+const { requireAuth } = require('../../../middleware/auth');
+const { User } = require('../../../models');
+
+describe('Auth Middleware', () => {
+ let req, res, next;
+
+ beforeEach(() => {
+ req = {
+ path: '/api/tasks',
+ session: {}
+ };
+ res = {
+ status: jest.fn().mockReturnThis(),
+ json: jest.fn()
+ };
+ next = jest.fn();
+ });
+
+ it('should skip authentication for health check', async () => {
+ req.path = '/api/health';
+
+ await requireAuth(req, res, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(res.status).not.toHaveBeenCalled();
+ });
+
+ it('should skip authentication for login route', async () => {
+ req.path = '/api/login';
+
+ await requireAuth(req, res, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(res.status).not.toHaveBeenCalled();
+ });
+
+ it('should skip authentication for current_user route', async () => {
+ req.path = '/api/current_user';
+
+ await requireAuth(req, res, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(res.status).not.toHaveBeenCalled();
+ });
+
+ it('should return 401 if no session', async () => {
+ req.session = null;
+
+ await requireAuth(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('should return 401 if no userId in session', async () => {
+ req.session = {};
+
+ await requireAuth(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('should return 401 and destroy session if user not found', async () => {
+ const bcrypt = require('bcrypt');
+ const user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ req.session = {
+ userId: user.id + 1, // Non-existent user ID
+ destroy: jest.fn()
+ };
+
+ await requireAuth(req, res, next);
+
+ expect(req.session.destroy).toHaveBeenCalled();
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(res.json).toHaveBeenCalledWith({ error: 'User not found' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('should set currentUser and call next for valid session', async () => {
+ const bcrypt = require('bcrypt');
+ const user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ req.session = {
+ userId: user.id
+ };
+
+ await requireAuth(req, res, next);
+
+ expect(req.currentUser).toBeDefined();
+ expect(req.currentUser.id).toBe(user.id);
+ expect(req.currentUser.email).toBe(user.email);
+ expect(next).toHaveBeenCalled();
+ expect(res.status).not.toHaveBeenCalled();
+ });
+
+ it('should handle database errors', async () => {
+ // Mock console.error to suppress expected error log in test output
+ const originalConsoleError = console.error;
+ console.error = jest.fn();
+
+ // Mock User.findByPk to throw an error
+ const originalFindByPk = User.findByPk;
+ User.findByPk = jest.fn().mockRejectedValue(new Error('Database connection error'));
+
+ req.session = {
+ userId: 123,
+ destroy: jest.fn()
+ };
+
+ await requireAuth(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(500);
+ expect(res.json).toHaveBeenCalledWith({ error: 'Authentication error' });
+ expect(next).not.toHaveBeenCalled();
+
+ // Restore original methods
+ User.findByPk = originalFindByPk;
+ console.error = originalConsoleError;
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/area.test.js b/backend/tests/unit/models/area.test.js
new file mode 100644
index 0000000..f4eb012
--- /dev/null
+++ b/backend/tests/unit/models/area.test.js
@@ -0,0 +1,74 @@
+const { Area, User } = require('../../../models');
+
+describe('Area Model', () => {
+ let user;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+ });
+
+ describe('validation', () => {
+ it('should create an area with valid data', async () => {
+ const areaData = {
+ name: 'Work',
+ description: 'Work related projects',
+ user_id: user.id
+ };
+
+ const area = await Area.create(areaData);
+
+ expect(area.name).toBe(areaData.name);
+ expect(area.description).toBe(areaData.description);
+ expect(area.user_id).toBe(user.id);
+ });
+
+ it('should require name', async () => {
+ const areaData = {
+ description: 'Area without name',
+ user_id: user.id
+ };
+
+ await expect(Area.create(areaData)).rejects.toThrow();
+ });
+
+ it('should require user_id', async () => {
+ const areaData = {
+ name: 'Test Area'
+ };
+
+ await expect(Area.create(areaData)).rejects.toThrow();
+ });
+
+ it('should allow null description', async () => {
+ const areaData = {
+ name: 'Test Area',
+ user_id: user.id,
+ description: null
+ };
+
+ const area = await Area.create(areaData);
+ expect(area.description).toBeNull();
+ });
+ });
+
+ describe('associations', () => {
+ it('should belong to a user', async () => {
+ const area = await Area.create({
+ name: 'Test Area',
+ user_id: user.id
+ });
+
+ const areaWithUser = await Area.findByPk(area.id, {
+ include: [{ model: User }]
+ });
+
+ expect(areaWithUser.User).toBeDefined();
+ expect(areaWithUser.User.id).toBe(user.id);
+ expect(areaWithUser.User.email).toBe(user.email);
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/inbox_item.test.js b/backend/tests/unit/models/inbox_item.test.js
new file mode 100644
index 0000000..fa707b6
--- /dev/null
+++ b/backend/tests/unit/models/inbox_item.test.js
@@ -0,0 +1,96 @@
+const { InboxItem, User } = require('../../../models');
+
+describe('InboxItem Model', () => {
+ let user;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+ });
+
+ describe('validation', () => {
+ it('should create an inbox item with valid data', async () => {
+ const inboxData = {
+ content: 'Remember to buy groceries',
+ status: 'added',
+ source: 'web',
+ user_id: user.id
+ };
+
+ const inboxItem = await InboxItem.create(inboxData);
+
+ expect(inboxItem.content).toBe(inboxData.content);
+ expect(inboxItem.status).toBe(inboxData.status);
+ expect(inboxItem.source).toBe(inboxData.source);
+ expect(inboxItem.user_id).toBe(user.id);
+ });
+
+ it('should require content', async () => {
+ const inboxData = {
+ user_id: user.id
+ };
+
+ await expect(InboxItem.create(inboxData)).rejects.toThrow();
+ });
+
+ it('should require user_id', async () => {
+ const inboxData = {
+ content: 'Test content'
+ };
+
+ await expect(InboxItem.create(inboxData)).rejects.toThrow();
+ });
+
+ it('should require status', async () => {
+ const inboxData = {
+ content: 'Test content',
+ user_id: user.id,
+ status: null
+ };
+
+ await expect(InboxItem.create(inboxData)).rejects.toThrow();
+ });
+
+ it('should require source', async () => {
+ const inboxData = {
+ content: 'Test content',
+ user_id: user.id,
+ source: null
+ };
+
+ await expect(InboxItem.create(inboxData)).rejects.toThrow();
+ });
+ });
+
+ describe('default values', () => {
+ it('should set correct default values', async () => {
+ const inboxItem = await InboxItem.create({
+ content: 'Test content',
+ user_id: user.id
+ });
+
+ expect(inboxItem.status).toBe('added');
+ expect(inboxItem.source).toBe('tududi');
+ });
+ });
+
+ describe('associations', () => {
+ it('should belong to a user', async () => {
+ const inboxItem = await InboxItem.create({
+ content: 'Test content',
+ user_id: user.id
+ });
+
+ const inboxItemWithUser = await InboxItem.findByPk(inboxItem.id, {
+ include: [{ model: User }]
+ });
+
+ expect(inboxItemWithUser.User).toBeDefined();
+ expect(inboxItemWithUser.User.id).toBe(user.id);
+ expect(inboxItemWithUser.User.email).toBe(user.email);
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/note.test.js b/backend/tests/unit/models/note.test.js
new file mode 100644
index 0000000..bb08555
--- /dev/null
+++ b/backend/tests/unit/models/note.test.js
@@ -0,0 +1,102 @@
+const { Note, User, Project } = require('../../../models');
+
+describe('Note Model', () => {
+ let user, project;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id
+ });
+ });
+
+ describe('validation', () => {
+ it('should create a note with valid data', async () => {
+ const noteData = {
+ title: 'Test Note',
+ content: 'This is a test note content',
+ user_id: user.id,
+ project_id: project.id
+ };
+
+ const note = await Note.create(noteData);
+
+ expect(note.title).toBe(noteData.title);
+ expect(note.content).toBe(noteData.content);
+ expect(note.user_id).toBe(user.id);
+ expect(note.project_id).toBe(project.id);
+ });
+
+ it('should require user_id', async () => {
+ const noteData = {
+ title: 'Test Note',
+ content: 'Test content'
+ };
+
+ await expect(Note.create(noteData)).rejects.toThrow();
+ });
+
+ it('should allow title and content to be null', async () => {
+ const noteData = {
+ title: null,
+ content: null,
+ user_id: user.id
+ };
+
+ const note = await Note.create(noteData);
+ expect(note.title).toBeNull();
+ expect(note.content).toBeNull();
+ });
+
+ it('should allow project_id to be null', async () => {
+ const noteData = {
+ title: 'Test Note',
+ content: 'Test content',
+ user_id: user.id,
+ project_id: null
+ };
+
+ const note = await Note.create(noteData);
+ expect(note.project_id).toBeNull();
+ });
+ });
+
+ describe('associations', () => {
+ it('should belong to a user', async () => {
+ const note = await Note.create({
+ title: 'Test Note',
+ user_id: user.id
+ });
+
+ const noteWithUser = await Note.findByPk(note.id, {
+ include: [{ model: User }]
+ });
+
+ expect(noteWithUser.User).toBeDefined();
+ expect(noteWithUser.User.id).toBe(user.id);
+ expect(noteWithUser.User.email).toBe(user.email);
+ });
+
+ it('should belong to a project', async () => {
+ const note = await Note.create({
+ title: 'Test Note',
+ user_id: user.id,
+ project_id: project.id
+ });
+
+ const noteWithProject = await Note.findByPk(note.id, {
+ include: [{ model: Project }]
+ });
+
+ expect(noteWithProject.Project).toBeDefined();
+ expect(noteWithProject.Project.id).toBe(project.id);
+ expect(noteWithProject.Project.name).toBe(project.name);
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/project.test.js b/backend/tests/unit/models/project.test.js
new file mode 100644
index 0000000..55af80d
--- /dev/null
+++ b/backend/tests/unit/models/project.test.js
@@ -0,0 +1,141 @@
+const { Project, User, Area } = require('../../../models');
+
+describe('Project Model', () => {
+ let user, area;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ area = await Area.create({
+ name: 'Work',
+ user_id: user.id
+ });
+ });
+
+ describe('validation', () => {
+ it('should create a project with valid data', async () => {
+ const projectData = {
+ name: 'Test Project',
+ description: 'Test Description',
+ active: true,
+ pin_to_sidebar: false,
+ priority: 1,
+ user_id: user.id,
+ area_id: area.id
+ };
+
+ const project = await Project.create(projectData);
+
+ expect(project.name).toBe(projectData.name);
+ expect(project.description).toBe(projectData.description);
+ expect(project.active).toBe(projectData.active);
+ expect(project.pin_to_sidebar).toBe(projectData.pin_to_sidebar);
+ expect(project.priority).toBe(projectData.priority);
+ expect(project.user_id).toBe(user.id);
+ expect(project.area_id).toBe(area.id);
+ });
+
+ it('should require name', async () => {
+ const projectData = {
+ description: 'Project without name',
+ user_id: user.id
+ };
+
+ await expect(Project.create(projectData)).rejects.toThrow();
+ });
+
+ it('should require user_id', async () => {
+ const projectData = {
+ name: 'Test Project'
+ };
+
+ await expect(Project.create(projectData)).rejects.toThrow();
+ });
+
+ it('should validate priority range', async () => {
+ const projectData = {
+ name: 'Test Project',
+ user_id: user.id,
+ priority: 5
+ };
+
+ await expect(Project.create(projectData)).rejects.toThrow();
+ });
+
+ it('should allow valid priority values', async () => {
+ for (let priority of [0, 1, 2]) {
+ const project = await Project.create({
+ name: `Test Project ${priority}`,
+ user_id: user.id,
+ priority: priority
+ });
+ expect(project.priority).toBe(priority);
+ }
+ });
+ });
+
+ describe('default values', () => {
+ it('should set correct default values', async () => {
+ const project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id
+ });
+
+ expect(project.active).toBe(false);
+ expect(project.pin_to_sidebar).toBe(false);
+ });
+ });
+
+ describe('optional fields', () => {
+ it('should allow optional fields to be null', async () => {
+ const project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id,
+ description: null,
+ priority: null,
+ due_date_at: null,
+ area_id: null
+ });
+
+ expect(project.description).toBeNull();
+ expect(project.priority).toBeNull();
+ expect(project.due_date_at).toBeNull();
+ expect(project.area_id).toBeNull();
+ });
+ });
+
+ describe('associations', () => {
+ it('should belong to a user', async () => {
+ const project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id
+ });
+
+ const projectWithUser = await Project.findByPk(project.id, {
+ include: [{ model: User }]
+ });
+
+ expect(projectWithUser.User).toBeDefined();
+ expect(projectWithUser.User.id).toBe(user.id);
+ });
+
+ it('should belong to an area', async () => {
+ const project = await Project.create({
+ name: 'Test Project',
+ user_id: user.id,
+ area_id: area.id
+ });
+
+ const projectWithArea = await Project.findByPk(project.id, {
+ include: [{ model: Area }]
+ });
+
+ expect(projectWithArea.Area).toBeDefined();
+ expect(projectWithArea.Area.id).toBe(area.id);
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/tag.test.js b/backend/tests/unit/models/tag.test.js
new file mode 100644
index 0000000..9a334f3
--- /dev/null
+++ b/backend/tests/unit/models/tag.test.js
@@ -0,0 +1,83 @@
+const { Tag, User } = require('../../../models');
+
+describe('Tag Model', () => {
+ let user;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+ });
+
+ describe('validation', () => {
+ it('should create a tag with valid data', async () => {
+ const tagData = {
+ name: 'work',
+ user_id: user.id
+ };
+
+ const tag = await Tag.create(tagData);
+
+ expect(tag.name).toBe(tagData.name);
+ expect(tag.user_id).toBe(user.id);
+ });
+
+ it('should require name', async () => {
+ const tagData = {
+ user_id: user.id
+ };
+
+ await expect(Tag.create(tagData)).rejects.toThrow();
+ });
+
+ it('should require user_id', async () => {
+ const tagData = {
+ name: 'work'
+ };
+
+ await expect(Tag.create(tagData)).rejects.toThrow();
+ });
+
+ it('should allow multiple tags with same name for different users', async () => {
+ const bcrypt = require('bcrypt');
+ const otherUser = await User.create({
+ email: 'other@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ const tag1 = await Tag.create({
+ name: 'work',
+ user_id: user.id
+ });
+
+ const tag2 = await Tag.create({
+ name: 'work',
+ user_id: otherUser.id
+ });
+
+ expect(tag1.name).toBe('work');
+ expect(tag2.name).toBe('work');
+ expect(tag1.user_id).toBe(user.id);
+ expect(tag2.user_id).toBe(otherUser.id);
+ });
+ });
+
+ describe('associations', () => {
+ it('should belong to a user', async () => {
+ const tag = await Tag.create({
+ name: 'work',
+ user_id: user.id
+ });
+
+ const tagWithUser = await Tag.findByPk(tag.id, {
+ include: [{ model: User }]
+ });
+
+ expect(tagWithUser.User).toBeDefined();
+ expect(tagWithUser.User.id).toBe(user.id);
+ expect(tagWithUser.User.email).toBe(user.email);
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/task.test.js b/backend/tests/unit/models/task.test.js
new file mode 100644
index 0000000..d8a9ffb
--- /dev/null
+++ b/backend/tests/unit/models/task.test.js
@@ -0,0 +1,183 @@
+const { Task, User } = require('../../../models');
+
+describe('Task Model', () => {
+ let user;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+ });
+
+ describe('validation', () => {
+ it('should create a task with valid data', async () => {
+ const taskData = {
+ name: 'Test Task',
+ description: 'Test Description',
+ user_id: user.id
+ };
+
+ const task = await Task.create(taskData);
+
+ expect(task.name).toBe(taskData.name);
+ expect(task.description).toBe(taskData.description);
+ expect(task.user_id).toBe(user.id);
+ expect(task.today).toBe(false);
+ expect(task.priority).toBe(0);
+ expect(task.status).toBe(0);
+ expect(task.recurrence_type).toBe('none');
+ });
+
+ it('should require name', async () => {
+ const taskData = {
+ user_id: user.id
+ };
+
+ await expect(Task.create(taskData)).rejects.toThrow();
+ });
+
+ it('should require user_id', async () => {
+ const taskData = {
+ name: 'Test Task'
+ };
+
+ await expect(Task.create(taskData)).rejects.toThrow();
+ });
+
+ it('should validate priority range', async () => {
+ const taskData = {
+ name: 'Test Task',
+ user_id: user.id,
+ priority: 5
+ };
+
+ await expect(Task.create(taskData)).rejects.toThrow();
+ });
+
+ it('should validate status range', async () => {
+ const taskData = {
+ name: 'Test Task',
+ user_id: user.id,
+ status: 10
+ };
+
+ await expect(Task.create(taskData)).rejects.toThrow();
+ });
+ });
+
+ describe('constants', () => {
+ it('should have correct priority constants', () => {
+ expect(Task.PRIORITY.LOW).toBe(0);
+ expect(Task.PRIORITY.MEDIUM).toBe(1);
+ expect(Task.PRIORITY.HIGH).toBe(2);
+ });
+
+ it('should have correct status constants', () => {
+ expect(Task.STATUS.NOT_STARTED).toBe(0);
+ expect(Task.STATUS.IN_PROGRESS).toBe(1);
+ expect(Task.STATUS.DONE).toBe(2);
+ expect(Task.STATUS.ARCHIVED).toBe(3);
+ expect(Task.STATUS.WAITING).toBe(4);
+ });
+ });
+
+ describe('instance methods', () => {
+ let task;
+
+ beforeEach(async () => {
+ task = await Task.create({
+ name: 'Test Task',
+ user_id: user.id
+ });
+ });
+
+ it('should return correct priority name', async () => {
+ task.priority = Task.PRIORITY.LOW;
+ expect(task.getPriorityName()).toBe('low');
+
+ task.priority = Task.PRIORITY.MEDIUM;
+ expect(task.getPriorityName()).toBe('medium');
+
+ task.priority = Task.PRIORITY.HIGH;
+ expect(task.getPriorityName()).toBe('high');
+ });
+
+ it('should return correct status name', async () => {
+ task.status = Task.STATUS.NOT_STARTED;
+ expect(task.getStatusName()).toBe('not_started');
+
+ task.status = Task.STATUS.IN_PROGRESS;
+ expect(task.getStatusName()).toBe('in_progress');
+
+ task.status = Task.STATUS.DONE;
+ expect(task.getStatusName()).toBe('done');
+
+ task.status = Task.STATUS.ARCHIVED;
+ expect(task.getStatusName()).toBe('archived');
+
+ task.status = Task.STATUS.WAITING;
+ expect(task.getStatusName()).toBe('waiting');
+ });
+ });
+
+ describe('default values', () => {
+ it('should set correct default values', async () => {
+ const task = await Task.create({
+ name: 'Test Task',
+ user_id: user.id
+ });
+
+ expect(task.today).toBe(false);
+ expect(task.priority).toBe(0);
+ expect(task.status).toBe(0);
+ expect(task.recurrence_type).toBe('none');
+ });
+ });
+
+ describe('optional fields', () => {
+ it('should allow optional fields to be null', async () => {
+ const task = await Task.create({
+ name: 'Test Task',
+ user_id: user.id,
+ description: null,
+ due_date: null,
+ note: null,
+ recurrence_interval: null,
+ recurrence_end_date: null,
+ last_generated_date: null,
+ project_id: null
+ });
+
+ expect(task.description).toBeNull();
+ expect(task.due_date).toBeNull();
+ expect(task.note).toBeNull();
+ expect(task.recurrence_interval).toBeNull();
+ expect(task.recurrence_end_date).toBeNull();
+ expect(task.last_generated_date).toBeNull();
+ expect(task.project_id).toBeNull();
+ });
+
+ it('should accept optional field values', async () => {
+ const dueDate = new Date();
+ const task = await Task.create({
+ name: 'Test Task',
+ description: 'Test Description',
+ due_date: dueDate,
+ today: true,
+ priority: Task.PRIORITY.HIGH,
+ status: Task.STATUS.IN_PROGRESS,
+ note: 'Test Note',
+ user_id: user.id
+ });
+
+ expect(task.description).toBe('Test Description');
+ expect(task.due_date).toEqual(dueDate);
+ expect(task.today).toBe(true);
+ expect(task.priority).toBe(Task.PRIORITY.HIGH);
+ expect(task.status).toBe(Task.STATUS.IN_PROGRESS);
+ expect(task.note).toBe('Test Note');
+ });
+ });
+});
\ No newline at end of file
diff --git a/backend/tests/unit/models/user.test.js b/backend/tests/unit/models/user.test.js
new file mode 100644
index 0000000..0ea4520
--- /dev/null
+++ b/backend/tests/unit/models/user.test.js
@@ -0,0 +1,135 @@
+const { User } = require('../../../models');
+
+describe('User Model', () => {
+ describe('validation', () => {
+ it('should create a user with valid data', async () => {
+ const bcrypt = require('bcrypt');
+ const userData = {
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ };
+
+ const user = await User.create(userData);
+
+ expect(user.email).toBe(userData.email);
+ expect(user.password_digest).toBeDefined();
+ expect(user.password_digest).toBe(userData.password_digest);
+ expect(user.appearance).toBe('light');
+ expect(user.language).toBe('en');
+ expect(user.timezone).toBe('UTC');
+ });
+
+ it('should require email', async () => {
+ const userData = {
+ password: 'password123'
+ };
+
+ await expect(User.create(userData)).rejects.toThrow();
+ });
+
+ it('should require valid email format', async () => {
+ const userData = {
+ email: 'invalid-email',
+ password: 'password123'
+ };
+
+ await expect(User.create(userData)).rejects.toThrow();
+ });
+
+ it('should require unique email', async () => {
+ const bcrypt = require('bcrypt');
+ const userData = {
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ };
+
+ await User.create(userData);
+ await expect(User.create(userData)).rejects.toThrow();
+ });
+
+ it('should validate appearance values', async () => {
+ const userData = {
+ email: 'test@example.com',
+ password: 'password123',
+ appearance: 'invalid'
+ };
+
+ await expect(User.create(userData)).rejects.toThrow();
+ });
+
+ it('should validate task_summary_frequency values', async () => {
+ const userData = {
+ email: 'test@example.com',
+ password: 'password123',
+ task_summary_frequency: 'invalid'
+ };
+
+ await expect(User.create(userData)).rejects.toThrow();
+ });
+ });
+
+ describe('password methods', () => {
+ let user;
+
+ beforeEach(async () => {
+ const bcrypt = require('bcrypt');
+ user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+ });
+
+ it('should hash password on creation', async () => {
+ expect(user.password_digest).toBeDefined();
+ expect(user.password_digest).not.toBe('password123');
+ });
+
+ it('should check password correctly', async () => {
+ const isValid = await user.checkPassword('password123');
+ expect(isValid).toBe(true);
+
+ const isInvalid = await user.checkPassword('wrongpassword');
+ expect(isInvalid).toBe(false);
+ });
+
+ it('should set new password using setPassword method', async () => {
+ const oldPasswordDigest = user.password_digest;
+ await user.setPassword('newpassword');
+
+ expect(user.password_digest).not.toBe(oldPasswordDigest);
+
+ const isValidNew = await user.checkPassword('newpassword');
+ expect(isValidNew).toBe(true);
+
+ const isValidOld = await user.checkPassword('password123');
+ expect(isValidOld).toBe(false);
+ });
+
+ it('should hash password on update', async () => {
+ const oldPasswordDigest = user.password_digest;
+ user.password = 'newpassword';
+ await user.save();
+
+ expect(user.password_digest).not.toBe(oldPasswordDigest);
+
+ const isValidNew = await user.checkPassword('newpassword');
+ expect(isValidNew).toBe(true);
+ });
+ });
+
+ describe('default values', () => {
+ it('should set correct default values', async () => {
+ const bcrypt = require('bcrypt');
+ const user = await User.create({
+ email: 'test@example.com',
+ password_digest: await bcrypt.hash('password123', 10)
+ });
+
+ expect(user.appearance).toBe('light');
+ expect(user.language).toBe('en');
+ expect(user.timezone).toBe('UTC');
+ expect(user.task_summary_enabled).toBe(false);
+ expect(user.task_summary_frequency).toBe('daily');
+ });
+ });
+});
\ No newline at end of file
diff --git a/config.ru b/config.ru
deleted file mode 100644
index 785bf5c..0000000
--- a/config.ru
+++ /dev/null
@@ -1,2 +0,0 @@
-require './app'
-run Sinatra::Application
\ No newline at end of file
diff --git a/config/initializers/scheduler.rb b/config/initializers/scheduler.rb
deleted file mode 100644
index f37bcaa..0000000
--- a/config/initializers/scheduler.rb
+++ /dev/null
@@ -1,184 +0,0 @@
-# config/initializers/scheduler.rb
-require 'rufus-scheduler'
-require_relative '../../app/services/task_summary_service'
-
-# Helper method to update user's summary tracking fields
-def update_summary_tracking(user, next_time)
- user.update(
- task_summary_last_run: Time.now,
- task_summary_next_run: next_time
- )
-end
-
-# Don't schedule in test environment or when reloading in development
-if ENV['RACK_ENV'] != 'test' && ENV['DISABLE_SCHEDULER'] != 'true'
- scheduler = Rufus::Scheduler.singleton
-
- # Daily schedule at 7 AM (for users with daily frequency)
- daily_job = scheduler.cron '0 7 * * *' do
- puts "Running scheduled task: Daily task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: 'daily')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- # Calculate next run time - tomorrow at 7 AM
- next_run = Time.now.tomorrow.change(hour: 7, min: 0, sec: 0)
- update_summary_tracking(user, next_run)
- puts "Sent daily summary to user #{user.id}"
- rescue => e
- puts "Error sending daily summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Weekdays schedule at 7 AM (Monday through Friday)
- weekday_job = scheduler.cron '0 7 * * 1-5' do
- puts "Running scheduled task: Weekday task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: 'weekdays')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- # Calculate next run time - next weekday at 7 AM
- current_day = Time.now.wday
- days_until_next_weekday = current_day == 5 ? 3 : 1 # If Friday, next is Monday (+3 days), otherwise next day
- next_run = Time.now.advance(days: days_until_next_weekday).change(hour: 7, min: 0, sec: 0)
- update_summary_tracking(user, next_run)
- puts "Sent weekday summary to user #{user.id}"
- rescue => e
- puts "Error sending weekday summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Weekly schedule at 7 AM on Monday
- weekly_job = scheduler.cron '0 7 * * 1' do
- puts "Running scheduled task: Weekly task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: 'weekly')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- # Calculate next run time - next Monday at 7 AM
- next_run = Time.now.advance(days: 7).change(hour: 7, min: 0, sec: 0)
- update_summary_tracking(user, next_run)
- puts "Sent weekly summary to user #{user.id}"
- rescue => e
- puts "Error sending weekly summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Hourly schedules for different intervals
-
- # Every 1 hour
- hourly_job = scheduler.every '1h' do
- puts "Running scheduled task: Hourly (1h) task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: '1h')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- next_run = Time.now + 1.hour
- update_summary_tracking(user, next_run)
- puts "Sent hourly summary to user #{user.id}"
- rescue => e
- puts "Error sending hourly summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Every 2 hours
- two_hourly_job = scheduler.every '2h' do
- puts "Running scheduled task: 2-hour task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: '2h')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- next_run = Time.now + 2.hours
- update_summary_tracking(user, next_run)
- puts "Sent 2-hour summary to user #{user.id}"
- rescue => e
- puts "Error sending 2-hour summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Every 4 hours
- four_hourly_job = scheduler.every '4h' do
- puts "Running scheduled task: 4-hour task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: '4h')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- next_run = Time.now + 4.hours
- update_summary_tracking(user, next_run)
- puts "Sent 4-hour summary to user #{user.id}"
- rescue => e
- puts "Error sending 4-hour summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Every 8 hours
- eight_hourly_job = scheduler.every '8h' do
- puts "Running scheduled task: 8-hour task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: '8h')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- next_run = Time.now + 8.hours
- update_summary_tracking(user, next_run)
- puts "Sent 8-hour summary to user #{user.id}"
- rescue => e
- puts "Error sending 8-hour summary to user #{user.id}: #{e.message}"
- end
- end
- end
-
- # Every 12 hours
- twelve_hourly_job = scheduler.every '12h' do
- puts "Running scheduled task: 12-hour task summary"
-
- User.where.not(telegram_bot_token: [nil, ''])
- .where.not(telegram_chat_id: [nil, ''])
- .where(task_summary_enabled: true)
- .where(task_summary_frequency: '12h')
- .each do |user|
- begin
- TaskSummaryService.send_summary_to_user(user.id)
- next_run = Time.now + 12.hours
- update_summary_tracking(user, next_run)
- puts "Sent 12-hour summary to user #{user.id}"
- rescue => e
- puts "Error sending 12-hour summary to user #{user.id}: #{e.message}"
- end
- end
- end
-end
-
diff --git a/config/initializers/telegram_initializer.rb b/config/initializers/telegram_initializer.rb
deleted file mode 100644
index 3953812..0000000
--- a/config/initializers/telegram_initializer.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env ruby
-# config/initializers/telegram_initializer.rb
-require_relative '../../app/routes/telegram_poller'
-require_relative '../../app/models/user'
-
-# Create a method to be called after database connection is established
-def initialize_telegram_polling
- if ENV['RACK_ENV'] != 'test' && ENV['DISABLE_TELEGRAM'] != 'true'
- puts "Initializing Telegram polling for configured users..."
-
- # Get singleton instance of the poller
- poller = TelegramPoller.instance
-
- # Make sure we have a database connection
- begin
- ActiveRecord::Base.connection_pool.with_connection do |connection|
- # Check if the users table exists
- if connection.table_exists?('users')
- begin
- # Find users with configured Telegram tokens
- users_with_telegram = User.where.not(telegram_bot_token: [nil, ''])
-
- if users_with_telegram.any?
- puts "Found #{users_with_telegram.count} users with Telegram configuration"
-
- # Add each user to the polling list
- users_with_telegram.each do |user|
- puts "Starting Telegram polling for user #{user.id}"
- poller.add_user(user)
- end
-
- puts "Telegram polling initialized successfully"
- else
- puts "No users with Telegram configuration found"
- end
- rescue => e
- puts "Error initializing Telegram polling: #{e.message}"
- puts e.backtrace.join("\n")
- end
- else
- puts "Users table doesn't exist yet, skipping Telegram initialization"
- end
- end
- rescue => e
- puts "Database connection not available for Telegram initialization: #{e.message}"
- puts "Telegram polling will be initialized later when the database is available."
- end
- end
-end
-
-# Don't run the initializer here - we'll hook it into the Sinatra app after ActiveRecord is initialized
\ No newline at end of file
diff --git a/console.rb b/console.rb
deleted file mode 100644
index 60e67a3..0000000
--- a/console.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'irb'
-require './app'
-
-IRB.start
diff --git a/cookies.txt b/cookies.txt
deleted file mode 100644
index f894453..0000000
--- a/cookies.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-# Netscape HTTP Cookie File
-# https://curl.se/docs/http-cookies.html
-# This file was generated by libcurl! Edit at your own risk.
-
-#HttpOnly_localhost FALSE / FALSE 1752327280 rack.session U6U152aP%2F1FvgHP%2BTqOI6ZFNAriX8YWVIlXFp6FFPuOc%2FBgNnbDCJdfBfkC6p6gVikrYfiVd17qZenOEAlYZFpDlg0nzPJZqCYxgc9bhu4o3QOdUrEEYLu5ryVLUxh7DdLd7OVrMt2yHk3wixuky0icIpTw5%2Bc4dxeh4Vt0cBvy4fQasw0FZfvAjbyaYCAlE7dqE5WZ5o1dT5xEbwCEH3JO14oPmta2xf%2Bx3PJjyvZCLh3Ipxm8%2F6qlCLUC0HNQa6NgRL7ak71bDj7e7NLQ%3D--rOzyGipe0E8%2BPtPC--lRaDcnZWXd1CIpbvnCLxJA%3D%3D
diff --git a/create_migration.sh b/create_migration.sh
deleted file mode 100755
index acebe29..0000000
--- a/create_migration.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /bin/bash
-
-bundle exec ../bin/rake db:create_migration "$1"
\ No newline at end of file
diff --git a/db/migrate/20231107102451_create_users.rb b/db/migrate/20231107102451_create_users.rb
deleted file mode 100644
index 9311a4b..0000000
--- a/db/migrate/20231107102451_create_users.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CreateUsers < ActiveRecord::Migration[7.1]
- def change
- create_table :users do |t|
- t.string :name
- t.string :email
- t.string :password_digest
-
- t.timestamps null: false
- end
- end
-end
diff --git a/db/migrate/20231107102516_create_areas.rb b/db/migrate/20231107102516_create_areas.rb
deleted file mode 100644
index 8bf364d..0000000
--- a/db/migrate/20231107102516_create_areas.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class CreateAreas < ActiveRecord::Migration[7.1]
- def change
- create_table :areas do |t|
- t.string :name
- t.references :user, null: false, foreign_key: true
-
- t.timestamps null: false
- end
- end
-end
diff --git a/db/migrate/20231107102609_create_projects.rb b/db/migrate/20231107102609_create_projects.rb
deleted file mode 100644
index 8d99e5e..0000000
--- a/db/migrate/20231107102609_create_projects.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CreateProjects < ActiveRecord::Migration[7.1]
- def change
- create_table :projects do |t|
- t.string :name
- t.references :user, null: false, foreign_key: true
- t.references :area, foreign_key: true
-
- t.timestamps null: false
- end
- end
-end
diff --git a/db/migrate/20231107102631_create_tasks.rb b/db/migrate/20231107102631_create_tasks.rb
deleted file mode 100644
index fe46d60..0000000
--- a/db/migrate/20231107102631_create_tasks.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateTasks < ActiveRecord::Migration[7.1]
- def change
- create_table :tasks do |t|
- t.string :name
- t.string :priority
- t.datetime :due_date
- t.references :user, null: false, foreign_key: true
- t.references :project, foreign_key: true
-
- t.timestamps null: false
- end
- end
-end
diff --git a/db/migrate/20231109055429_add_fields_to_tasks.rb b/db/migrate/20231109055429_add_fields_to_tasks.rb
deleted file mode 100644
index 9878868..0000000
--- a/db/migrate/20231109055429_add_fields_to_tasks.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class AddFieldsToTasks < ActiveRecord::Migration[7.1]
- def change
- add_column :tasks, :today, :boolean, default: false
- add_column :tasks, :description, :text
- add_column :tasks, :completed, :boolean, default: false
- end
-end
diff --git a/db/migrate/20231109055533_add_fields_to_projects.rb b/db/migrate/20231109055533_add_fields_to_projects.rb
deleted file mode 100644
index d940a86..0000000
--- a/db/migrate/20231109055533_add_fields_to_projects.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddFieldsToProjects < ActiveRecord::Migration[7.1]
- def change
- add_column :projects, :description, :text
- end
-end
diff --git a/db/migrate/20231110163101_add_cascade_delete_to_projects_and_tasks.rb b/db/migrate/20231110163101_add_cascade_delete_to_projects_and_tasks.rb
deleted file mode 100644
index 822d9a7..0000000
--- a/db/migrate/20231110163101_add_cascade_delete_to_projects_and_tasks.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class AddCascadeDeleteToProjectsAndTasks < ActiveRecord::Migration[7.1]
- def change
- remove_foreign_key :projects, :areas
- add_foreign_key :projects, :areas, on_delete: :cascade
-
- remove_foreign_key :tasks, :projects
- add_foreign_key :tasks, :projects, on_delete: :cascade
- end
-end
diff --git a/db/migrate/20231114203847_add_tags.rb b/db/migrate/20231114203847_add_tags.rb
deleted file mode 100644
index a6401a7..0000000
--- a/db/migrate/20231114203847_add_tags.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class AddTags < ActiveRecord::Migration[7.1]
- def change
- create_table :tags do |t|
- t.string :name
- t.references :user, null: false, foreign_key: { on_delete: :cascade }
-
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20231114210336_create_tasks_tags.rb b/db/migrate/20231114210336_create_tasks_tags.rb
deleted file mode 100644
index 12b6d4a..0000000
--- a/db/migrate/20231114210336_create_tasks_tags.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class CreateTasksTags < ActiveRecord::Migration[7.1]
- def change
- create_table :tasks_tags, id: false do |t|
- t.belongs_to :task, null: false, foreign_key: true
- t.belongs_to :tag, null: false, foreign_key: true
- end
- end
-end
diff --git a/db/migrate/20231115092055_rename_tasks_tags_to_tags_tasks.rb b/db/migrate/20231115092055_rename_tasks_tags_to_tags_tasks.rb
deleted file mode 100644
index 4749cd2..0000000
--- a/db/migrate/20231115092055_rename_tasks_tags_to_tags_tasks.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class RenameTasksTagsToTagsTasks < ActiveRecord::Migration[7.1]
- def change
- rename_table :tasks_tags, :tags_tasks
- end
-end
diff --git a/db/migrate/20231116112552_create_notes.rb b/db/migrate/20231116112552_create_notes.rb
deleted file mode 100644
index c127ea6..0000000
--- a/db/migrate/20231116112552_create_notes.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class CreateNotes < ActiveRecord::Migration[7.1]
- def change
- create_table :notes do |t|
- t.text :content
- t.references :user, null: false, foreign_key: { on_delete: :cascade }
-
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20231116120633_create_join_table_notes_tags.rb b/db/migrate/20231116120633_create_join_table_notes_tags.rb
deleted file mode 100644
index 2c605ca..0000000
--- a/db/migrate/20231116120633_create_join_table_notes_tags.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class CreateJoinTableNotesTags < ActiveRecord::Migration[7.1]
- def change
- create_join_table :notes, :tags do |t|
- t.index :note_id
- t.index :tag_id
- end
- end
-end
diff --git a/db/migrate/20231117170940_add_title_to_notes.rb b/db/migrate/20231117170940_add_title_to_notes.rb
deleted file mode 100644
index 3bae3a8..0000000
--- a/db/migrate/20231117170940_add_title_to_notes.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddTitleToNotes < ActiveRecord::Migration[7.1]
- def change
- add_column :notes, :title, :string
- end
-end
diff --git a/db/migrate/20231117174412_add_project_to_notes.rb b/db/migrate/20231117174412_add_project_to_notes.rb
deleted file mode 100644
index afc74d3..0000000
--- a/db/migrate/20231117174412_add_project_to_notes.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddProjectToNotes < ActiveRecord::Migration[7.1]
- def change
- add_reference :notes, :project, foreign_key: true
- end
-end
diff --git a/db/migrate/20231127092131_change_priority_in_tasks.rb b/db/migrate/20231127092131_change_priority_in_tasks.rb
deleted file mode 100644
index 4b8b044..0000000
--- a/db/migrate/20231127092131_change_priority_in_tasks.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-class ChangePriorityInTasks < ActiveRecord::Migration[7.1]
- def up
- add_column :tasks, :new_priority, :integer
-
- execute <<-SQL.squish
- UPDATE tasks SET new_priority = CASE
- WHEN priority = 'Low' THEN 0
- WHEN priority = 'Medium' THEN 1
- WHEN priority = 'High' THEN 2
- ELSE 0
- END
- SQL
-
- remove_column :tasks, :priority
- rename_column :tasks, :new_priority, :priority
- end
-
- def down
- add_column :tasks, :old_priority, :string
-
- execute <<-SQL.squish
- UPDATE tasks SET old_priority = CASE
- WHEN priority = 0 THEN 'Low'
- WHEN priority = 1 THEN 'Medium'
- WHEN priority = 2 THEN 'High'
- ELSE 'Low'
- END
- SQL
-
- remove_column :tasks, :priority
- rename_column :tasks, :old_priority, :priority
- end
-end
diff --git a/db/migrate/20231127094906_add_note_and_status_to_tasks.rb b/db/migrate/20231127094906_add_note_and_status_to_tasks.rb
deleted file mode 100644
index 3f94ba8..0000000
--- a/db/migrate/20231127094906_add_note_and_status_to_tasks.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-class AddNoteAndStatusToTasks < ActiveRecord::Migration[7.1]
- def up
- add_column :tasks, :note, :text
- add_column :tasks, :status, :integer, default: 0
-
- execute <<-SQL.squish
- UPDATE tasks SET status = CASE
- WHEN completed = 't' THEN 2
- ELSE 0
- END
- SQL
-
- remove_column :tasks, :completed
- end
-
- def down
- add_column :tasks, :completed, :boolean, default: false
-
- execute <<-SQL.squish
- UPDATE tasks SET completed = 't'
- WHERE status = 2
- SQL
-
- remove_column :tasks, :status
- remove_column :tasks, :note
- end
-end
diff --git a/db/migrate/20240326093339_add_active_to_projects.rb b/db/migrate/20240326093339_add_active_to_projects.rb
deleted file mode 100644
index ac54d4b..0000000
--- a/db/migrate/20240326093339_add_active_to_projects.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddActiveToProjects < ActiveRecord::Migration[7.1]
- def change
- add_column :projects, :active, :boolean, default: false
- end
-end
diff --git a/db/migrate/20241006225631_add_pin_to_sidebar_to_projects.rb b/db/migrate/20241006225631_add_pin_to_sidebar_to_projects.rb
deleted file mode 100644
index 43c8a6b..0000000
--- a/db/migrate/20241006225631_add_pin_to_sidebar_to_projects.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddPinToSidebarToProjects < ActiveRecord::Migration[7.1]
- def change
- add_column :projects, :pin_to_sidebar, :boolean, default: false
- end
-end
diff --git a/db/migrate/20241007143928_add_profile_fields_to_users.rb b/db/migrate/20241007143928_add_profile_fields_to_users.rb
deleted file mode 100644
index 5970356..0000000
--- a/db/migrate/20241007143928_add_profile_fields_to_users.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class AddProfileFieldsToUsers < ActiveRecord::Migration[7.1]
- def change
- add_column :users, :appearance, :string, default: 'light', null: false
- add_column :users, :language, :string, default: 'en', null: false
- add_column :users, :timezone, :string, default: 'UTC', null: false
- add_column :users, :avatar_image, :string
- end
-end
diff --git a/db/migrate/20241016105827_create_description_for_area.rb b/db/migrate/20241016105827_create_description_for_area.rb
deleted file mode 100644
index 444d68f..0000000
--- a/db/migrate/20241016105827_create_description_for_area.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class CreateDescriptionForArea < ActiveRecord::Migration[7.1]
- def change
- add_column :areas, :description, :string
- end
-end
diff --git a/db/migrate/20241121113756_create_projects_tags.rb b/db/migrate/20241121113756_create_projects_tags.rb
deleted file mode 100644
index 7334df7..0000000
--- a/db/migrate/20241121113756_create_projects_tags.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class CreateProjectsTags < ActiveRecord::Migration[7.1]
- def change
- create_table :projects_tags, id: false do |t|
- t.integer :project_id, null: false
- t.integer :tag_id, null: false
- end
-
- add_index :projects_tags, :project_id
- add_index :projects_tags, :tag_id
-
- add_foreign_key :projects_tags, :projects
- add_foreign_key :projects_tags, :tags
- end
-end
diff --git a/db/migrate/20241126095028_add_priority_to_projects.rb b/db/migrate/20241126095028_add_priority_to_projects.rb
deleted file mode 100644
index 74b8ac9..0000000
--- a/db/migrate/20241126095028_add_priority_to_projects.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddPriorityToProjects < ActiveRecord::Migration[7.1]
- def change
- add_column :projects, :priority, :integer
- end
-end
diff --git a/db/migrate/20250224162915_add_due_date_at_to_projects.rb b/db/migrate/20250224162915_add_due_date_at_to_projects.rb
deleted file mode 100644
index 34f47ac..0000000
--- a/db/migrate/20250224162915_add_due_date_at_to_projects.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddDueDateAtToProjects < ActiveRecord::Migration[7.1]
- def change
- add_column :projects, :due_date_at, :datetime
- end
-end
diff --git a/db/migrate/20250414134722_create_inbox_items.rb b/db/migrate/20250414134722_create_inbox_items.rb
deleted file mode 100644
index 009477b..0000000
--- a/db/migrate/20250414134722_create_inbox_items.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CreateInboxItems < ActiveRecord::Migration[7.1]
- def change
- create_table :inbox_items do |t|
- t.string :content, null: false
- t.references :user, null: false, foreign_key: true
- t.string :status, default: 'added'
- t.string :source, default: 'tududi'
- t.timestamps
- end
- end
-end
diff --git a/db/migrate/20250414150330_add_telegram_token_to_users.rb b/db/migrate/20250414150330_add_telegram_token_to_users.rb
deleted file mode 100644
index e2f930c..0000000
--- a/db/migrate/20250414150330_add_telegram_token_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class AddTelegramTokenToUsers < ActiveRecord::Migration[7.1]
- def change
- add_column :users, :telegram_bot_token, :string
- add_column :users, :telegram_chat_id, :string
- end
-end
diff --git a/db/migrate/20250416231240_add_task_summary_to_users.rb b/db/migrate/20250416231240_add_task_summary_to_users.rb
deleted file mode 100644
index 798375b..0000000
--- a/db/migrate/20250416231240_add_task_summary_to_users.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class AddTaskSummaryToUsers < ActiveRecord::Migration[7.1]
- def change
- add_column :users, :task_summary_enabled, :boolean, default: false
- add_column :users, :task_summary_frequency, :string, default: 'daily'
- end
-end
-
diff --git a/db/migrate/20250416235420_add_task_summary_run_tracking_to_users.rb b/db/migrate/20250416235420_add_task_summary_run_tracking_to_users.rb
deleted file mode 100644
index 2cc9878..0000000
--- a/db/migrate/20250416235420_add_task_summary_run_tracking_to_users.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class AddTaskSummaryRunTrackingToUsers < ActiveRecord::Migration[7.1]
- def change
- add_column :users, :task_summary_last_run, :datetime
- add_column :users, :task_summary_next_run, :datetime
- end
-end
-
diff --git a/db/schema.rb b/db/schema.rb
deleted file mode 100644
index a261c6d..0000000
--- a/db/schema.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-# This file is auto-generated from the current state of the database. Instead
-# of editing this file, please use the migrations feature of Active Record to
-# incrementally modify your database, and then regenerate this schema definition.
-#
-# This file is the source Rails uses to define your schema when running `bin/rails
-# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
-# be faster and is potentially less error prone than running all of your
-# migrations from scratch. Old migrations may fail to apply correctly if those
-# migrations use external dependencies or application code.
-#
-# It's strongly recommended that you check this file into your version control system.
-
-ActiveRecord::Schema[7.1].define(version: 2025_04_16_235420) do
- create_table "areas", force: :cascade do |t|
- t.string "name"
- t.integer "user_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "description"
- t.index ["user_id"], name: "index_areas_on_user_id"
- end
-
- create_table "inbox_items", force: :cascade do |t|
- t.string "content", null: false
- t.integer "user_id", null: false
- t.string "status", default: "added"
- t.string "source", default: "tududi"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["user_id"], name: "index_inbox_items_on_user_id"
- end
-
- create_table "notes", force: :cascade do |t|
- t.text "content"
- t.integer "user_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "title"
- t.integer "project_id"
- t.index ["project_id"], name: "index_notes_on_project_id"
- t.index ["user_id"], name: "index_notes_on_user_id"
- end
-
- create_table "notes_tags", id: false, force: :cascade do |t|
- t.integer "note_id", null: false
- t.integer "tag_id", null: false
- t.index ["note_id"], name: "index_notes_tags_on_note_id"
- t.index ["tag_id"], name: "index_notes_tags_on_tag_id"
- end
-
- create_table "projects", force: :cascade do |t|
- t.string "name"
- t.integer "user_id", null: false
- t.integer "area_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.text "description"
- t.boolean "active", default: false
- t.boolean "pin_to_sidebar", default: false
- t.integer "priority"
- t.datetime "due_date_at"
- t.index ["area_id"], name: "index_projects_on_area_id"
- t.index ["user_id"], name: "index_projects_on_user_id"
- end
-
- create_table "projects_tags", id: false, force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "tag_id", null: false
- t.index ["project_id"], name: "index_projects_tags_on_project_id"
- t.index ["tag_id"], name: "index_projects_tags_on_tag_id"
- end
-
- create_table "tags", force: :cascade do |t|
- t.string "name"
- t.integer "user_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["user_id"], name: "index_tags_on_user_id"
- end
-
- create_table "tags_tasks", id: false, force: :cascade do |t|
- t.integer "task_id", null: false
- t.integer "tag_id", null: false
- t.index ["tag_id"], name: "index_tags_tasks_on_tag_id"
- t.index ["task_id"], name: "index_tags_tasks_on_task_id"
- end
-
- create_table "tasks", force: :cascade do |t|
- t.string "name"
- t.datetime "due_date"
- t.integer "user_id", null: false
- t.integer "project_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "today", default: false
- t.text "description"
- t.integer "priority"
- t.text "note"
- t.integer "status", default: 0
- t.string "recurrence_type", default: "none"
- t.integer "recurrence_interval"
- t.datetime "recurrence_end_date"
- t.datetime "last_generated_date"
- t.index ["last_generated_date"], name: "index_tasks_on_last_generated_date"
- t.index ["project_id"], name: "index_tasks_on_project_id"
- t.index ["recurrence_type"], name: "index_tasks_on_recurrence_type"
- t.index ["user_id"], name: "index_tasks_on_user_id"
- end
-
- create_table "users", force: :cascade do |t|
- t.string "name"
- t.string "email"
- t.string "password_digest"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "appearance", default: "light", null: false
- t.string "language", default: "en", null: false
- t.string "timezone", default: "UTC", null: false
- t.string "avatar_image"
- t.string "telegram_bot_token"
- t.string "telegram_chat_id"
- t.boolean "task_summary_enabled", default: false
- t.string "task_summary_frequency", default: "daily"
- t.datetime "task_summary_last_run"
- t.datetime "task_summary_next_run"
- end
-
- add_foreign_key "areas", "users"
- add_foreign_key "inbox_items", "users"
- add_foreign_key "notes", "projects"
- add_foreign_key "notes", "users", on_delete: :cascade
- add_foreign_key "projects", "areas", on_delete: :cascade
- add_foreign_key "projects", "users"
- add_foreign_key "projects_tags", "projects"
- add_foreign_key "projects_tags", "tags"
- add_foreign_key "tags", "users", on_delete: :cascade
- add_foreign_key "tags_tasks", "tags"
- add_foreign_key "tags_tasks", "tasks"
- add_foreign_key "tasks", "projects", on_delete: :cascade
- add_foreign_key "tasks", "users"
-end
diff --git a/db/seeds.rb b/db/seeds.rb
deleted file mode 100644
index 96188ef..0000000
--- a/db/seeds.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'faker'
-
-user = User.create(email: 'myemail@somewhere.com', password: 'awes0meHax0Rp4ssword')
-user_id = user.id
-
-4.times do
- Area.create(name: Faker::Space.galaxy, user_id: user_id)
-end
-
-areas = Area.where(user_id: user_id)
-
-total_projects = 10
-projects_per_area = total_projects / areas.count
-areas.each do |area|
- projects_per_area.times do
- Project.create(
- name: Faker::App.name,
- description: Faker::Lorem.sentence(word_count: 10),
- user_id: user_id,
- area_id: area.id
- )
- end
-end
-
-projects = Project.where(user_id: user_id)
-
-projects.each do |project|
- 8.times do
- Task.create(
- name: Faker::Lorem.sentence(word_count: 3),
- priority: %w[Low Medium High].sample,
- due_date: [Date.today, Date.today + rand(1..30), nil].sample,
- description: Faker::Lorem.sentence(word_count: 15),
- status: [0, 1, 2].sample,
- user_id: user_id,
- project_id: project.id
- )
- end
-end
diff --git a/dist/frontend_components_Tasks_tsx.bcee0af8633c28243d32.js b/dist/frontend_components_Tasks_tsx.bcee0af8633c28243d32.js
deleted file mode 100644
index c0218be..0000000
--- a/dist/frontend_components_Tasks_tsx.bcee0af8633c28243d32.js
+++ /dev/null
@@ -1,92 +0,0 @@
-"use strict";
-/*
- * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
- * This devtool is neither made for production nor for readable output files.
- * It uses "eval()" calls to create a separate source file in the browser devtools.
- * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
- * or disable the default devtool with "devtool: false".
- * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
- */
-(self["webpackChunktududi"] = self["webpackChunktududi"] || []).push([["frontend_components_Tasks_tsx"],{
-
-/***/ "./frontend/components/Task/getDescription.ts":
-/*!****************************************************!*\
- !*** ./frontend/components/Task/getDescription.ts ***!
- \****************************************************/
-/***/ ((module, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ getDescription: () => (/* binding */ getDescription)\n/* harmony export */ });\n/* provided dependency */ var __react_refresh_utils__ = __webpack_require__(/*! ./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js */ \"./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js\");\n/* provided dependency */ var __react_refresh_error_overlay__ = __webpack_require__(/*! ./node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js */ \"./node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js\");\n__webpack_require__.$Refresh$.runtime = __webpack_require__(/*! ./node_modules/react-refresh/runtime.js */ \"./node_modules/react-refresh/runtime.js\");\n\nvar getDescription = function getDescription(query, projects, t) {\n try {\n // Default descriptions as fallbacks in case translation function fails\n var defaultDescriptions = {\n project: \"Project tasks\",\n today: \"Tasks due today or scheduled for immediate attention\",\n inbox: \"Uncategorized tasks without project or due date\",\n next: \"Tasks that are actionable in the near future\",\n upcoming: \"Tasks scheduled for the upcoming week\",\n someday: \"Tasks without urgency or specific due date\",\n completed: \"Tasks you've completed\",\n allTasks: \"All tasks from different projects and priorities\"\n };\n\n // Check for project_id first\n var projectId = query.get('project_id');\n if (projectId) {\n try {\n var project = projects.find(function (p) {\n var _p$id;\n return ((_p$id = p.id) === null || _p$id === void 0 ? void 0 : _p$id.toString()) === projectId;\n });\n if (project) {\n return t(\"taskViews.project.withName\", {\n projectName: project.name\n });\n } else {\n return t(\"taskViews.project.noName\");\n }\n } catch (e) {\n console.error(\"Translation error for project description:\", e);\n // Fallback with project name if available\n var _project = projects.find(function (p) {\n var _p$id2;\n return ((_p$id2 = p.id) === null || _p$id2 === void 0 ? void 0 : _p$id2.toString()) === projectId;\n });\n return _project ? \"Tasks for project: \".concat(_project.name) : defaultDescriptions.project;\n }\n }\n\n // Then check for type and status parameters\n try {\n if (query.get('type') === 'today') {\n return t(\"taskViews.today\");\n }\n if (query.get('type') === 'inbox') {\n return t(\"taskViews.inbox\");\n }\n if (query.get('type') === 'next') {\n return t(\"taskViews.next\");\n }\n if (query.get('type') === 'upcoming') {\n return t(\"taskViews.upcoming\");\n }\n if (query.get('type') === 'someday') {\n return t(\"taskViews.someday\");\n }\n if (query.get('status') === 'done') {\n return t(\"taskViews.completed\");\n }\n return t(\"taskViews.allTasks\");\n } catch (e) {\n console.error(\"Translation error for task view description:\", e);\n\n // Return appropriate fallback based on type or status\n if (query.get('type') === 'today') return defaultDescriptions.today;\n if (query.get('type') === 'inbox') return defaultDescriptions.inbox;\n if (query.get('type') === 'next') return defaultDescriptions.next;\n if (query.get('type') === 'upcoming') return defaultDescriptions.upcoming;\n if (query.get('type') === 'someday') return defaultDescriptions.someday;\n if (query.get('status') === 'done') return defaultDescriptions.completed;\n return defaultDescriptions.allTasks;\n }\n } catch (error) {\n console.error(\"Error in getDescription:\", error);\n return \"Tasks overview\";\n }\n};\n\nconst $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;\nconst $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(\n\t$ReactRefreshModuleId$\n);\n\nfunction $ReactRefreshModuleRuntime$(exports) {\n\tif (true) {\n\t\tlet errorOverlay;\n\t\tif (typeof __react_refresh_error_overlay__ !== 'undefined') {\n\t\t\terrorOverlay = __react_refresh_error_overlay__;\n\t\t}\n\t\tlet testMode;\n\t\tif (typeof __react_refresh_test__ !== 'undefined') {\n\t\t\ttestMode = __react_refresh_test__;\n\t\t}\n\t\treturn __react_refresh_utils__.executeRuntime(\n\t\t\texports,\n\t\t\t$ReactRefreshModuleId$,\n\t\t\tmodule.hot,\n\t\t\terrorOverlay,\n\t\t\ttestMode\n\t\t);\n\t}\n}\n\nif (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {\n\t$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);\n} else {\n\t$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);\n}\n\n//# sourceURL=webpack://tududi/./frontend/components/Task/getDescription.ts?");
-
-/***/ }),
-
-/***/ "./frontend/components/Task/getTitleAndIcon.ts":
-/*!*****************************************************!*\
- !*** ./frontend/components/Task/getTitleAndIcon.ts ***!
- \*****************************************************/
-/***/ ((module, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ getTitleAndIcon: () => (/* binding */ getTitleAndIcon)\n/* harmony export */ });\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/FolderIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/CalendarIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/InboxIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/ArrowRightIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/ClockIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/MoonIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/CheckCircleIcon.js\");\n/* harmony import */ var _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @heroicons/react/24/outline */ \"./node_modules/@heroicons/react/24/outline/esm/Bars4Icon.js\");\n/* provided dependency */ var __react_refresh_utils__ = __webpack_require__(/*! ./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js */ \"./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js\");\n/* provided dependency */ var __react_refresh_error_overlay__ = __webpack_require__(/*! ./node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js */ \"./node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js\");\n__webpack_require__.$Refresh$.runtime = __webpack_require__(/*! ./node_modules/react-refresh/runtime.js */ \"./node_modules/react-refresh/runtime.js\");\n\n\nvar getTitleAndIcon = function getTitleAndIcon(query, projects, t) {\n try {\n // Default titles as fallbacks in case translation function fails\n var defaultTitles = {\n project: 'Project',\n today: 'Today',\n inbox: 'Inbox',\n next: 'Next Actions',\n upcoming: 'Upcoming',\n someday: 'Someday',\n completed: 'Completed',\n allTasks: 'All Tasks'\n };\n var projectId = query.get('project_id');\n if (projectId) {\n var project = projects.find(function (p) {\n var _p$id;\n return ((_p$id = p.id) === null || _p$id === void 0 ? void 0 : _p$id.toString()) === projectId;\n });\n return {\n title: project ? project.name : t('sidebar.projects'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n };\n }\n try {\n if (query.get('type') === 'today') {\n return {\n title: t('tasks.today'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n };\n }\n if (query.get('type') === 'inbox') {\n return {\n title: t('sidebar.inbox'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_2__[\"default\"]\n };\n }\n if (query.get('type') === 'next') {\n return {\n title: t('sidebar.nextActions'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_3__[\"default\"]\n };\n }\n if (query.get('type') === 'upcoming') {\n return {\n title: t('sidebar.upcoming'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n };\n }\n if (query.get('type') === 'someday') {\n return {\n title: t('taskViews.someday') || defaultTitles.someday,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_5__[\"default\"]\n };\n }\n if (query.get('status') === 'done') {\n return {\n title: t('sidebar.completed'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_6__[\"default\"]\n };\n }\n return {\n title: t('sidebar.allTasks'),\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_7__[\"default\"]\n };\n } catch (e) {\n console.error(\"Translation error for task view title:\", e);\n\n // Return appropriate fallback based on type or status\n if (query.get('type') === 'today') return {\n title: defaultTitles.today,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n };\n if (query.get('type') === 'inbox') return {\n title: defaultTitles.inbox,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_2__[\"default\"]\n };\n if (query.get('type') === 'next') return {\n title: defaultTitles.next,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_3__[\"default\"]\n };\n if (query.get('type') === 'upcoming') return {\n title: defaultTitles.upcoming,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n };\n if (query.get('type') === 'someday') return {\n title: defaultTitles.someday,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_5__[\"default\"]\n };\n if (query.get('status') === 'done') return {\n title: defaultTitles.completed,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_6__[\"default\"]\n };\n return {\n title: defaultTitles.allTasks,\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_7__[\"default\"]\n };\n }\n } catch (error) {\n console.error(\"Error in getTitleAndIcon:\", error);\n return {\n title: \"Tasks\",\n icon: _heroicons_react_24_outline__WEBPACK_IMPORTED_MODULE_7__[\"default\"]\n };\n }\n};\n\nconst $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;\nconst $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(\n\t$ReactRefreshModuleId$\n);\n\nfunction $ReactRefreshModuleRuntime$(exports) {\n\tif (true) {\n\t\tlet errorOverlay;\n\t\tif (typeof __react_refresh_error_overlay__ !== 'undefined') {\n\t\t\terrorOverlay = __react_refresh_error_overlay__;\n\t\t}\n\t\tlet testMode;\n\t\tif (typeof __react_refresh_test__ !== 'undefined') {\n\t\t\ttestMode = __react_refresh_test__;\n\t\t}\n\t\treturn __react_refresh_utils__.executeRuntime(\n\t\t\texports,\n\t\t\t$ReactRefreshModuleId$,\n\t\t\tmodule.hot,\n\t\t\terrorOverlay,\n\t\t\ttestMode\n\t\t);\n\t}\n}\n\nif (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {\n\t$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);\n} else {\n\t$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);\n}\n\n//# sourceURL=webpack://tududi/./frontend/components/Task/getTitleAndIcon.ts?");
-
-/***/ }),
-
-/***/ "./frontend/components/Tasks.tsx":
-/*!***************************************!*\
- !*** ./frontend/components/Tasks.tsx ***!
- \***************************************/
-/***/ ((module, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! react-router-dom */ \"./node_modules/react-router/dist/index.js\");\n/* harmony import */ var react_i18next__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-i18next */ \"./node_modules/react-i18next/dist/es/index.js\");\n/* harmony import */ var _Task_TaskList__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./Task/TaskList */ \"./frontend/components/Task/TaskList.tsx\");\n/* harmony import */ var _Task_NewTask__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./Task/NewTask */ \"./frontend/components/Task/NewTask.tsx\");\n/* harmony import */ var _Task_getTitleAndIcon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./Task/getTitleAndIcon */ \"./frontend/components/Task/getTitleAndIcon.ts\");\n/* harmony import */ var _Task_getDescription__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./Task/getDescription */ \"./frontend/components/Task/getDescription.ts\");\n/* harmony import */ var _utils_tasksService__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../utils/tasksService */ \"./frontend/utils/tasksService.ts\");\n/* harmony import */ var _heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @heroicons/react/24/solid */ \"./node_modules/@heroicons/react/24/solid/esm/TagIcon.js\");\n/* harmony import */ var _heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! @heroicons/react/24/solid */ \"./node_modules/@heroicons/react/24/solid/esm/XMarkIcon.js\");\n/* harmony import */ var _heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! @heroicons/react/24/solid */ \"./node_modules/@heroicons/react/24/solid/esm/ChevronDoubleDownIcon.js\");\n/* harmony import */ var _heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! @heroicons/react/24/solid */ \"./node_modules/@heroicons/react/24/solid/esm/ChevronDownIcon.js\");\n/* harmony import */ var _heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! @heroicons/react/24/solid */ \"./node_modules/@heroicons/react/24/solid/esm/MagnifyingGlassIcon.js\");\n/* provided dependency */ var __react_refresh_utils__ = __webpack_require__(/*! ./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js */ \"./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js\");\n/* provided dependency */ var __react_refresh_error_overlay__ = __webpack_require__(/*! ./node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js */ \"./node_modules/@pmmmwh/react-refresh-webpack-plugin/overlay/index.js\");\n__webpack_require__.$Refresh$.runtime = __webpack_require__(/*! ./node_modules/react-refresh/runtime.js */ \"./node_modules/react-refresh/runtime.js\");\n\nfunction _typeof(o) { \"@babel/helpers - typeof\"; return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o; }, _typeof(o); }\nvar _s = __webpack_require__.$Refresh$.signature();\nfunction _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }\nfunction _nonIterableSpread() { throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); }\nfunction _iterableToArray(r) { if (\"undefined\" != typeof Symbol && null != r[Symbol.iterator] || null != r[\"@@iterator\"]) return Array.from(r); }\nfunction _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }\nfunction _regeneratorRuntime() { \"use strict\"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return e; }; var t, e = {}, r = Object.prototype, n = r.hasOwnProperty, o = Object.defineProperty || function (t, e, r) { t[e] = r.value; }, i = \"function\" == typeof Symbol ? Symbol : {}, a = i.iterator || \"@@iterator\", c = i.asyncIterator || \"@@asyncIterator\", u = i.toStringTag || \"@@toStringTag\"; function define(t, e, r) { return Object.defineProperty(t, e, { value: r, enumerable: !0, configurable: !0, writable: !0 }), t[e]; } try { define({}, \"\"); } catch (t) { define = function define(t, e, r) { return t[e] = r; }; } function wrap(t, e, r, n) { var i = e && e.prototype instanceof Generator ? e : Generator, a = Object.create(i.prototype), c = new Context(n || []); return o(a, \"_invoke\", { value: makeInvokeMethod(t, r, c) }), a; } function tryCatch(t, e, r) { try { return { type: \"normal\", arg: t.call(e, r) }; } catch (t) { return { type: \"throw\", arg: t }; } } e.wrap = wrap; var h = \"suspendedStart\", l = \"suspendedYield\", f = \"executing\", s = \"completed\", y = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var p = {}; define(p, a, function () { return this; }); var d = Object.getPrototypeOf, v = d && d(d(values([]))); v && v !== r && n.call(v, a) && (p = v); var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p); function defineIteratorMethods(t) { [\"next\", \"throw\", \"return\"].forEach(function (e) { define(t, e, function (t) { return this._invoke(e, t); }); }); } function AsyncIterator(t, e) { function invoke(r, o, i, a) { var c = tryCatch(t[r], t, o); if (\"throw\" !== c.type) { var u = c.arg, h = u.value; return h && \"object\" == _typeof(h) && n.call(h, \"__await\") ? e.resolve(h.__await).then(function (t) { invoke(\"next\", t, i, a); }, function (t) { invoke(\"throw\", t, i, a); }) : e.resolve(h).then(function (t) { u.value = t, i(u); }, function (t) { return invoke(\"throw\", t, i, a); }); } a(c.arg); } var r; o(this, \"_invoke\", { value: function value(t, n) { function callInvokeWithMethodAndArg() { return new e(function (e, r) { invoke(t, n, e, r); }); } return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); } }); } function makeInvokeMethod(e, r, n) { var o = h; return function (i, a) { if (o === f) throw Error(\"Generator is already running\"); if (o === s) { if (\"throw\" === i) throw a; return { value: t, done: !0 }; } for (n.method = i, n.arg = a;;) { var c = n.delegate; if (c) { var u = maybeInvokeDelegate(c, n); if (u) { if (u === y) continue; return u; } } if (\"next\" === n.method) n.sent = n._sent = n.arg;else if (\"throw\" === n.method) { if (o === h) throw o = s, n.arg; n.dispatchException(n.arg); } else \"return\" === n.method && n.abrupt(\"return\", n.arg); o = f; var p = tryCatch(e, r, n); if (\"normal\" === p.type) { if (o = n.done ? s : l, p.arg === y) continue; return { value: p.arg, done: n.done }; } \"throw\" === p.type && (o = s, n.method = \"throw\", n.arg = p.arg); } }; } function maybeInvokeDelegate(e, r) { var n = r.method, o = e.iterator[n]; if (o === t) return r.delegate = null, \"throw\" === n && e.iterator[\"return\"] && (r.method = \"return\", r.arg = t, maybeInvokeDelegate(e, r), \"throw\" === r.method) || \"return\" !== n && (r.method = \"throw\", r.arg = new TypeError(\"The iterator does not provide a '\" + n + \"' method\")), y; var i = tryCatch(o, e.iterator, r.arg); if (\"throw\" === i.type) return r.method = \"throw\", r.arg = i.arg, r.delegate = null, y; var a = i.arg; return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, \"return\" !== r.method && (r.method = \"next\", r.arg = t), r.delegate = null, y) : a : (r.method = \"throw\", r.arg = new TypeError(\"iterator result is not an object\"), r.delegate = null, y); } function pushTryEntry(t) { var e = { tryLoc: t[0] }; 1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e); } function resetTryEntry(t) { var e = t.completion || {}; e.type = \"normal\", delete e.arg, t.completion = e; } function Context(t) { this.tryEntries = [{ tryLoc: \"root\" }], t.forEach(pushTryEntry, this), this.reset(!0); } function values(e) { if (e || \"\" === e) { var r = e[a]; if (r) return r.call(e); if (\"function\" == typeof e.next) return e; if (!isNaN(e.length)) { var o = -1, i = function next() { for (; ++o < e.length;) if (n.call(e, o)) return next.value = e[o], next.done = !1, next; return next.value = t, next.done = !0, next; }; return i.next = i; } } throw new TypeError(_typeof(e) + \" is not iterable\"); } return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, \"constructor\", { value: GeneratorFunctionPrototype, configurable: !0 }), o(GeneratorFunctionPrototype, \"constructor\", { value: GeneratorFunction, configurable: !0 }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, \"GeneratorFunction\"), e.isGeneratorFunction = function (t) { var e = \"function\" == typeof t && t.constructor; return !!e && (e === GeneratorFunction || \"GeneratorFunction\" === (e.displayName || e.name)); }, e.mark = function (t) { return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, \"GeneratorFunction\")), t.prototype = Object.create(g), t; }, e.awrap = function (t) { return { __await: t }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function () { return this; }), e.AsyncIterator = AsyncIterator, e.async = function (t, r, n, o, i) { void 0 === i && (i = Promise); var a = new AsyncIterator(wrap(t, r, n, o), i); return e.isGeneratorFunction(r) ? a : a.next().then(function (t) { return t.done ? t.value : a.next(); }); }, defineIteratorMethods(g), define(g, u, \"Generator\"), define(g, a, function () { return this; }), define(g, \"toString\", function () { return \"[object Generator]\"; }), e.keys = function (t) { var e = Object(t), r = []; for (var n in e) r.push(n); return r.reverse(), function next() { for (; r.length;) { var t = r.pop(); if (t in e) return next.value = t, next.done = !1, next; } return next.done = !0, next; }; }, e.values = values, Context.prototype = { constructor: Context, reset: function reset(e) { if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = \"next\", this.arg = t, this.tryEntries.forEach(resetTryEntry), !e) for (var r in this) \"t\" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t); }, stop: function stop() { this.done = !0; var t = this.tryEntries[0].completion; if (\"throw\" === t.type) throw t.arg; return this.rval; }, dispatchException: function dispatchException(e) { if (this.done) throw e; var r = this; function handle(n, o) { return a.type = \"throw\", a.arg = e, r.next = n, o && (r.method = \"next\", r.arg = t), !!o; } for (var o = this.tryEntries.length - 1; o >= 0; --o) { var i = this.tryEntries[o], a = i.completion; if (\"root\" === i.tryLoc) return handle(\"end\"); if (i.tryLoc <= this.prev) { var c = n.call(i, \"catchLoc\"), u = n.call(i, \"finallyLoc\"); if (c && u) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } else if (c) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); } else { if (!u) throw Error(\"try statement without catch or finally\"); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } } } }, abrupt: function abrupt(t, e) { for (var r = this.tryEntries.length - 1; r >= 0; --r) { var o = this.tryEntries[r]; if (o.tryLoc <= this.prev && n.call(o, \"finallyLoc\") && this.prev < o.finallyLoc) { var i = o; break; } } i && (\"break\" === t || \"continue\" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null); var a = i ? i.completion : {}; return a.type = t, a.arg = e, i ? (this.method = \"next\", this.next = i.finallyLoc, y) : this.complete(a); }, complete: function complete(t, e) { if (\"throw\" === t.type) throw t.arg; return \"break\" === t.type || \"continue\" === t.type ? this.next = t.arg : \"return\" === t.type ? (this.rval = this.arg = t.arg, this.method = \"return\", this.next = \"end\") : \"normal\" === t.type && e && (this.next = e), y; }, finish: function finish(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y; } }, \"catch\": function _catch(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.tryLoc === t) { var n = r.completion; if (\"throw\" === n.type) { var o = n.arg; resetTryEntry(r); } return o; } } throw Error(\"illegal catch attempt\"); }, delegateYield: function delegateYield(e, r, n) { return this.delegate = { iterator: values(e), resultName: r, nextLoc: n }, \"next\" === this.method && (this.arg = t), y; } }, e; }\nfunction asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }\nfunction _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, \"next\", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, \"throw\", n); } _next(void 0); }); }; }\nfunction _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }\nfunction _nonIterableRest() { throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); }\nfunction _unsupportedIterableToArray(r, a) { if (r) { if (\"string\" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return \"Object\" === t && r.constructor && (t = r.constructor.name), \"Map\" === t || \"Set\" === t ? Array.from(r) : \"Arguments\" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }\nfunction _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }\nfunction _iterableToArrayLimit(r, l) { var t = null == r ? null : \"undefined\" != typeof Symbol && r[Symbol.iterator] || r[\"@@iterator\"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t[\"return\"] && (u = t[\"return\"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }\nfunction _arrayWithHoles(r) { if (Array.isArray(r)) return r; }\n\n\n\n\n\n\n\n\n\nvar capitalize = function capitalize(str) {\n return str.charAt(0).toUpperCase() + str.slice(1);\n};\n\n// Helper function to get search placeholder by language\nvar getSearchPlaceholder = function getSearchPlaceholder(language) {\n var placeholders = {\n en: 'Search tasks...',\n el: 'Αναζήτηση εργασιών...',\n es: 'Buscar tareas...',\n de: 'Aufgaben suchen...',\n jp: 'タスクを検索...',\n ua: 'Пошук завдань...'\n };\n return placeholders[language] || 'Search tasks...';\n};\nvar Tasks = function Tasks() {\n _s();\n var _useTranslation = (0,react_i18next__WEBPACK_IMPORTED_MODULE_1__.useTranslation)(),\n t = _useTranslation.t,\n i18n = _useTranslation.i18n;\n var _useState = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]),\n _useState2 = _slicedToArray(_useState, 2),\n tasks = _useState2[0],\n setTasks = _useState2[1];\n var _useState3 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]),\n _useState4 = _slicedToArray(_useState3, 2),\n projects = _useState4[0],\n setProjects = _useState4[1];\n var _useState5 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(true),\n _useState6 = _slicedToArray(_useState5, 2),\n loading = _useState6[0],\n setLoading = _useState6[1];\n var _useState7 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null),\n _useState8 = _slicedToArray(_useState7, 2),\n error = _useState8[0],\n setError = _useState8[1];\n var _useState9 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false),\n _useState10 = _slicedToArray(_useState9, 2),\n dropdownOpen = _useState10[0],\n setDropdownOpen = _useState10[1];\n var _useState11 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(\"due_date:asc\"),\n _useState12 = _slicedToArray(_useState11, 2),\n orderBy = _useState12[0],\n setOrderBy = _useState12[1];\n var _useState13 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(\"\"),\n _useState14 = _slicedToArray(_useState13, 2),\n taskSearchQuery = _useState14[0],\n setTaskSearchQuery = _useState14[1];\n var dropdownRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(null);\n var location = (0,react_router_dom__WEBPACK_IMPORTED_MODULE_7__.useLocation)();\n var navigate = (0,react_router_dom__WEBPACK_IMPORTED_MODULE_7__.useNavigate)();\n var query = new URLSearchParams(location.search);\n var _ref = location.state || {},\n stateTitle = _ref.title,\n stateIcon = _ref.icon;\n var _ref2 = stateTitle && stateIcon ? {\n title: stateTitle,\n icon: stateIcon\n } : (0,_Task_getTitleAndIcon__WEBPACK_IMPORTED_MODULE_4__.getTitleAndIcon)(query, projects, t),\n title = _ref2.title,\n icon = _ref2.icon;\n var IconComponent = typeof icon === \"string\" ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(icon) : icon;\n var tag = query.get(\"tag\");\n var status = query.get(\"status\");\n (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function () {\n var savedOrderBy = localStorage.getItem(\"order_by\") || \"due_date:asc\";\n setOrderBy(savedOrderBy);\n var params = new URLSearchParams(location.search);\n if (!params.get(\"order_by\")) {\n params.set(\"order_by\", savedOrderBy);\n navigate({\n pathname: location.pathname,\n search: \"?\".concat(params.toString())\n }, {\n replace: true\n });\n }\n }, [location.pathname]);\n (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function () {\n var handleClickOutside = function handleClickOutside(event) {\n if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {\n setDropdownOpen(false);\n }\n };\n if (dropdownOpen) {\n document.addEventListener(\"mousedown\", handleClickOutside);\n }\n return function () {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n };\n }, [dropdownOpen]);\n (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function () {\n var fetchData = /*#__PURE__*/function () {\n var _ref3 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee() {\n var tagId, _yield$Promise$all, _yield$Promise$all2, tasksResponse, projectsResponse, tasksData, projectsData;\n return _regeneratorRuntime().wrap(function _callee$(_context) {\n while (1) switch (_context.prev = _context.next) {\n case 0:\n setLoading(true);\n setError(null);\n _context.prev = 2;\n tagId = query.get(\"tag\");\n _context.next = 6;\n return Promise.all([fetch(\"/api/tasks\".concat(location.search).concat(tagId ? \"&tag=\".concat(tagId) : \"\")), fetch(\"/api/projects\")]);\n case 6:\n _yield$Promise$all = _context.sent;\n _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2);\n tasksResponse = _yield$Promise$all2[0];\n projectsResponse = _yield$Promise$all2[1];\n if (!tasksResponse.ok) {\n _context.next = 17;\n break;\n }\n _context.next = 13;\n return tasksResponse.json();\n case 13:\n tasksData = _context.sent;\n setTasks(tasksData.tasks || []);\n _context.next = 18;\n break;\n case 17:\n throw new Error(\"Failed to fetch tasks.\");\n case 18:\n if (!projectsResponse.ok) {\n _context.next = 25;\n break;\n }\n _context.next = 21;\n return projectsResponse.json();\n case 21:\n projectsData = _context.sent;\n setProjects((projectsData === null || projectsData === void 0 ? void 0 : projectsData.projects) || []);\n _context.next = 26;\n break;\n case 25:\n throw new Error(\"Failed to fetch projects.\");\n case 26:\n _context.next = 31;\n break;\n case 28:\n _context.prev = 28;\n _context.t0 = _context[\"catch\"](2);\n setError(_context.t0.message);\n case 31:\n _context.prev = 31;\n setLoading(false);\n return _context.finish(31);\n case 34:\n case \"end\":\n return _context.stop();\n }\n }, _callee, null, [[2, 28, 31, 34]]);\n }));\n return function fetchData() {\n return _ref3.apply(this, arguments);\n };\n }();\n fetchData();\n }, [location]);\n var handleRemoveTag = function handleRemoveTag() {\n var params = new URLSearchParams(location.search);\n params[\"delete\"](\"tag\");\n navigate({\n pathname: location.pathname,\n search: \"?\".concat(params.toString())\n });\n };\n var handleTaskCreate = /*#__PURE__*/function () {\n var _ref4 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(taskData) {\n var newTask;\n return _regeneratorRuntime().wrap(function _callee2$(_context2) {\n while (1) switch (_context2.prev = _context2.next) {\n case 0:\n _context2.prev = 0;\n _context2.next = 3;\n return (0,_utils_tasksService__WEBPACK_IMPORTED_MODULE_6__.createTask)(taskData);\n case 3:\n newTask = _context2.sent;\n // Add the new task optimistically to avoid race conditions\n setTasks(function (prevTasks) {\n return [newTask].concat(_toConsumableArray(prevTasks));\n });\n _context2.next = 12;\n break;\n case 7:\n _context2.prev = 7;\n _context2.t0 = _context2[\"catch\"](0);\n console.error(\"Error creating task:\", _context2.t0);\n setError(\"Error creating task.\");\n throw _context2.t0;\n case 12:\n case \"end\":\n return _context2.stop();\n }\n }, _callee2, null, [[0, 7]]);\n }));\n return function handleTaskCreate(_x) {\n return _ref4.apply(this, arguments);\n };\n }();\n var handleTaskUpdate = /*#__PURE__*/function () {\n var _ref5 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee3(updatedTask) {\n var response, errorData;\n return _regeneratorRuntime().wrap(function _callee3$(_context3) {\n while (1) switch (_context3.prev = _context3.next) {\n case 0:\n _context3.prev = 0;\n _context3.next = 3;\n return fetch(\"/api/task/\".concat(updatedTask.id), {\n method: \"PATCH\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(updatedTask)\n });\n case 3:\n response = _context3.sent;\n if (!response.ok) {\n _context3.next = 8;\n break;\n }\n setTasks(function (prevTasks) {\n return prevTasks.map(function (task) {\n return task.id === updatedTask.id ? updatedTask : task;\n });\n });\n _context3.next = 13;\n break;\n case 8:\n _context3.next = 10;\n return response.json();\n case 10:\n errorData = _context3.sent;\n console.error(\"Failed to update task:\", errorData.error);\n setError(\"Failed to update task.\");\n case 13:\n _context3.next = 19;\n break;\n case 15:\n _context3.prev = 15;\n _context3.t0 = _context3[\"catch\"](0);\n console.error(\"Error updating task:\", _context3.t0);\n setError(\"Error updating task.\");\n case 19:\n case \"end\":\n return _context3.stop();\n }\n }, _callee3, null, [[0, 15]]);\n }));\n return function handleTaskUpdate(_x2) {\n return _ref5.apply(this, arguments);\n };\n }();\n var handleTaskDelete = /*#__PURE__*/function () {\n var _ref6 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee4(taskId) {\n var response, errorData;\n return _regeneratorRuntime().wrap(function _callee4$(_context4) {\n while (1) switch (_context4.prev = _context4.next) {\n case 0:\n _context4.prev = 0;\n _context4.next = 3;\n return fetch(\"/api/task/\".concat(taskId), {\n method: \"DELETE\"\n });\n case 3:\n response = _context4.sent;\n if (!response.ok) {\n _context4.next = 8;\n break;\n }\n setTasks(function (prevTasks) {\n return prevTasks.filter(function (task) {\n return task.id !== taskId;\n });\n });\n _context4.next = 13;\n break;\n case 8:\n _context4.next = 10;\n return response.json();\n case 10:\n errorData = _context4.sent;\n console.error(\"Failed to delete task:\", errorData.error);\n setError(\"Failed to delete task.\");\n case 13:\n _context4.next = 19;\n break;\n case 15:\n _context4.prev = 15;\n _context4.t0 = _context4[\"catch\"](0);\n console.error(\"Error deleting task:\", _context4.t0);\n setError(\"Error deleting task.\");\n case 19:\n case \"end\":\n return _context4.stop();\n }\n }, _callee4, null, [[0, 15]]);\n }));\n return function handleTaskDelete(_x3) {\n return _ref6.apply(this, arguments);\n };\n }();\n var handleSortChange = function handleSortChange(order) {\n setOrderBy(order);\n localStorage.setItem(\"order_by\", order);\n var params = new URLSearchParams(location.search);\n params.set(\"order_by\", order);\n navigate({\n pathname: location.pathname,\n search: \"?\".concat(params.toString())\n }, {\n replace: true\n });\n setDropdownOpen(false);\n };\n var description = (0,_Task_getDescription__WEBPACK_IMPORTED_MODULE_5__.getDescription)(query, projects, t);\n var isNewTaskAllowed = function isNewTaskAllowed() {\n return status !== \"done\";\n };\n var filteredTasks = tasks.filter(function (task) {\n return task.name.toLowerCase().includes(taskSearchQuery.toLowerCase());\n });\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"flex justify-center px-4 lg:px-2\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"w-full max-w-5xl\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"flex flex-col sm:flex-row items-start sm:items-center justify-between mb-4\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"flex items-center mb-2 sm:mb-0\"\n }, IconComponent && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(IconComponent, {\n className: \"h-6 w-6 mr-2\"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h2\", {\n className: \"text-2xl font-light\"\n }, title), tag && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"ml-4 flex items-center space-x-2\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n className: \"flex items-center space-x-1 px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600\",\n onClick: handleRemoveTag\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_8__[\"default\"], {\n className: \"h-4 w-4 text-gray-500 dark:text-gray-300\"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"span\", {\n className: \"text-xs text-gray-700 dark:text-gray-300\"\n }, capitalize(tag)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_9__[\"default\"], {\n className: \"h-4 w-4 text-gray-500 dark:text-gray-300 hover:text-red-500\"\n })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"relative inline-block text-left\",\n ref: dropdownRef\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n type: \"button\",\n className: \"inline-flex justify-center w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none\",\n id: \"menu-button\",\n \"aria-expanded\": dropdownOpen,\n \"aria-haspopup\": \"true\",\n onClick: function onClick() {\n return setDropdownOpen(!dropdownOpen);\n }\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_10__[\"default\"], {\n className: \"h-5 w-5 text-gray-500 mr-2\"\n }), \" \", t(\"sort.\".concat(orderBy.split(\":\")[0]), capitalize(orderBy.split(\":\")[0].replace(\"_\", \" \"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_11__[\"default\"], {\n className: \"h-5 w-5 ml-2 text-gray-500 dark:text-gray-300\"\n })), dropdownOpen && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"origin-top-right absolute left-0 sm:right-0 sm:left-auto mt-2 w-full sm:w-56 max-w-full rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10\",\n role: \"menu\",\n \"aria-orientation\": \"vertical\",\n \"aria-labelledby\": \"menu-button\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"py-1 max-h-60 overflow-y-auto\",\n role: \"none\"\n }, [\"due_date:asc\", \"name:asc\", \"priority:desc\", \"status:desc\", \"created_at:desc\"].map(function (order) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n key: order,\n onClick: function onClick() {\n return handleSortChange(order);\n },\n className: \"block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left\",\n role: \"menuitem\"\n }, t(\"sort.\".concat(order.split(\":\")[0]), capitalize(order.split(\":\")[0].replace(\"_\", \" \"))));\n }))))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", {\n className: \"mb-6 text-sm text-gray-500 dark:text-gray-400\"\n }, description), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"mb-4\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n className: \"flex items-center bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm p-2\"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_heroicons_react_24_solid__WEBPACK_IMPORTED_MODULE_12__[\"default\"], {\n className: \"h-5 w-5 text-gray-500 dark:text-gray-400 mr-2\"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n placeholder: getSearchPlaceholder(i18n.language),\n value: taskSearchQuery,\n onChange: function onChange(e) {\n return setTaskSearchQuery(e.target.value);\n },\n className: \"w-full bg-transparent border-none focus:ring-0 focus:outline-none dark:text-white\"\n }))), loading ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", null, t('common.loading', 'Loading...')) : error ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", {\n className: \"text-red-500\"\n }, error) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, isNewTaskAllowed() && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_Task_NewTask__WEBPACK_IMPORTED_MODULE_3__[\"default\"], {\n onTaskCreate: (/*#__PURE__*/function () {\n var _ref7 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee5(taskName) {\n return _regeneratorRuntime().wrap(function _callee5$(_context5) {\n while (1) switch (_context5.prev = _context5.next) {\n case 0:\n _context5.next = 2;\n return handleTaskCreate({\n name: taskName,\n status: \"not_started\"\n });\n case 2:\n return _context5.abrupt(\"return\", _context5.sent);\n case 3:\n case \"end\":\n return _context5.stop();\n }\n }, _callee5);\n }));\n return function (_x4) {\n return _ref7.apply(this, arguments);\n };\n }())\n }), filteredTasks.length > 0 ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_Task_TaskList__WEBPACK_IMPORTED_MODULE_2__[\"default\"], {\n tasks: filteredTasks,\n onTaskCreate: handleTaskCreate,\n onTaskUpdate: handleTaskUpdate,\n onTaskDelete: handleTaskDelete,\n projects: projects\n }) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", {\n className: \"text-gray-500 text-center mt-4\"\n }, t('tasks.noTasksAvailable', 'Δεν υπάρχουν διαθέσιμες εργασίες.')))));\n};\n_s(Tasks, \"5Z8QLe2HIWOxwpw6vPkauZx5154=\", false, function () {\n return [react_i18next__WEBPACK_IMPORTED_MODULE_1__.useTranslation, react_router_dom__WEBPACK_IMPORTED_MODULE_7__.useLocation, react_router_dom__WEBPACK_IMPORTED_MODULE_7__.useNavigate];\n});\n_c = Tasks;\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Tasks);\nvar _c;\n__webpack_require__.$Refresh$.register(_c, \"Tasks\");\n\nconst $ReactRefreshModuleId$ = __webpack_require__.$Refresh$.moduleId;\nconst $ReactRefreshCurrentExports$ = __react_refresh_utils__.getModuleExports(\n\t$ReactRefreshModuleId$\n);\n\nfunction $ReactRefreshModuleRuntime$(exports) {\n\tif (true) {\n\t\tlet errorOverlay;\n\t\tif (typeof __react_refresh_error_overlay__ !== 'undefined') {\n\t\t\terrorOverlay = __react_refresh_error_overlay__;\n\t\t}\n\t\tlet testMode;\n\t\tif (typeof __react_refresh_test__ !== 'undefined') {\n\t\t\ttestMode = __react_refresh_test__;\n\t\t}\n\t\treturn __react_refresh_utils__.executeRuntime(\n\t\t\texports,\n\t\t\t$ReactRefreshModuleId$,\n\t\t\tmodule.hot,\n\t\t\terrorOverlay,\n\t\t\ttestMode\n\t\t);\n\t}\n}\n\nif (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Promise) {\n\t$ReactRefreshCurrentExports$.then($ReactRefreshModuleRuntime$);\n} else {\n\t$ReactRefreshModuleRuntime$($ReactRefreshCurrentExports$);\n}\n\n//# sourceURL=webpack://tududi/./frontend/components/Tasks.tsx?");
-
-/***/ }),
-
-/***/ "./node_modules/@heroicons/react/24/outline/esm/ArrowRightIcon.js":
-/*!************************************************************************!*\
- !*** ./node_modules/@heroicons/react/24/outline/esm/ArrowRightIcon.js ***!
- \************************************************************************/
-/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nfunction ArrowRightIcon({\n title,\n titleId,\n ...props\n}, svgRef) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"svg\", Object.assign({\n xmlns: \"http://www.w3.org/2000/svg\",\n fill: \"none\",\n viewBox: \"0 0 24 24\",\n strokeWidth: 1.5,\n stroke: \"currentColor\",\n \"aria-hidden\": \"true\",\n \"data-slot\": \"icon\",\n ref: svgRef,\n \"aria-labelledby\": titleId\n }, props), title ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"title\", {\n id: titleId\n }, title) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"path\", {\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n d: \"M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3\"\n }));\n}\nconst ForwardRef = /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.forwardRef(ArrowRightIcon);\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ForwardRef);\n\n//# sourceURL=webpack://tududi/./node_modules/@heroicons/react/24/outline/esm/ArrowRightIcon.js?");
-
-/***/ }),
-
-/***/ "./node_modules/@heroicons/react/24/outline/esm/Bars4Icon.js":
-/*!*******************************************************************!*\
- !*** ./node_modules/@heroicons/react/24/outline/esm/Bars4Icon.js ***!
- \*******************************************************************/
-/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nfunction Bars4Icon({\n title,\n titleId,\n ...props\n}, svgRef) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"svg\", Object.assign({\n xmlns: \"http://www.w3.org/2000/svg\",\n fill: \"none\",\n viewBox: \"0 0 24 24\",\n strokeWidth: 1.5,\n stroke: \"currentColor\",\n \"aria-hidden\": \"true\",\n \"data-slot\": \"icon\",\n ref: svgRef,\n \"aria-labelledby\": titleId\n }, props), title ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"title\", {\n id: titleId\n }, title) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"path\", {\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n d: \"M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5\"\n }));\n}\nconst ForwardRef = /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.forwardRef(Bars4Icon);\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ForwardRef);\n\n//# sourceURL=webpack://tududi/./node_modules/@heroicons/react/24/outline/esm/Bars4Icon.js?");
-
-/***/ }),
-
-/***/ "./node_modules/@heroicons/react/24/outline/esm/CalendarIcon.js":
-/*!**********************************************************************!*\
- !*** ./node_modules/@heroicons/react/24/outline/esm/CalendarIcon.js ***!
- \**********************************************************************/
-/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nfunction CalendarIcon({\n title,\n titleId,\n ...props\n}, svgRef) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"svg\", Object.assign({\n xmlns: \"http://www.w3.org/2000/svg\",\n fill: \"none\",\n viewBox: \"0 0 24 24\",\n strokeWidth: 1.5,\n stroke: \"currentColor\",\n \"aria-hidden\": \"true\",\n \"data-slot\": \"icon\",\n ref: svgRef,\n \"aria-labelledby\": titleId\n }, props), title ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"title\", {\n id: titleId\n }, title) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"path\", {\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n d: \"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5\"\n }));\n}\nconst ForwardRef = /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.forwardRef(CalendarIcon);\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ForwardRef);\n\n//# sourceURL=webpack://tududi/./node_modules/@heroicons/react/24/outline/esm/CalendarIcon.js?");
-
-/***/ }),
-
-/***/ "./node_modules/@heroicons/react/24/solid/esm/ChevronDoubleDownIcon.js":
-/*!*****************************************************************************!*\
- !*** ./node_modules/@heroicons/react/24/solid/esm/ChevronDoubleDownIcon.js ***!
- \*****************************************************************************/
-/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nfunction ChevronDoubleDownIcon({\n title,\n titleId,\n ...props\n}, svgRef) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"svg\", Object.assign({\n xmlns: \"http://www.w3.org/2000/svg\",\n viewBox: \"0 0 24 24\",\n fill: \"currentColor\",\n \"aria-hidden\": \"true\",\n \"data-slot\": \"icon\",\n ref: svgRef,\n \"aria-labelledby\": titleId\n }, props), title ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"title\", {\n id: titleId\n }, title) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"path\", {\n fillRule: \"evenodd\",\n d: \"M11.47 13.28a.75.75 0 0 0 1.06 0l7.5-7.5a.75.75 0 0 0-1.06-1.06L12 11.69 5.03 4.72a.75.75 0 0 0-1.06 1.06l7.5 7.5Z\",\n clipRule: \"evenodd\"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"path\", {\n fillRule: \"evenodd\",\n d: \"M11.47 19.28a.75.75 0 0 0 1.06 0l7.5-7.5a.75.75 0 1 0-1.06-1.06L12 17.69l-6.97-6.97a.75.75 0 0 0-1.06 1.06l7.5 7.5Z\",\n clipRule: \"evenodd\"\n }));\n}\nconst ForwardRef = /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.forwardRef(ChevronDoubleDownIcon);\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ForwardRef);\n\n//# sourceURL=webpack://tududi/./node_modules/@heroicons/react/24/solid/esm/ChevronDoubleDownIcon.js?");
-
-/***/ }),
-
-/***/ "./node_modules/@heroicons/react/24/solid/esm/ChevronDownIcon.js":
-/*!***********************************************************************!*\
- !*** ./node_modules/@heroicons/react/24/solid/esm/ChevronDownIcon.js ***!
- \***********************************************************************/
-/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
-
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nfunction ChevronDownIcon({\n title,\n titleId,\n ...props\n}, svgRef) {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"svg\", Object.assign({\n xmlns: \"http://www.w3.org/2000/svg\",\n viewBox: \"0 0 24 24\",\n fill: \"currentColor\",\n \"aria-hidden\": \"true\",\n \"data-slot\": \"icon\",\n ref: svgRef,\n \"aria-labelledby\": titleId\n }, props), title ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"title\", {\n id: titleId\n }, title) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__.createElement(\"path\", {\n fillRule: \"evenodd\",\n d: \"M12.53 16.28a.75.75 0 0 1-1.06 0l-7.5-7.5a.75.75 0 0 1 1.06-1.06L12 14.69l6.97-6.97a.75.75 0 1 1 1.06 1.06l-7.5 7.5Z\",\n clipRule: \"evenodd\"\n }));\n}\nconst ForwardRef = /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.forwardRef(ChevronDownIcon);\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ForwardRef);\n\n//# sourceURL=webpack://tududi/./node_modules/@heroicons/react/24/solid/esm/ChevronDownIcon.js?");
-
-/***/ })
-
-}]);
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
deleted file mode 100644
index 85b2881..0000000
--- a/dist/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
- Tududi
-
-
-
-
-
\ No newline at end of file
diff --git a/dist/main.5904574f25ca7c8b87ba.js b/dist/main.5904574f25ca7c8b87ba.js
deleted file mode 100644
index b1e143c..0000000
--- a/dist/main.5904574f25ca7c8b87ba.js
+++ /dev/null
@@ -1,5392 +0,0 @@
-/*
- * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
- * This devtool is neither made for production nor for readable output files.
- * It uses "eval()" calls to create a separate source file in the browser devtools.
- * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
- * or disable the default devtool with "devtool: false".
- * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
- */
-/******/ (() => { // webpackBootstrap
-/******/ var __webpack_modules__ = ({
-
-/***/ "./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js":
-/*!***************************************************************************************!*\
- !*** ./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js ***!
- \***************************************************************************************/
-/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
-
-eval("/* global __webpack_require__ */\nvar Refresh = __webpack_require__(/*! react-refresh/runtime */ \"./node_modules/react-refresh/runtime.js\");\n\n/**\n * Extracts exports from a webpack module object.\n * @param {string} moduleId A Webpack module ID.\n * @returns {*} An exports object from the module.\n */\nfunction getModuleExports(moduleId) {\n if (typeof moduleId === 'undefined') {\n // `moduleId` is unavailable, which indicates that this module is not in the cache,\n // which means we won't be able to capture any exports,\n // and thus they cannot be refreshed safely.\n // These are likely runtime or dynamically generated modules.\n return {};\n }\n\n var maybeModule = __webpack_require__.c[moduleId];\n if (typeof maybeModule === 'undefined') {\n // `moduleId` is available but the module in cache is unavailable,\n // which indicates the module is somehow corrupted (e.g. broken Webpacak `module` globals).\n // We will warn the user (as this is likely a mistake) and assume they cannot be refreshed.\n console.warn('[React Refresh] Failed to get exports for module: ' + moduleId + '.');\n return {};\n }\n\n var exportsOrPromise = maybeModule.exports;\n if (typeof Promise !== 'undefined' && exportsOrPromise instanceof Promise) {\n return exportsOrPromise.then(function (exports) {\n return exports;\n });\n }\n return exportsOrPromise;\n}\n\n/**\n * Calculates the signature of a React refresh boundary.\n * If this signature changes, it's unsafe to accept the boundary.\n *\n * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/907d6af22ac6ebe58572be418e9253a90665ecbd/packages/metro/src/lib/polyfills/require.js#L795-L816).\n * @param {*} moduleExports A Webpack module exports object.\n * @returns {string[]} A React refresh boundary signature array.\n */\nfunction getReactRefreshBoundarySignature(moduleExports) {\n var signature = [];\n signature.push(Refresh.getFamilyByType(moduleExports));\n\n if (moduleExports == null || typeof moduleExports !== 'object') {\n // Exit if we can't iterate over exports.\n return signature;\n }\n\n for (var key in moduleExports) {\n if (key === '__esModule') {\n continue;\n }\n\n signature.push(key);\n signature.push(Refresh.getFamilyByType(moduleExports[key]));\n }\n\n return signature;\n}\n\n/**\n * Creates a data object to be retained across refreshes.\n * This object should not transtively reference previous exports,\n * which can form infinite chain of objects across refreshes, which can pressure RAM.\n *\n * @param {*} moduleExports A Webpack module exports object.\n * @returns {*} A React refresh boundary signature array.\n */\nfunction getWebpackHotData(moduleExports) {\n return {\n signature: getReactRefreshBoundarySignature(moduleExports),\n isReactRefreshBoundary: isReactRefreshBoundary(moduleExports),\n };\n}\n\n/**\n * Creates a helper that performs a delayed React refresh.\n * @returns {function(function(): void): void} A debounced React refresh function.\n */\nfunction createDebounceUpdate() {\n /**\n * A cached setTimeout handler.\n * @type {number | undefined}\n */\n var refreshTimeout;\n\n /**\n * Performs react refresh on a delay and clears the error overlay.\n * @param {function(): void} callback\n * @returns {void}\n */\n function enqueueUpdate(callback) {\n if (typeof refreshTimeout === 'undefined') {\n refreshTimeout = setTimeout(function () {\n refreshTimeout = undefined;\n Refresh.performReactRefresh();\n callback();\n }, 30);\n }\n }\n\n return enqueueUpdate;\n}\n\n/**\n * Checks if all exports are likely a React component.\n *\n * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L748-L774).\n * @param {*} moduleExports A Webpack module exports object.\n * @returns {boolean} Whether the exports are React component like.\n */\nfunction isReactRefreshBoundary(moduleExports) {\n if (Refresh.isLikelyComponentType(moduleExports)) {\n return true;\n }\n if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') {\n // Exit if we can't iterate over exports.\n return false;\n }\n\n var hasExports = false;\n var areAllExportsComponents = true;\n for (var key in moduleExports) {\n hasExports = true;\n\n // This is the ES Module indicator flag\n if (key === '__esModule') {\n continue;\n }\n\n // We can (and have to) safely execute getters here,\n // as Webpack manually assigns harmony exports to getters,\n // without any side-effects attached.\n // Ref: https://github.com/webpack/webpack/blob/b93048643fe74de2a6931755911da1212df55897/lib/MainTemplate.js#L281\n var exportValue = moduleExports[key];\n if (!Refresh.isLikelyComponentType(exportValue)) {\n areAllExportsComponents = false;\n }\n }\n\n return hasExports && areAllExportsComponents;\n}\n\n/**\n * Checks if exports are likely a React component and registers them.\n *\n * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L818-L835).\n * @param {*} moduleExports A Webpack module exports object.\n * @param {string} moduleId A Webpack module ID.\n * @returns {void}\n */\nfunction registerExportsForReactRefresh(moduleExports, moduleId) {\n if (Refresh.isLikelyComponentType(moduleExports)) {\n // Register module.exports if it is likely a component\n Refresh.register(moduleExports, moduleId + ' %exports%');\n }\n\n if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') {\n // Exit if we can't iterate over the exports.\n return;\n }\n\n for (var key in moduleExports) {\n // Skip registering the ES Module indicator\n if (key === '__esModule') {\n continue;\n }\n\n var exportValue = moduleExports[key];\n if (Refresh.isLikelyComponentType(exportValue)) {\n var typeID = moduleId + ' %exports% ' + key;\n Refresh.register(exportValue, typeID);\n }\n }\n}\n\n/**\n * Compares previous and next module objects to check for mutated boundaries.\n *\n * This implementation is based on the one in [Metro](https://github.com/facebook/metro/blob/907d6af22ac6ebe58572be418e9253a90665ecbd/packages/metro/src/lib/polyfills/require.js#L776-L792).\n * @param {*} prevSignature The signature of the current Webpack module exports object.\n * @param {*} nextSignature The signature of the next Webpack module exports object.\n * @returns {boolean} Whether the React refresh boundary should be invalidated.\n */\nfunction shouldInvalidateReactRefreshBoundary(prevSignature, nextSignature) {\n if (prevSignature.length !== nextSignature.length) {\n return true;\n }\n\n for (var i = 0; i < nextSignature.length; i += 1) {\n if (prevSignature[i] !== nextSignature[i]) {\n return true;\n }\n }\n\n return false;\n}\n\nvar enqueueUpdate = createDebounceUpdate();\nfunction executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isTest) {\n registerExportsForReactRefresh(moduleExports, moduleId);\n\n if (webpackHot) {\n var isHotUpdate = !!webpackHot.data;\n var prevData;\n if (isHotUpdate) {\n prevData = webpackHot.data.prevData;\n }\n\n if (isReactRefreshBoundary(moduleExports)) {\n webpackHot.dispose(\n /**\n * A callback to performs a full refresh if React has unrecoverable errors,\n * and also caches the to-be-disposed module.\n * @param {*} data A hot module data object from Webpack HMR.\n * @returns {void}\n */\n function hotDisposeCallback(data) {\n // We have to mutate the data object to get data registered and cached\n data.prevData = getWebpackHotData(moduleExports);\n }\n );\n webpackHot.accept(\n /**\n * An error handler to allow self-recovering behaviours.\n * @param {Error} error An error occurred during evaluation of a module.\n * @returns {void}\n */\n function hotErrorHandler(error) {\n if (typeof refreshOverlay !== 'undefined' && refreshOverlay) {\n refreshOverlay.handleRuntimeError(error);\n }\n\n if (typeof isTest !== 'undefined' && isTest) {\n if (window.onHotAcceptError) {\n window.onHotAcceptError(error.message);\n }\n }\n\n __webpack_require__.c[moduleId].hot.accept(hotErrorHandler);\n }\n );\n\n if (isHotUpdate) {\n if (\n prevData &&\n prevData.isReactRefreshBoundary &&\n shouldInvalidateReactRefreshBoundary(\n prevData.signature,\n getReactRefreshBoundarySignature(moduleExports)\n )\n ) {\n webpackHot.invalidate();\n } else {\n enqueueUpdate(\n /**\n * A function to dismiss the error overlay after performing React refresh.\n * @returns {void}\n */\n function updateCallback() {\n if (typeof refreshOverlay !== 'undefined' && refreshOverlay) {\n refreshOverlay.clearRuntimeErrors();\n }\n }\n );\n }\n }\n } else {\n if (isHotUpdate && typeof prevData !== 'undefined') {\n webpackHot.invalidate();\n }\n }\n }\n}\n\nmodule.exports = Object.freeze({\n enqueueUpdate: enqueueUpdate,\n executeRuntime: executeRuntime,\n getModuleExports: getModuleExports,\n isReactRefreshBoundary: isReactRefreshBoundary,\n registerExportsForReactRefresh: registerExportsForReactRefresh,\n});\n\n\n//# sourceURL=webpack://tududi/./node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js?");
-
-/***/ }),
-
-/***/ "./node_modules/@remix-run/router/dist/router.js":
-/*!*******************************************************!*\
- !*** ./node_modules/@remix-run/router/dist/router.js ***!
- \*******************************************************/
-/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
-
-"use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ AbortedDeferredError: () => (/* binding */ AbortedDeferredError),\n/* harmony export */ Action: () => (/* binding */ Action),\n/* harmony export */ IDLE_BLOCKER: () => (/* binding */ IDLE_BLOCKER),\n/* harmony export */ IDLE_FETCHER: () => (/* binding */ IDLE_FETCHER),\n/* harmony export */ IDLE_NAVIGATION: () => (/* binding */ IDLE_NAVIGATION),\n/* harmony export */ UNSAFE_DEFERRED_SYMBOL: () => (/* binding */ UNSAFE_DEFERRED_SYMBOL),\n/* harmony export */ UNSAFE_DeferredData: () => (/* binding */ DeferredData),\n/* harmony export */ UNSAFE_ErrorResponseImpl: () => (/* binding */ ErrorResponseImpl),\n/* harmony export */ UNSAFE_convertRouteMatchToUiMatch: () => (/* binding */ convertRouteMatchToUiMatch),\n/* harmony export */ UNSAFE_convertRoutesToDataRoutes: () => (/* binding */ convertRoutesToDataRoutes),\n/* harmony export */ UNSAFE_decodePath: () => (/* binding */ decodePath),\n/* harmony export */ UNSAFE_getResolveToMatches: () => (/* binding */ getResolveToMatches),\n/* harmony export */ UNSAFE_invariant: () => (/* binding */ invariant),\n/* harmony export */ UNSAFE_warning: () => (/* binding */ warning),\n/* harmony export */ createBrowserHistory: () => (/* binding */ createBrowserHistory),\n/* harmony export */ createHashHistory: () => (/* binding */ createHashHistory),\n/* harmony export */ createMemoryHistory: () => (/* binding */ createMemoryHistory),\n/* harmony export */ createPath: () => (/* binding */ createPath),\n/* harmony export */ createRouter: () => (/* binding */ createRouter),\n/* harmony export */ createStaticHandler: () => (/* binding */ createStaticHandler),\n/* harmony export */ defer: () => (/* binding */ defer),\n/* harmony export */ generatePath: () => (/* binding */ generatePath),\n/* harmony export */ getStaticContextFromError: () => (/* binding */ getStaticContextFromError),\n/* harmony export */ getToPathname: () => (/* binding */ getToPathname),\n/* harmony export */ isDataWithResponseInit: () => (/* binding */ isDataWithResponseInit),\n/* harmony export */ isDeferredData: () => (/* binding */ isDeferredData),\n/* harmony export */ isRouteErrorResponse: () => (/* binding */ isRouteErrorResponse),\n/* harmony export */ joinPaths: () => (/* binding */ joinPaths),\n/* harmony export */ json: () => (/* binding */ json),\n/* harmony export */ matchPath: () => (/* binding */ matchPath),\n/* harmony export */ matchRoutes: () => (/* binding */ matchRoutes),\n/* harmony export */ normalizePathname: () => (/* binding */ normalizePathname),\n/* harmony export */ parsePath: () => (/* binding */ parsePath),\n/* harmony export */ redirect: () => (/* binding */ redirect),\n/* harmony export */ redirectDocument: () => (/* binding */ redirectDocument),\n/* harmony export */ replace: () => (/* binding */ replace),\n/* harmony export */ resolvePath: () => (/* binding */ resolvePath),\n/* harmony export */ resolveTo: () => (/* binding */ resolveTo),\n/* harmony export */ stripBasename: () => (/* binding */ stripBasename),\n/* harmony export */ unstable_data: () => (/* binding */ data)\n/* harmony export */ });\n/**\n * @remix-run/router v1.19.2\n *\n * Copyright (c) Remix Software Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE.md file in the root directory of this source tree.\n *\n * @license MIT\n */\nfunction _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n };\n return _extends.apply(this, arguments);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Types and Constants\n////////////////////////////////////////////////////////////////////////////////\n/**\n * Actions represent the type of change to a location value.\n */\nvar Action;\n(function (Action) {\n /**\n * A POP indicates a change to an arbitrary index in the history stack, such\n * as a back or forward navigation. It does not describe the direction of the\n * navigation, only that the current index changed.\n *\n * Note: This is the default action for newly created history objects.\n */\n Action[\"Pop\"] = \"POP\";\n /**\n * A PUSH indicates a new entry being added to the history stack, such as when\n * a link is clicked and a new page loads. When this happens, all subsequent\n * entries in the stack are lost.\n */\n Action[\"Push\"] = \"PUSH\";\n /**\n * A REPLACE indicates the entry at the current index in the history stack\n * being replaced by a new one.\n */\n Action[\"Replace\"] = \"REPLACE\";\n})(Action || (Action = {}));\nconst PopStateEventType = \"popstate\";\n/**\n * Memory history stores the current location in memory. It is designed for use\n * in stateful non-browser environments like tests and React Native.\n */\nfunction createMemoryHistory(options) {\n if (options === void 0) {\n options = {};\n }\n let {\n initialEntries = [\"/\"],\n initialIndex,\n v5Compat = false\n } = options;\n let entries; // Declare so we can access from createMemoryLocation\n entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === \"string\" ? null : entry.state, index === 0 ? \"default\" : undefined));\n let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);\n let action = Action.Pop;\n let listener = null;\n function clampIndex(n) {\n return Math.min(Math.max(n, 0), entries.length - 1);\n }\n function getCurrentLocation() {\n return entries[index];\n }\n function createMemoryLocation(to, state, key) {\n if (state === void 0) {\n state = null;\n }\n let location = createLocation(entries ? getCurrentLocation().pathname : \"/\", to, state, key);\n warning(location.pathname.charAt(0) === \"/\", \"relative pathnames are not supported in memory history: \" + JSON.stringify(to));\n return location;\n }\n function createHref(to) {\n return typeof to === \"string\" ? to : createPath(to);\n }\n let history = {\n get index() {\n return index;\n },\n get action() {\n return action;\n },\n get location() {\n return getCurrentLocation();\n },\n createHref,\n createURL(to) {\n return new URL(createHref(to), \"http://localhost\");\n },\n encodeLocation(to) {\n let path = typeof to === \"string\" ? parsePath(to) : to;\n return {\n pathname: path.pathname || \"\",\n search: path.search || \"\",\n hash: path.hash || \"\"\n };\n },\n push(to, state) {\n action = Action.Push;\n let nextLocation = createMemoryLocation(to, state);\n index += 1;\n entries.splice(index, entries.length, nextLocation);\n if (v5Compat && listener) {\n listener({\n action,\n location: nextLocation,\n delta: 1\n });\n }\n },\n replace(to, state) {\n action = Action.Replace;\n let nextLocation = createMemoryLocation(to, state);\n entries[index] = nextLocation;\n if (v5Compat && listener) {\n listener({\n action,\n location: nextLocation,\n delta: 0\n });\n }\n },\n go(delta) {\n action = Action.Pop;\n let nextIndex = clampIndex(index + delta);\n let nextLocation = entries[nextIndex];\n index = nextIndex;\n if (listener) {\n listener({\n action,\n location: nextLocation,\n delta\n });\n }\n },\n listen(fn) {\n listener = fn;\n return () => {\n listener = null;\n };\n }\n };\n return history;\n}\n/**\n * Browser history stores the location in regular URLs. This is the standard for\n * most web apps, but it requires some configuration on the server to ensure you\n * serve the same app at multiple URLs.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory\n */\nfunction createBrowserHistory(options) {\n if (options === void 0) {\n options = {};\n }\n function createBrowserLocation(window, globalHistory) {\n let {\n pathname,\n search,\n hash\n } = window.location;\n return createLocation(\"\", {\n pathname,\n search,\n hash\n },\n // state defaults to `null` because `window.history.state` does\n globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || \"default\");\n }\n function createBrowserHref(window, to) {\n return typeof to === \"string\" ? to : createPath(to);\n }\n return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);\n}\n/**\n * Hash history stores the location in window.location.hash. This makes it ideal\n * for situations where you don't want to send the location to the server for\n * some reason, either because you do cannot configure it or the URL space is\n * reserved for something else.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory\n */\nfunction createHashHistory(options) {\n if (options === void 0) {\n options = {};\n }\n function createHashLocation(window, globalHistory) {\n let {\n pathname = \"/\",\n search = \"\",\n hash = \"\"\n } = parsePath(window.location.hash.substr(1));\n // Hash URL should always have a leading / just like window.location.pathname\n // does, so if an app ends up at a route like /#something then we add a\n // leading slash so all of our path-matching behaves the same as if it would\n // in a browser router. This is particularly important when there exists a\n // root splat route () since that matches internally against\n // \"/*\" and we'd expect /#something to 404 in a hash router app.\n if (!pathname.startsWith(\"/\") && !pathname.startsWith(\".\")) {\n pathname = \"/\" + pathname;\n }\n return createLocation(\"\", {\n pathname,\n search,\n hash\n },\n // state defaults to `null` because `window.history.state` does\n globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || \"default\");\n }\n function createHashHref(window, to) {\n let base = window.document.querySelector(\"base\");\n let href = \"\";\n if (base && base.getAttribute(\"href\")) {\n let url = window.location.href;\n let hashIndex = url.indexOf(\"#\");\n href = hashIndex === -1 ? url : url.slice(0, hashIndex);\n }\n return href + \"#\" + (typeof to === \"string\" ? to : createPath(to));\n }\n function validateHashLocation(location, to) {\n warning(location.pathname.charAt(0) === \"/\", \"relative pathnames are not supported in hash history.push(\" + JSON.stringify(to) + \")\");\n }\n return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);\n}\nfunction invariant(value, message) {\n if (value === false || value === null || typeof value === \"undefined\") {\n throw new Error(message);\n }\n}\nfunction warning(cond, message) {\n if (!cond) {\n // eslint-disable-next-line no-console\n if (typeof console !== \"undefined\") console.warn(message);\n try {\n // Welcome to debugging history!\n //\n // This error is thrown as a convenience, so you can more easily\n // find the source for a warning that appears in the console by\n // enabling \"pause on exceptions\" in your JavaScript debugger.\n throw new Error(message);\n // eslint-disable-next-line no-empty\n } catch (e) {}\n }\n}\nfunction createKey() {\n return Math.random().toString(36).substr(2, 8);\n}\n/**\n * For browser-based histories, we combine the state and key into an object\n */\nfunction getHistoryState(location, index) {\n return {\n usr: location.state,\n key: location.key,\n idx: index\n };\n}\n/**\n * Creates a Location object with a unique key from the given Path\n */\nfunction createLocation(current, to, state, key) {\n if (state === void 0) {\n state = null;\n }\n let location = _extends({\n pathname: typeof current === \"string\" ? current : current.pathname,\n search: \"\",\n hash: \"\"\n }, typeof to === \"string\" ? parsePath(to) : to, {\n state,\n // TODO: This could be cleaned up. push/replace should probably just take\n // full Locations now and avoid the need to run through this flow at all\n // But that's a pretty big refactor to the current test suite so going to\n // keep as is for the time being and just let any incoming keys take precedence\n key: to && to.key || key || createKey()\n });\n return location;\n}\n/**\n * Creates a string URL path from the given pathname, search, and hash components.\n */\nfunction createPath(_ref) {\n let {\n pathname = \"/\",\n search = \"\",\n hash = \"\"\n } = _ref;\n if (search && search !== \"?\") pathname += search.charAt(0) === \"?\" ? search : \"?\" + search;\n if (hash && hash !== \"#\") pathname += hash.charAt(0) === \"#\" ? hash : \"#\" + hash;\n return pathname;\n}\n/**\n * Parses a string URL path into its separate pathname, search, and hash components.\n */\nfunction parsePath(path) {\n let parsedPath = {};\n if (path) {\n let hashIndex = path.indexOf(\"#\");\n if (hashIndex >= 0) {\n parsedPath.hash = path.substr(hashIndex);\n path = path.substr(0, hashIndex);\n }\n let searchIndex = path.indexOf(\"?\");\n if (searchIndex >= 0) {\n parsedPath.search = path.substr(searchIndex);\n path = path.substr(0, searchIndex);\n }\n if (path) {\n parsedPath.pathname = path;\n }\n }\n return parsedPath;\n}\nfunction getUrlBasedHistory(getLocation, createHref, validateLocation, options) {\n if (options === void 0) {\n options = {};\n }\n let {\n window = document.defaultView,\n v5Compat = false\n } = options;\n let globalHistory = window.history;\n let action = Action.Pop;\n let listener = null;\n let index = getIndex();\n // Index should only be null when we initialize. If not, it's because the\n // user called history.pushState or history.replaceState directly, in which\n // case we should log a warning as it will result in bugs.\n if (index == null) {\n index = 0;\n globalHistory.replaceState(_extends({}, globalHistory.state, {\n idx: index\n }), \"\");\n }\n function getIndex() {\n let state = globalHistory.state || {\n idx: null\n };\n return state.idx;\n }\n function handlePop() {\n action = Action.Pop;\n let nextIndex = getIndex();\n let delta = nextIndex == null ? null : nextIndex - index;\n index = nextIndex;\n if (listener) {\n listener({\n action,\n location: history.location,\n delta\n });\n }\n }\n function push(to, state) {\n action = Action.Push;\n let location = createLocation(history.location, to, state);\n if (validateLocation) validateLocation(location, to);\n index = getIndex() + 1;\n let historyState = getHistoryState(location, index);\n let url = history.createHref(location);\n // try...catch because iOS limits us to 100 pushState calls :/\n try {\n globalHistory.pushState(historyState, \"\", url);\n } catch (error) {\n // If the exception is because `state` can't be serialized, let that throw\n // outwards just like a replace call would so the dev knows the cause\n // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps\n // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal\n if (error instanceof DOMException && error.name === \"DataCloneError\") {\n throw error;\n }\n // They are going to lose state here, but there is no real\n // way to warn them about it since the page will refresh...\n window.location.assign(url);\n }\n if (v5Compat && listener) {\n listener({\n action,\n location: history.location,\n delta: 1\n });\n }\n }\n function replace(to, state) {\n action = Action.Replace;\n let location = createLocation(history.location, to, state);\n if (validateLocation) validateLocation(location, to);\n index = getIndex();\n let historyState = getHistoryState(location, index);\n let url = history.createHref(location);\n globalHistory.replaceState(historyState, \"\", url);\n if (v5Compat && listener) {\n listener({\n action,\n location: history.location,\n delta: 0\n });\n }\n }\n function createURL(to) {\n // window.location.origin is \"null\" (the literal string value) in Firefox\n // under certain conditions, notably when serving from a local HTML file\n // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297\n let base = window.location.origin !== \"null\" ? window.location.origin : window.location.href;\n let href = typeof to === \"string\" ? to : createPath(to);\n // Treating this as a full URL will strip any trailing spaces so we need to\n // pre-encode them since they might be part of a matching splat param from\n // an ancestor route\n href = href.replace(/ $/, \"%20\");\n invariant(base, \"No window.location.(origin|href) available to create URL for href: \" + href);\n return new URL(href, base);\n }\n let history = {\n get action() {\n return action;\n },\n get location() {\n return getLocation(window, globalHistory);\n },\n listen(fn) {\n if (listener) {\n throw new Error(\"A history only accepts one active listener\");\n }\n window.addEventListener(PopStateEventType, handlePop);\n listener = fn;\n return () => {\n window.removeEventListener(PopStateEventType, handlePop);\n listener = null;\n };\n },\n createHref(to) {\n return createHref(window, to);\n },\n createURL,\n encodeLocation(to) {\n // Encode a Location the same way window.location would\n let url = createURL(to);\n return {\n pathname: url.pathname,\n search: url.search,\n hash: url.hash\n };\n },\n push,\n replace,\n go(n) {\n return globalHistory.go(n);\n }\n };\n return history;\n}\n//#endregion\n\nvar ResultType;\n(function (ResultType) {\n ResultType[\"data\"] = \"data\";\n ResultType[\"deferred\"] = \"deferred\";\n ResultType[\"redirect\"] = \"redirect\";\n ResultType[\"error\"] = \"error\";\n})(ResultType || (ResultType = {}));\nconst immutableRouteKeys = new Set([\"lazy\", \"caseSensitive\", \"path\", \"id\", \"index\", \"children\"]);\nfunction isIndexRoute(route) {\n return route.index === true;\n}\n// Walk the route tree generating unique IDs where necessary, so we are working\n// solely with AgnosticDataRouteObject's within the Router\nfunction convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manifest) {\n if (parentPath === void 0) {\n parentPath = [];\n }\n if (manifest === void 0) {\n manifest = {};\n }\n return routes.map((route, index) => {\n let treePath = [...parentPath, String(index)];\n let id = typeof route.id === \"string\" ? route.id : treePath.join(\"-\");\n invariant(route.index !== true || !route.children, \"Cannot specify children on an index route\");\n invariant(!manifest[id], \"Found a route id collision on id \\\"\" + id + \"\\\". Route \" + \"id's must be globally unique within Data Router usages\");\n if (isIndexRoute(route)) {\n let indexRoute = _extends({}, route, mapRouteProperties(route), {\n id\n });\n manifest[id] = indexRoute;\n return indexRoute;\n } else {\n let pathOrLayoutRoute = _extends({}, route, mapRouteProperties(route), {\n id,\n children: undefined\n });\n manifest[id] = pathOrLayoutRoute;\n if (route.children) {\n pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest);\n }\n return pathOrLayoutRoute;\n }\n });\n}\n/**\n * Matches the given routes to a location and returns the match data.\n *\n * @see https://reactrouter.com/utils/match-routes\n */\nfunction matchRoutes(routes, locationArg, basename) {\n if (basename === void 0) {\n basename = \"/\";\n }\n return matchRoutesImpl(routes, locationArg, basename, false);\n}\nfunction matchRoutesImpl(routes, locationArg, basename, allowPartial) {\n let location = typeof locationArg === \"string\" ? parsePath(locationArg) : locationArg;\n let pathname = stripBasename(location.pathname || \"/\", basename);\n if (pathname == null) {\n return null;\n }\n let branches = flattenRoutes(routes);\n rankRouteBranches(branches);\n let matches = null;\n for (let i = 0; matches == null && i < branches.length; ++i) {\n // Incoming pathnames are generally encoded from either window.location\n // or from router.navigate, but we want to match against the unencoded\n // paths in the route definitions. Memory router locations won't be\n // encoded here but there also shouldn't be anything to decode so this\n // should be a safe operation. This avoids needing matchRoutes to be\n // history-aware.\n let decoded = decodePath(pathname);\n matches = matchRouteBranch(branches[i], decoded, allowPartial);\n }\n return matches;\n}\nfunction convertRouteMatchToUiMatch(match, loaderData) {\n let {\n route,\n pathname,\n params\n } = match;\n return {\n id: route.id,\n pathname,\n params,\n data: loaderData[route.id],\n handle: route.handle\n };\n}\nfunction flattenRoutes(routes, branches, parentsMeta, parentPath) {\n if (branches === void 0) {\n branches = [];\n }\n if (parentsMeta === void 0) {\n parentsMeta = [];\n }\n if (parentPath === void 0) {\n parentPath = \"\";\n }\n let flattenRoute = (route, index, relativePath) => {\n let meta = {\n relativePath: relativePath === undefined ? route.path || \"\" : relativePath,\n caseSensitive: route.caseSensitive === true,\n childrenIndex: index,\n route\n };\n if (meta.relativePath.startsWith(\"/\")) {\n invariant(meta.relativePath.startsWith(parentPath), \"Absolute route path \\\"\" + meta.relativePath + \"\\\" nested under path \" + (\"\\\"\" + parentPath + \"\\\" is not valid. An absolute child route path \") + \"must start with the combined path of all its parent routes.\");\n meta.relativePath = meta.relativePath.slice(parentPath.length);\n }\n let path = joinPaths([parentPath, meta.relativePath]);\n let routesMeta = parentsMeta.concat(meta);\n // Add the children before adding this route to the array, so we traverse the\n // route tree depth-first and child routes appear before their parents in\n // the \"flattened\" version.\n if (route.children && route.children.length > 0) {\n invariant(\n // Our types know better, but runtime JS may not!\n // @ts-expect-error\n route.index !== true, \"Index routes must not have child routes. Please remove \" + (\"all child routes from route path \\\"\" + path + \"\\\".\"));\n flattenRoutes(route.children, branches, routesMeta, path);\n }\n // Routes without a path shouldn't ever match by themselves unless they are\n // index routes, so don't add them to the list of possible branches.\n if (route.path == null && !route.index) {\n return;\n }\n branches.push({\n path,\n score: computeScore(path, route.index),\n routesMeta\n });\n };\n routes.forEach((route, index) => {\n var _route$path;\n // coarse-grain check for optional params\n if (route.path === \"\" || !((_route$path = route.path) != null && _route$path.includes(\"?\"))) {\n flattenRoute(route, index);\n } else {\n for (let exploded of explodeOptionalSegments(route.path)) {\n flattenRoute(route, index, exploded);\n }\n }\n });\n return branches;\n}\n/**\n * Computes all combinations of optional path segments for a given path,\n * excluding combinations that are ambiguous and of lower priority.\n *\n * For example, `/one/:two?/three/:four?/:five?` explodes to:\n * - `/one/three`\n * - `/one/:two/three`\n * - `/one/three/:four`\n * - `/one/three/:five`\n * - `/one/:two/three/:four`\n * - `/one/:two/three/:five`\n * - `/one/three/:four/:five`\n * - `/one/:two/three/:four/:five`\n */\nfunction explodeOptionalSegments(path) {\n let segments = path.split(\"/\");\n if (segments.length === 0) return [];\n let [first, ...rest] = segments;\n // Optional path segments are denoted by a trailing `?`\n let isOptional = first.endsWith(\"?\");\n // Compute the corresponding required segment: `foo?` -> `foo`\n let required = first.replace(/\\?$/, \"\");\n if (rest.length === 0) {\n // Intepret empty string as omitting an optional segment\n // `[\"one\", \"\", \"three\"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`\n return isOptional ? [required, \"\"] : [required];\n }\n let restExploded = explodeOptionalSegments(rest.join(\"/\"));\n let result = [];\n // All child paths with the prefix. Do this for all children before the\n // optional version for all children, so we get consistent ordering where the\n // parent optional aspect is preferred as required. Otherwise, we can get\n // child sections interspersed where deeper optional segments are higher than\n // parent optional segments, where for example, /:two would explode _earlier_\n // then /:one. By always including the parent as required _for all children_\n // first, we avoid this issue\n result.push(...restExploded.map(subpath => subpath === \"\" ? required : [required, subpath].join(\"/\")));\n // Then, if this is an optional value, add all child versions without\n if (isOptional) {\n result.push(...restExploded);\n }\n // for absolute paths, ensure `/` instead of empty segment\n return result.map(exploded => path.startsWith(\"/\") && exploded === \"\" ? \"/\" : exploded);\n}\nfunction rankRouteBranches(branches) {\n branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first\n : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));\n}\nconst paramRe = /^:[\\w-]+$/;\nconst dynamicSegmentValue = 3;\nconst indexRouteValue = 2;\nconst emptySegmentValue = 1;\nconst staticSegmentValue = 10;\nconst splatPenalty = -2;\nconst isSplat = s => s === \"*\";\nfunction computeScore(path, index) {\n let segments = path.split(\"/\");\n let initialScore = segments.length;\n if (segments.some(isSplat)) {\n initialScore += splatPenalty;\n }\n if (index) {\n initialScore += indexRouteValue;\n }\n return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === \"\" ? emptySegmentValue : staticSegmentValue), initialScore);\n}\nfunction compareIndexes(a, b) {\n let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);\n return siblings ?\n // If two routes are siblings, we should try to match the earlier sibling\n // first. This allows people to have fine-grained control over the matching\n // behavior by simply putting routes with identical paths in the order they\n // want them tried.\n a[a.length - 1] - b[b.length - 1] :\n // Otherwise, it doesn't really make sense to rank non-siblings by index,\n // so they sort equally.\n 0;\n}\nfunction matchRouteBranch(branch, pathname, allowPartial) {\n if (allowPartial === void 0) {\n allowPartial = false;\n }\n let {\n routesMeta\n } = branch;\n let matchedParams = {};\n let matchedPathname = \"/\";\n let matches = [];\n for (let i = 0; i < routesMeta.length; ++i) {\n let meta = routesMeta[i];\n let end = i === routesMeta.length - 1;\n let remainingPathname = matchedPathname === \"/\" ? pathname : pathname.slice(matchedPathname.length) || \"/\";\n let match = matchPath({\n path: meta.relativePath,\n caseSensitive: meta.caseSensitive,\n end\n }, remainingPathname);\n let route = meta.route;\n if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) {\n match = matchPath({\n path: meta.relativePath,\n caseSensitive: meta.caseSensitive,\n end: false\n }, remainingPathname);\n }\n if (!match) {\n return null;\n }\n Object.assign(matchedParams, match.params);\n matches.push({\n // TODO: Can this as be avoided?\n params: matchedParams,\n pathname: joinPaths([matchedPathname, match.pathname]),\n pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),\n route\n });\n if (match.pathnameBase !== \"/\") {\n matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);\n }\n }\n return matches;\n}\n/**\n * Returns a path with params interpolated.\n *\n * @see https://reactrouter.com/utils/generate-path\n */\nfunction generatePath(originalPath, params) {\n if (params === void 0) {\n params = {};\n }\n let path = originalPath;\n if (path.endsWith(\"*\") && path !== \"*\" && !path.endsWith(\"/*\")) {\n warning(false, \"Route path \\\"\" + path + \"\\\" will be treated as if it were \" + (\"\\\"\" + path.replace(/\\*$/, \"/*\") + \"\\\" because the `*` character must \") + \"always follow a `/` in the pattern. To get rid of this warning, \" + (\"please change the route path to \\\"\" + path.replace(/\\*$/, \"/*\") + \"\\\".\"));\n path = path.replace(/\\*$/, \"/*\");\n }\n // ensure `/` is added at the beginning if the path is absolute\n const prefix = path.startsWith(\"/\") ? \"/\" : \"\";\n const stringify = p => p == null ? \"\" : typeof p === \"string\" ? p : String(p);\n const segments = path.split(/\\/+/).map((segment, index, array) => {\n const isLastSegment = index === array.length - 1;\n // only apply the splat if it's the last segment\n if (isLastSegment && segment === \"*\") {\n const star = \"*\";\n // Apply the splat\n return stringify(params[star]);\n }\n const keyMatch = segment.match(/^:([\\w-]+)(\\??)$/);\n if (keyMatch) {\n const [, key, optional] = keyMatch;\n let param = params[key];\n invariant(optional === \"?\" || param != null, \"Missing \\\":\" + key + \"\\\" param\");\n return stringify(param);\n }\n // Remove any optional markers from optional static segments\n return segment.replace(/\\?$/g, \"\");\n })\n // Remove empty segments\n .filter(segment => !!segment);\n return prefix + segments.join(\"/\");\n}\n/**\n * Performs pattern matching on a URL pathname and returns information about\n * the match.\n *\n * @see https://reactrouter.com/utils/match-path\n */\nfunction matchPath(pattern, pathname) {\n if (typeof pattern === \"string\") {\n pattern = {\n path: pattern,\n caseSensitive: false,\n end: true\n };\n }\n let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);\n let match = pathname.match(matcher);\n if (!match) return null;\n let matchedPathname = match[0];\n let pathnameBase = matchedPathname.replace(/(.)\\/+$/, \"$1\");\n let captureGroups = match.slice(1);\n let params = compiledParams.reduce((memo, _ref, index) => {\n let {\n paramName,\n isOptional\n } = _ref;\n // We need to compute the pathnameBase here using the raw splat value\n // instead of using params[\"*\"] later because it will be decoded then\n if (paramName === \"*\") {\n let splatValue = captureGroups[index] || \"\";\n pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\\/+$/, \"$1\");\n }\n const value = captureGroups[index];\n if (isOptional && !value) {\n memo[paramName] = undefined;\n } else {\n memo[paramName] = (value || \"\").replace(/%2F/g, \"/\");\n }\n return memo;\n }, {});\n return {\n params,\n pathname: matchedPathname,\n pathnameBase,\n pattern\n };\n}\nfunction compilePath(path, caseSensitive, end) {\n if (caseSensitive === void 0) {\n caseSensitive = false;\n }\n if (end === void 0) {\n end = true;\n }\n warning(path === \"*\" || !path.endsWith(\"*\") || path.endsWith(\"/*\"), \"Route path \\\"\" + path + \"\\\" will be treated as if it were \" + (\"\\\"\" + path.replace(/\\*$/, \"/*\") + \"\\\" because the `*` character must \") + \"always follow a `/` in the pattern. To get rid of this warning, \" + (\"please change the route path to \\\"\" + path.replace(/\\*$/, \"/*\") + \"\\\".\"));\n let params = [];\n let regexpSource = \"^\" + path.replace(/\\/*\\*?$/, \"\") // Ignore trailing / and /*, we'll handle it below\n .replace(/^\\/*/, \"/\") // Make sure it has a leading /\n .replace(/[\\\\.*+^${}|()[\\]]/g, \"\\\\$&\") // Escape special regex chars\n .replace(/\\/:([\\w-]+)(\\?)?/g, (_, paramName, isOptional) => {\n params.push({\n paramName,\n isOptional: isOptional != null\n });\n return isOptional ? \"/?([^\\\\/]+)?\" : \"/([^\\\\/]+)\";\n });\n if (path.endsWith(\"*\")) {\n params.push({\n paramName: \"*\"\n });\n regexpSource += path === \"*\" || path === \"/*\" ? \"(.*)$\" // Already matched the initial /, just match the rest\n : \"(?:\\\\/(.+)|\\\\/*)$\"; // Don't include the / in params[\"*\"]\n } else if (end) {\n // When matching to the end, ignore trailing slashes\n regexpSource += \"\\\\/*$\";\n } else if (path !== \"\" && path !== \"/\") {\n // If our path is non-empty and contains anything beyond an initial slash,\n // then we have _some_ form of path in our regex, so we should expect to\n // match only if we find the end of this path segment. Look for an optional\n // non-captured trailing slash (to match a portion of the URL) or the end\n // of the path (if we've matched to the end). We used to do this with a\n // word boundary but that gives false positives on routes like\n // /user-preferences since `-` counts as a word boundary.\n regexpSource += \"(?:(?=\\\\/|$))\";\n } else ;\n let matcher = new RegExp(regexpSource, caseSensitive ? undefined : \"i\");\n return [matcher, params];\n}\nfunction decodePath(value) {\n try {\n return value.split(\"/\").map(v => decodeURIComponent(v).replace(/\\//g, \"%2F\")).join(\"/\");\n } catch (error) {\n warning(false, \"The URL path \\\"\" + value + \"\\\" could not be decoded because it is is a \" + \"malformed URL segment. This is probably due to a bad percent \" + (\"encoding (\" + error + \").\"));\n return value;\n }\n}\n/**\n * @private\n */\nfunction stripBasename(pathname, basename) {\n if (basename === \"/\") return pathname;\n if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {\n return null;\n }\n // We want to leave trailing slash behavior in the user's control, so if they\n // specify a basename with a trailing slash, we should support it\n let startIndex = basename.endsWith(\"/\") ? basename.length - 1 : basename.length;\n let nextChar = pathname.charAt(startIndex);\n if (nextChar && nextChar !== \"/\") {\n // pathname does not start with basename/\n return null;\n }\n return pathname.slice(startIndex) || \"/\";\n}\n/**\n * Returns a resolved path object relative to the given pathname.\n *\n * @see https://reactrouter.com/utils/resolve-path\n */\nfunction resolvePath(to, fromPathname) {\n if (fromPathname === void 0) {\n fromPathname = \"/\";\n }\n let {\n pathname: toPathname,\n search = \"\",\n hash = \"\"\n } = typeof to === \"string\" ? parsePath(to) : to;\n let pathname = toPathname ? toPathname.startsWith(\"/\") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname;\n return {\n pathname,\n search: normalizeSearch(search),\n hash: normalizeHash(hash)\n };\n}\nfunction resolvePathname(relativePath, fromPathname) {\n let segments = fromPathname.replace(/\\/+$/, \"\").split(\"/\");\n let relativeSegments = relativePath.split(\"/\");\n relativeSegments.forEach(segment => {\n if (segment === \"..\") {\n // Keep the root \"\" segment so the pathname starts at /\n if (segments.length > 1) segments.pop();\n } else if (segment !== \".\") {\n segments.push(segment);\n }\n });\n return segments.length > 1 ? segments.join(\"/\") : \"/\";\n}\nfunction getInvalidPathError(char, field, dest, path) {\n return \"Cannot include a '\" + char + \"' character in a manually specified \" + (\"`to.\" + field + \"` field [\" + JSON.stringify(path) + \"]. Please separate it out to the \") + (\"`to.\" + dest + \"` field. Alternatively you may provide the full path as \") + \"a string in and the router will parse it for you.\";\n}\n/**\n * @private\n *\n * When processing relative navigation we want to ignore ancestor routes that\n * do not contribute to the path, such that index/pathless layout routes don't\n * interfere.\n *\n * For example, when moving a route element into an index route and/or a\n * pathless layout route, relative link behavior contained within should stay\n * the same. Both of the following examples should link back to the root:\n *\n * \n * \n * \n *\n * \n * \n * }> // <-- Does not contribute\n * // <-- Does not contribute\n * \n * \n */\nfunction getPathContributingMatches(matches) {\n return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);\n}\n// Return the array of pathnames for the current route matches - used to\n// generate the routePathnames input for resolveTo()\nfunction getResolveToMatches(matches, v7_relativeSplatPath) {\n let pathMatches = getPathContributingMatches(matches);\n // When v7_relativeSplatPath is enabled, use the full pathname for the leaf\n // match so we include splat values for \".\" links. See:\n // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329\n if (v7_relativeSplatPath) {\n return pathMatches.map((match, idx) => idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase);\n }\n return pathMatches.map(match => match.pathnameBase);\n}\n/**\n * @private\n */\nfunction resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {\n if (isPathRelative === void 0) {\n isPathRelative = false;\n }\n let to;\n if (typeof toArg === \"string\") {\n to = parsePath(toArg);\n } else {\n to = _extends({}, toArg);\n invariant(!to.pathname || !to.pathname.includes(\"?\"), getInvalidPathError(\"?\", \"pathname\", \"search\", to));\n invariant(!to.pathname || !to.pathname.includes(\"#\"), getInvalidPathError(\"#\", \"pathname\", \"hash\", to));\n invariant(!to.search || !to.search.includes(\"#\"), getInvalidPathError(\"#\", \"search\", \"hash\", to));\n }\n let isEmptyPath = toArg === \"\" || to.pathname === \"\";\n let toPathname = isEmptyPath ? \"/\" : to.pathname;\n let from;\n // Routing is relative to the current pathname if explicitly requested.\n //\n // If a pathname is explicitly provided in `to`, it should be relative to the\n // route context. This is explained in `Note on `` values` in our\n // migration guide from v5 as a means of disambiguation between `to` values\n // that begin with `/` and those that do not. However, this is problematic for\n // `to` values that do not provide a pathname. `to` can simply be a search or\n // hash string, in which case we should assume that the navigation is relative\n // to the current location's pathname and *not* the route pathname.\n if (toPathname == null) {\n from = locationPathname;\n } else {\n let routePathnameIndex = routePathnames.length - 1;\n // With relative=\"route\" (the default), each leading .. segment means\n // \"go up one route\" instead of \"go up one URL segment\". This is a key\n // difference from how works and a major reason we call this a\n // \"to\" value instead of a \"href\".\n if (!isPathRelative && toPathname.startsWith(\"..\")) {\n let toSegments = toPathname.split(\"/\");\n while (toSegments[0] === \"..\") {\n toSegments.shift();\n routePathnameIndex -= 1;\n }\n to.pathname = toSegments.join(\"/\");\n }\n from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : \"/\";\n }\n let path = resolvePath(to, from);\n // Ensure the pathname has a trailing slash if the original \"to\" had one\n let hasExplicitTrailingSlash = toPathname && toPathname !== \"/\" && toPathname.endsWith(\"/\");\n // Or if this was a link to the current path which has a trailing slash\n let hasCurrentTrailingSlash = (isEmptyPath || toPathname === \".\") && locationPathname.endsWith(\"/\");\n if (!path.pathname.endsWith(\"/\") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {\n path.pathname += \"/\";\n }\n return path;\n}\n/**\n * @private\n */\nfunction getToPathname(to) {\n // Empty strings should be treated the same as / paths\n return to === \"\" || to.pathname === \"\" ? \"/\" : typeof to === \"string\" ? parsePath(to).pathname : to.pathname;\n}\n/**\n * @private\n */\nconst joinPaths = paths => paths.join(\"/\").replace(/\\/\\/+/g, \"/\");\n/**\n * @private\n */\nconst normalizePathname = pathname => pathname.replace(/\\/+$/, \"\").replace(/^\\/*/, \"/\");\n/**\n * @private\n */\nconst normalizeSearch = search => !search || search === \"?\" ? \"\" : search.startsWith(\"?\") ? search : \"?\" + search;\n/**\n * @private\n */\nconst normalizeHash = hash => !hash || hash === \"#\" ? \"\" : hash.startsWith(\"#\") ? hash : \"#\" + hash;\n/**\n * This is a shortcut for creating `application/json` responses. Converts `data`\n * to JSON and sets the `Content-Type` header.\n */\nconst json = function json(data, init) {\n if (init === void 0) {\n init = {};\n }\n let responseInit = typeof init === \"number\" ? {\n status: init\n } : init;\n let headers = new Headers(responseInit.headers);\n if (!headers.has(\"Content-Type\")) {\n headers.set(\"Content-Type\", \"application/json; charset=utf-8\");\n }\n return new Response(JSON.stringify(data), _extends({}, responseInit, {\n headers\n }));\n};\nclass DataWithResponseInit {\n constructor(data, init) {\n this.type = \"DataWithResponseInit\";\n this.data = data;\n this.init = init || null;\n }\n}\n/**\n * Create \"responses\" that contain `status`/`headers` without forcing\n * serialization into an actual `Response` - used by Remix single fetch\n */\nfunction data(data, init) {\n return new DataWithResponseInit(data, typeof init === \"number\" ? {\n status: init\n } : init);\n}\nclass AbortedDeferredError extends Error {}\nclass DeferredData {\n constructor(data, responseInit) {\n this.pendingKeysSet = new Set();\n this.subscribers = new Set();\n this.deferredKeys = [];\n invariant(data && typeof data === \"object\" && !Array.isArray(data), \"defer() only accepts plain objects\");\n // Set up an AbortController + Promise we can race against to exit early\n // cancellation\n let reject;\n this.abortPromise = new Promise((_, r) => reject = r);\n this.controller = new AbortController();\n let onAbort = () => reject(new AbortedDeferredError(\"Deferred data aborted\"));\n this.unlistenAbortSignal = () => this.controller.signal.removeEventListener(\"abort\", onAbort);\n this.controller.signal.addEventListener(\"abort\", onAbort);\n this.data = Object.entries(data).reduce((acc, _ref2) => {\n let [key, value] = _ref2;\n return Object.assign(acc, {\n [key]: this.trackPromise(key, value)\n });\n }, {});\n if (this.done) {\n // All incoming values were resolved\n this.unlistenAbortSignal();\n }\n this.init = responseInit;\n }\n trackPromise(key, value) {\n if (!(value instanceof Promise)) {\n return value;\n }\n this.deferredKeys.push(key);\n this.pendingKeysSet.add(key);\n // We store a little wrapper promise that will be extended with\n // _data/_error props upon resolve/reject\n let promise = Promise.race([value, this.abortPromise]).then(data => this.onSettle(promise, key, undefined, data), error => this.onSettle(promise, key, error));\n // Register rejection listeners to avoid uncaught promise rejections on\n // errors or aborted deferred values\n promise.catch(() => {});\n Object.defineProperty(promise, \"_tracked\", {\n get: () => true\n });\n return promise;\n }\n onSettle(promise, key, error, data) {\n if (this.controller.signal.aborted && error instanceof AbortedDeferredError) {\n this.unlistenAbortSignal();\n Object.defineProperty(promise, \"_error\", {\n get: () => error\n });\n return Promise.reject(error);\n }\n this.pendingKeysSet.delete(key);\n if (this.done) {\n // Nothing left to abort!\n this.unlistenAbortSignal();\n }\n // If the promise was resolved/rejected with undefined, we'll throw an error as you\n // should always resolve with a value or null\n if (error === undefined && data === undefined) {\n let undefinedError = new Error(\"Deferred data for key \\\"\" + key + \"\\\" resolved/rejected with `undefined`, \" + \"you must resolve/reject with a value or `null`.\");\n Object.defineProperty(promise, \"_error\", {\n get: () => undefinedError\n });\n this.emit(false, key);\n return Promise.reject(undefinedError);\n }\n if (data === undefined) {\n Object.defineProperty(promise, \"_error\", {\n get: () => error\n });\n this.emit(false, key);\n return Promise.reject(error);\n }\n Object.defineProperty(promise, \"_data\", {\n get: () => data\n });\n this.emit(false, key);\n return data;\n }\n emit(aborted, settledKey) {\n this.subscribers.forEach(subscriber => subscriber(aborted, settledKey));\n }\n subscribe(fn) {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n cancel() {\n this.controller.abort();\n this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));\n this.emit(true);\n }\n async resolveData(signal) {\n let aborted = false;\n if (!this.done) {\n let onAbort = () => this.cancel();\n signal.addEventListener(\"abort\", onAbort);\n aborted = await new Promise(resolve => {\n this.subscribe(aborted => {\n signal.removeEventListener(\"abort\", onAbort);\n if (aborted || this.done) {\n resolve(aborted);\n }\n });\n });\n }\n return aborted;\n }\n get done() {\n return this.pendingKeysSet.size === 0;\n }\n get unwrappedData() {\n invariant(this.data !== null && this.done, \"Can only unwrap data on initialized and settled deferreds\");\n return Object.entries(this.data).reduce((acc, _ref3) => {\n let [key, value] = _ref3;\n return Object.assign(acc, {\n [key]: unwrapTrackedPromise(value)\n });\n }, {});\n }\n get pendingKeys() {\n return Array.from(this.pendingKeysSet);\n }\n}\nfunction isTrackedPromise(value) {\n return value instanceof Promise && value._tracked === true;\n}\nfunction unwrapTrackedPromise(value) {\n if (!isTrackedPromise(value)) {\n return value;\n }\n if (value._error) {\n throw value._error;\n }\n return value._data;\n}\nconst defer = function defer(data, init) {\n if (init === void 0) {\n init = {};\n }\n let responseInit = typeof init === \"number\" ? {\n status: init\n } : init;\n return new DeferredData(data, responseInit);\n};\n/**\n * A redirect response. Sets the status code and the `Location` header.\n * Defaults to \"302 Found\".\n */\nconst redirect = function redirect(url, init) {\n if (init === void 0) {\n init = 302;\n }\n let responseInit = init;\n if (typeof responseInit === \"number\") {\n responseInit = {\n status: responseInit\n };\n } else if (typeof responseInit.status === \"undefined\") {\n responseInit.status = 302;\n }\n let headers = new Headers(responseInit.headers);\n headers.set(\"Location\", url);\n return new Response(null, _extends({}, responseInit, {\n headers\n }));\n};\n/**\n * A redirect response that will force a document reload to the new location.\n * Sets the status code and the `Location` header.\n * Defaults to \"302 Found\".\n */\nconst redirectDocument = (url, init) => {\n let response = redirect(url, init);\n response.headers.set(\"X-Remix-Reload-Document\", \"true\");\n return response;\n};\n/**\n * A redirect response that will perform a `history.replaceState` instead of a\n * `history.pushState` for client-side navigation redirects.\n * Sets the status code and the `Location` header.\n * Defaults to \"302 Found\".\n */\nconst replace = (url, init) => {\n let response = redirect(url, init);\n response.headers.set(\"X-Remix-Replace\", \"true\");\n return response;\n};\n/**\n * @private\n * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies\n *\n * We don't export the class for public use since it's an implementation\n * detail, but we export the interface above so folks can build their own\n * abstractions around instances via isRouteErrorResponse()\n */\nclass ErrorResponseImpl {\n constructor(status, statusText, data, internal) {\n if (internal === void 0) {\n internal = false;\n }\n this.status = status;\n this.statusText = statusText || \"\";\n this.internal = internal;\n if (data instanceof Error) {\n this.data = data.toString();\n this.error = data;\n } else {\n this.data = data;\n }\n }\n}\n/**\n * Check if the given error is an ErrorResponse generated from a 4xx/5xx\n * Response thrown from an action/loader\n */\nfunction isRouteErrorResponse(error) {\n return error != null && typeof error.status === \"number\" && typeof error.statusText === \"string\" && typeof error.internal === \"boolean\" && \"data\" in error;\n}\n\nconst validMutationMethodsArr = [\"post\", \"put\", \"patch\", \"delete\"];\nconst validMutationMethods = new Set(validMutationMethodsArr);\nconst validRequestMethodsArr = [\"get\", ...validMutationMethodsArr];\nconst validRequestMethods = new Set(validRequestMethodsArr);\nconst redirectStatusCodes = new Set([301, 302, 303, 307, 308]);\nconst redirectPreserveMethodStatusCodes = new Set([307, 308]);\nconst IDLE_NAVIGATION = {\n state: \"idle\",\n location: undefined,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n json: undefined,\n text: undefined\n};\nconst IDLE_FETCHER = {\n state: \"idle\",\n data: undefined,\n formMethod: undefined,\n formAction: undefined,\n formEncType: undefined,\n formData: undefined,\n json: undefined,\n text: undefined\n};\nconst IDLE_BLOCKER = {\n state: \"unblocked\",\n proceed: undefined,\n reset: undefined,\n location: undefined\n};\nconst ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i;\nconst defaultMapRouteProperties = route => ({\n hasErrorBoundary: Boolean(route.hasErrorBoundary)\n});\nconst TRANSITIONS_STORAGE_KEY = \"remix-router-transitions\";\n//#endregion\n////////////////////////////////////////////////////////////////////////////////\n//#region createRouter\n////////////////////////////////////////////////////////////////////////////////\n/**\n * Create a router and listen to history POP navigations\n */\nfunction createRouter(init) {\n const routerWindow = init.window ? init.window : typeof window !== \"undefined\" ? window : undefined;\n const isBrowser = typeof routerWindow !== \"undefined\" && typeof routerWindow.document !== \"undefined\" && typeof routerWindow.document.createElement !== \"undefined\";\n const isServer = !isBrowser;\n invariant(init.routes.length > 0, \"You must provide a non-empty routes array to createRouter\");\n let mapRouteProperties;\n if (init.mapRouteProperties) {\n mapRouteProperties = init.mapRouteProperties;\n } else if (init.detectErrorBoundary) {\n // If they are still using the deprecated version, wrap it with the new API\n let detectErrorBoundary = init.detectErrorBoundary;\n mapRouteProperties = route => ({\n hasErrorBoundary: detectErrorBoundary(route)\n });\n } else {\n mapRouteProperties = defaultMapRouteProperties;\n }\n // Routes keyed by ID\n let manifest = {};\n // Routes in tree format for matching\n let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);\n let inFlightDataRoutes;\n let basename = init.basename || \"/\";\n let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;\n let patchRoutesOnNavigationImpl = init.unstable_patchRoutesOnNavigation;\n // Config driven behavior flags\n let future = _extends({\n v7_fetcherPersist: false,\n v7_normalizeFormMethod: false,\n v7_partialHydration: false,\n v7_prependBasename: false,\n v7_relativeSplatPath: false,\n v7_skipActionErrorRevalidation: false\n }, init.future);\n // Cleanup function for history\n let unlistenHistory = null;\n // Externally-provided functions to call on all state changes\n let subscribers = new Set();\n // FIFO queue of previously discovered routes to prevent re-calling on\n // subsequent navigations to the same path\n let discoveredRoutesMaxSize = 1000;\n let discoveredRoutes = new Set();\n // Externally-provided object to hold scroll restoration locations during routing\n let savedScrollPositions = null;\n // Externally-provided function to get scroll restoration keys\n let getScrollRestorationKey = null;\n // Externally-provided function to get current scroll position\n let getScrollPosition = null;\n // One-time flag to control the initial hydration scroll restoration. Because\n // we don't get the saved positions from until _after_\n // the initial render, we need to manually trigger a separate updateState to\n // send along the restoreScrollPosition\n // Set to true if we have `hydrationData` since we assume we were SSR'd and that\n // SSR did the initial scroll restoration.\n let initialScrollRestored = init.hydrationData != null;\n let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);\n let initialErrors = null;\n if (initialMatches == null && !patchRoutesOnNavigationImpl) {\n // If we do not match a user-provided-route, fall back to the root\n // to allow the error boundary to take over\n let error = getInternalRouterError(404, {\n pathname: init.history.location.pathname\n });\n let {\n matches,\n route\n } = getShortCircuitMatches(dataRoutes);\n initialMatches = matches;\n initialErrors = {\n [route.id]: error\n };\n }\n // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and\n // our initial match is a splat route, clear them out so we run through lazy\n // discovery on hydration in case there's a more accurate lazy route match.\n // In SSR apps (with `hydrationData`), we expect that the server will send\n // up the proper matched routes so we don't want to run lazy discovery on\n // initial hydration and want to hydrate into the splat route.\n if (initialMatches && !init.hydrationData) {\n let fogOfWar = checkFogOfWar(initialMatches, dataRoutes, init.history.location.pathname);\n if (fogOfWar.active) {\n initialMatches = null;\n }\n }\n let initialized;\n if (!initialMatches) {\n initialized = false;\n initialMatches = [];\n // If partial hydration and fog of war is enabled, we will be running\n // `patchRoutesOnNavigation` during hydration so include any partial matches as\n // the initial matches so we can properly render `HydrateFallback`'s\n if (future.v7_partialHydration) {\n let fogOfWar = checkFogOfWar(null, dataRoutes, init.history.location.pathname);\n if (fogOfWar.active && fogOfWar.matches) {\n initialMatches = fogOfWar.matches;\n }\n }\n } else if (initialMatches.some(m => m.route.lazy)) {\n // All initialMatches need to be loaded before we're ready. If we have lazy\n // functions around still then we'll need to run them in initialize()\n initialized = false;\n } else if (!initialMatches.some(m => m.route.loader)) {\n // If we've got no loaders to run, then we're good to go\n initialized = true;\n } else if (future.v7_partialHydration) {\n // If partial hydration is enabled, we're initialized so long as we were\n // provided with hydrationData for every route with a loader, and no loaders\n // were marked for explicit hydration\n let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;\n let errors = init.hydrationData ? init.hydrationData.errors : null;\n let isRouteInitialized = m => {\n // No loader, nothing to initialize\n if (!m.route.loader) {\n return true;\n }\n // Explicitly opting-in to running on hydration\n if (typeof m.route.loader === \"function\" && m.route.loader.hydrate === true) {\n return false;\n }\n // Otherwise, initialized if hydrated with data or an error\n return loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined;\n };\n // If errors exist, don't consider routes below the boundary\n if (errors) {\n let idx = initialMatches.findIndex(m => errors[m.route.id] !== undefined);\n initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized);\n } else {\n initialized = initialMatches.every(isRouteInitialized);\n }\n } else {\n // Without partial hydration - we're initialized if we were provided any\n // hydrationData - which is expected to be complete\n initialized = init.hydrationData != null;\n }\n let router;\n let state = {\n historyAction: init.history.action,\n location: init.history.location,\n matches: initialMatches,\n initialized,\n navigation: IDLE_NAVIGATION,\n // Don't restore on initial updateState() if we were SSR'd\n restoreScrollPosition: init.hydrationData != null ? false : null,\n preventScrollReset: false,\n revalidation: \"idle\",\n loaderData: init.hydrationData && init.hydrationData.loaderData || {},\n actionData: init.hydrationData && init.hydrationData.actionData || null,\n errors: init.hydrationData && init.hydrationData.errors || initialErrors,\n fetchers: new Map(),\n blockers: new Map()\n };\n // -- Stateful internal variables to manage navigations --\n // Current navigation in progress (to be committed in completeNavigation)\n let pendingAction = Action.Pop;\n // Should the current navigation prevent the scroll reset if scroll cannot\n // be restored?\n let pendingPreventScrollReset = false;\n // AbortController for the active navigation\n let pendingNavigationController;\n // Should the current navigation enable document.startViewTransition?\n let pendingViewTransitionEnabled = false;\n // Store applied view transitions so we can apply them on POP\n let appliedViewTransitions = new Map();\n // Cleanup function for persisting applied transitions to sessionStorage\n let removePageHideEventListener = null;\n // We use this to avoid touching history in completeNavigation if a\n // revalidation is entirely uninterrupted\n let isUninterruptedRevalidation = false;\n // Use this internal flag to force revalidation of all loaders:\n // - submissions (completed or interrupted)\n // - useRevalidator()\n // - X-Remix-Revalidate (from redirect)\n let isRevalidationRequired = false;\n // Use this internal array to capture routes that require revalidation due\n // to a cancelled deferred on action submission\n let cancelledDeferredRoutes = [];\n // Use this internal array to capture fetcher loads that were cancelled by an\n // action navigation and require revalidation\n let cancelledFetcherLoads = new Set();\n // AbortControllers for any in-flight fetchers\n let fetchControllers = new Map();\n // Track loads based on the order in which they started\n let incrementingLoadId = 0;\n // Track the outstanding pending navigation data load to be compared against\n // the globally incrementing load when a fetcher load lands after a completed\n // navigation\n let pendingNavigationLoadId = -1;\n // Fetchers that triggered data reloads as a result of their actions\n let fetchReloadIds = new Map();\n // Fetchers that triggered redirect navigations\n let fetchRedirectIds = new Set();\n // Most recent href/match for fetcher.load calls for fetchers\n let fetchLoadMatches = new Map();\n // Ref-count mounted fetchers so we know when it's ok to clean them up\n let activeFetchers = new Map();\n // Fetchers that have requested a delete when using v7_fetcherPersist,\n // they'll be officially removed after they return to idle\n let deletedFetchers = new Set();\n // Store DeferredData instances for active route matches. When a\n // route loader returns defer() we stick one in here. Then, when a nested\n // promise resolves we update loaderData. If a new navigation starts we\n // cancel active deferreds for eliminated routes.\n let activeDeferreds = new Map();\n // Store blocker functions in a separate Map outside of router state since\n // we don't need to update UI state if they change\n let blockerFunctions = new Map();\n // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so\n // that we only kick them off once for a given combo\n let pendingPatchRoutes = new Map();\n // Flag to ignore the next history update, so we can revert the URL change on\n // a POP navigation that was blocked by the user without touching router state\n let unblockBlockerHistoryUpdate = undefined;\n // Initialize the router, all side effects should be kicked off from here.\n // Implemented as a Fluent API for ease of:\n // let router = createRouter(init).initialize();\n function initialize() {\n // If history informs us of a POP navigation, start the navigation but do not update\n // state. We'll update our own state once the navigation completes\n unlistenHistory = init.history.listen(_ref => {\n let {\n action: historyAction,\n location,\n delta\n } = _ref;\n // Ignore this event if it was just us resetting the URL from a\n // blocked POP navigation\n if (unblockBlockerHistoryUpdate) {\n unblockBlockerHistoryUpdate();\n unblockBlockerHistoryUpdate = undefined;\n return;\n }\n warning(blockerFunctions.size === 0 || delta != null, \"You are trying to use a blocker on a POP navigation to a location \" + \"that was not created by @remix-run/router. This will fail silently in \" + \"production. This can happen if you are navigating outside the router \" + \"via `window.history.pushState`/`window.location.hash` instead of using \" + \"router navigation APIs. This can also happen if you are using \" + \"createHashRouter and the user manually changes the URL.\");\n let blockerKey = shouldBlockNavigation({\n currentLocation: state.location,\n nextLocation: location,\n historyAction\n });\n if (blockerKey && delta != null) {\n // Restore the URL to match the current UI, but don't update router state\n let nextHistoryUpdatePromise = new Promise(resolve => {\n unblockBlockerHistoryUpdate = resolve;\n });\n init.history.go(delta * -1);\n // Put the blocker into a blocked state\n updateBlocker(blockerKey, {\n state: \"blocked\",\n location,\n proceed() {\n updateBlocker(blockerKey, {\n state: \"proceeding\",\n proceed: undefined,\n reset: undefined,\n location\n });\n // Re-do the same POP navigation we just blocked, after the url\n // restoration is also complete. See:\n // https://github.com/remix-run/react-router/issues/11613\n nextHistoryUpdatePromise.then(() => init.history.go(delta));\n },\n reset() {\n let blockers = new Map(state.blockers);\n blockers.set(blockerKey, IDLE_BLOCKER);\n updateState({\n blockers\n });\n }\n });\n return;\n }\n return startNavigation(historyAction, location);\n });\n if (isBrowser) {\n // FIXME: This feels gross. How can we cleanup the lines between\n // scrollRestoration/appliedTransitions persistance?\n restoreAppliedTransitions(routerWindow, appliedViewTransitions);\n let _saveAppliedTransitions = () => persistAppliedTransitions(routerWindow, appliedViewTransitions);\n routerWindow.addEventListener(\"pagehide\", _saveAppliedTransitions);\n removePageHideEventListener = () => routerWindow.removeEventListener(\"pagehide\", _saveAppliedTransitions);\n }\n // Kick off initial data load if needed. Use Pop to avoid modifying history\n // Note we don't do any handling of lazy here. For SPA's it'll get handled\n // in the normal navigation flow. For SSR it's expected that lazy modules are\n // resolved prior to router creation since we can't go into a fallbackElement\n // UI for SSR'd apps\n if (!state.initialized) {\n startNavigation(Action.Pop, state.location, {\n initialHydration: true\n });\n }\n return router;\n }\n // Clean up a router and it's side effects\n function dispose() {\n if (unlistenHistory) {\n unlistenHistory();\n }\n if (removePageHideEventListener) {\n removePageHideEventListener();\n }\n subscribers.clear();\n pendingNavigationController && pendingNavigationController.abort();\n state.fetchers.forEach((_, key) => deleteFetcher(key));\n state.blockers.forEach((_, key) => deleteBlocker(key));\n }\n // Subscribe to state updates for the router\n function subscribe(fn) {\n subscribers.add(fn);\n return () => subscribers.delete(fn);\n }\n // Update our state and notify the calling context of the change\n function updateState(newState, opts) {\n if (opts === void 0) {\n opts = {};\n }\n state = _extends({}, state, newState);\n // Prep fetcher cleanup so we can tell the UI which fetcher data entries\n // can be removed\n let completedFetchers = [];\n let deletedFetchersKeys = [];\n if (future.v7_fetcherPersist) {\n state.fetchers.forEach((fetcher, key) => {\n if (fetcher.state === \"idle\") {\n if (deletedFetchers.has(key)) {\n // Unmounted from the UI and can be totally removed\n deletedFetchersKeys.push(key);\n } else {\n // Returned to idle but still mounted in the UI, so semi-remains for\n // revalidations and such\n completedFetchers.push(key);\n }\n }\n });\n }\n // Iterate over a local copy so that if flushSync is used and we end up\n // removing and adding a new subscriber due to the useCallback dependencies,\n // we don't get ourselves into a loop calling the new subscriber immediately\n [...subscribers].forEach(subscriber => subscriber(state, {\n deletedFetchers: deletedFetchersKeys,\n unstable_viewTransitionOpts: opts.viewTransitionOpts,\n unstable_flushSync: opts.flushSync === true\n }));\n // Remove idle fetchers from state since we only care about in-flight fetchers.\n if (future.v7_fetcherPersist) {\n completedFetchers.forEach(key => state.fetchers.delete(key));\n deletedFetchersKeys.forEach(key => deleteFetcher(key));\n }\n }\n // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION\n // and setting state.[historyAction/location/matches] to the new route.\n // - Location is a required param\n // - Navigation will always be set to IDLE_NAVIGATION\n // - Can pass any other state in newState\n function completeNavigation(location, newState, _temp) {\n var _location$state, _location$state2;\n let {\n flushSync\n } = _temp === void 0 ? {} : _temp;\n // Deduce if we're in a loading/actionReload state:\n // - We have committed actionData in the store\n // - The current navigation was a mutation submission\n // - We're past the submitting state and into the loading state\n // - The location being loaded is not the result of a redirect\n let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === \"loading\" && ((_location$state = location.state) == null ? void 0 : _location$state._isRedirect) !== true;\n let actionData;\n if (newState.actionData) {\n if (Object.keys(newState.actionData).length > 0) {\n actionData = newState.actionData;\n } else {\n // Empty actionData -> clear prior actionData due to an action error\n actionData = null;\n }\n } else if (isActionReload) {\n // Keep the current data if we're wrapping up the action reload\n actionData = state.actionData;\n } else {\n // Clear actionData on any other completed navigations\n actionData = null;\n }\n // Always preserve any existing loaderData from re-used routes\n let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;\n // On a successful navigation we can assume we got through all blockers\n // so we can start fresh\n let blockers = state.blockers;\n if (blockers.size > 0) {\n blockers = new Map(blockers);\n blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));\n }\n // Always respect the user flag. Otherwise don't reset on mutation\n // submission navigations unless they redirect\n let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;\n // Commit any in-flight routes at the end of the HMR revalidation \"navigation\"\n if (inFlightDataRoutes) {\n dataRoutes = inFlightDataRoutes;\n inFlightDataRoutes = undefined;\n }\n if (isUninterruptedRevalidation) ; else if (pendingAction === Action.Pop) ; else if (pendingAction === Action.Push) {\n init.history.push(location, location.state);\n } else if (pendingAction === Action.Replace) {\n init.history.replace(location, location.state);\n }\n let viewTransitionOpts;\n // On POP, enable transitions if they were enabled on the original navigation\n if (pendingAction === Action.Pop) {\n // Forward takes precedence so they behave like the original navigation\n let priorPaths = appliedViewTransitions.get(state.location.pathname);\n if (priorPaths && priorPaths.has(location.pathname)) {\n viewTransitionOpts = {\n currentLocation: state.location,\n nextLocation: location\n };\n } else if (appliedViewTransitions.has(location.pathname)) {\n // If we don't have a previous forward nav, assume we're popping back to\n // the new location and enable if that location previously enabled\n viewTransitionOpts = {\n currentLocation: location,\n nextLocation: state.location\n };\n }\n } else if (pendingViewTransitionEnabled) {\n // Store the applied transition on PUSH/REPLACE\n let toPaths = appliedViewTransitions.get(state.location.pathname);\n if (toPaths) {\n toPaths.add(location.pathname);\n } else {\n toPaths = new Set([location.pathname]);\n appliedViewTransitions.set(state.location.pathname, toPaths);\n }\n viewTransitionOpts = {\n currentLocation: state.location,\n nextLocation: location\n };\n }\n updateState(_extends({}, newState, {\n actionData,\n loaderData,\n historyAction: pendingAction,\n location,\n initialized: true,\n navigation: IDLE_NAVIGATION,\n revalidation: \"idle\",\n restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),\n preventScrollReset,\n blockers\n }), {\n viewTransitionOpts,\n flushSync: flushSync === true\n });\n // Reset stateful navigation vars\n pendingAction = Action.Pop;\n pendingPreventScrollReset = false;\n pendingViewTransitionEnabled = false;\n isUninterruptedRevalidation = false;\n isRevalidationRequired = false;\n cancelledDeferredRoutes = [];\n }\n // Trigger a navigation event, which can either be a numerical POP or a PUSH\n // replace with an optional submission\n async function navigate(to, opts) {\n if (typeof to === \"number\") {\n init.history.go(to);\n return;\n }\n let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, future.v7_relativeSplatPath, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);\n let {\n path,\n submission,\n error\n } = normalizeNavigateOptions(future.v7_normalizeFormMethod, false, normalizedPath, opts);\n let currentLocation = state.location;\n let nextLocation = createLocation(state.location, path, opts && opts.state);\n // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded\n // URL from window.location, so we need to encode it here so the behavior\n // remains the same as POP and non-data-router usages. new URL() does all\n // the same encoding we'd get from a history.pushState/window.location read\n // without having to touch history\n nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));\n let userReplace = opts && opts.replace != null ? opts.replace : undefined;\n let historyAction = Action.Push;\n if (userReplace === true) {\n historyAction = Action.Replace;\n } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {\n // By default on submissions to the current location we REPLACE so that\n // users don't have to double-click the back button to get to the prior\n // location. If the user redirects to a different location from the\n // action/loader this will be ignored and the redirect will be a PUSH\n historyAction = Action.Replace;\n }\n let preventScrollReset = opts && \"preventScrollReset\" in opts ? opts.preventScrollReset === true : undefined;\n let flushSync = (opts && opts.unstable_flushSync) === true;\n let blockerKey = shouldBlockNavigation({\n currentLocation,\n nextLocation,\n historyAction\n });\n if (blockerKey) {\n // Put the blocker into a blocked state\n updateBlocker(blockerKey, {\n state: \"blocked\",\n location: nextLocation,\n proceed() {\n updateBlocker(blockerKey, {\n state: \"proceeding\",\n proceed: undefined,\n reset: undefined,\n location: nextLocation\n });\n // Send the same navigation through\n navigate(to, opts);\n },\n reset() {\n let blockers = new Map(state.blockers);\n blockers.set(blockerKey, IDLE_BLOCKER);\n updateState({\n blockers\n });\n }\n });\n return;\n }\n return await startNavigation(historyAction, nextLocation, {\n submission,\n // Send through the formData serialization error if we have one so we can\n // render at the right error boundary after we match routes\n pendingError: error,\n preventScrollReset,\n replace: opts && opts.replace,\n enableViewTransition: opts && opts.unstable_viewTransition,\n flushSync\n });\n }\n // Revalidate all current loaders. If a navigation is in progress or if this\n // is interrupted by a navigation, allow this to \"succeed\" by calling all\n // loaders during the next loader round\n function revalidate() {\n interruptActiveLoads();\n updateState({\n revalidation: \"loading\"\n });\n // If we're currently submitting an action, we don't need to start a new\n // navigation, we'll just let the follow up loader execution call all loaders\n if (state.navigation.state === \"submitting\") {\n return;\n }\n // If we're currently in an idle state, start a new navigation for the current\n // action/location and mark it as uninterrupted, which will skip the history\n // update in completeNavigation\n if (state.navigation.state === \"idle\") {\n startNavigation(state.historyAction, state.location, {\n startUninterruptedRevalidation: true\n });\n return;\n }\n // Otherwise, if we're currently in a loading state, just start a new\n // navigation to the navigation.location but do not trigger an uninterrupted\n // revalidation so that history correctly updates once the navigation completes\n startNavigation(pendingAction || state.historyAction, state.navigation.location, {\n overrideNavigation: state.navigation,\n // Proxy through any rending view transition\n enableViewTransition: pendingViewTransitionEnabled === true\n });\n }\n // Start a navigation to the given action/location. Can optionally provide a\n // overrideNavigation which will override the normalLoad in the case of a redirect\n // navigation\n async function startNavigation(historyAction, location, opts) {\n // Abort any in-progress navigations and start a new one. Unset any ongoing\n // uninterrupted revalidations unless told otherwise, since we want this\n // new navigation to update history normally\n pendingNavigationController && pendingNavigationController.abort();\n pendingNavigationController = null;\n pendingAction = historyAction;\n isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true;\n // Save the current scroll position every time we start a new navigation,\n // and track whether we should reset scroll on completion\n saveScrollPosition(state.location, state.matches);\n pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;\n pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;\n let routesToUse = inFlightDataRoutes || dataRoutes;\n let loadingNavigation = opts && opts.overrideNavigation;\n let matches = matchRoutes(routesToUse, location, basename);\n let flushSync = (opts && opts.flushSync) === true;\n let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);\n if (fogOfWar.active && fogOfWar.matches) {\n matches = fogOfWar.matches;\n }\n // Short circuit with a 404 on the root error boundary if we match nothing\n if (!matches) {\n let {\n error,\n notFoundMatches,\n route\n } = handleNavigational404(location.pathname);\n completeNavigation(location, {\n matches: notFoundMatches,\n loaderData: {},\n errors: {\n [route.id]: error\n }\n }, {\n flushSync\n });\n return;\n }\n // Short circuit if it's only a hash change and not a revalidation or\n // mutation submission.\n //\n // Ignore on initial page loads because since the initial load will always\n // be \"same hash\". For example, on /page#hash and submit a